From 48ae2cbfb693e852b63aed9407735e26a865da79 Mon Sep 17 00:00:00 2001 From: AlbertoV Date: Mon, 19 Oct 2026 12:58:03 +0200 Subject: [PATCH] docs: remove repetition from JS feature guides --- js/ERROR_MONITORING.md | 452 ++++++++--------------------------------- js/README.md | 163 +++------------ 2 files changed, 119 insertions(+), 496 deletions(-) diff --git a/js/ERROR_MONITORING.md b/js/ERROR_MONITORING.md index 9da06ba..8e899d0 100644 --- a/js/ERROR_MONITORING.md +++ b/js/ERROR_MONITORING.md @@ -1,128 +1,22 @@ # Traceflow Error Monitoring -Error Monitoring collects JavaScript errors from your applications. Each occurrence -contains the exception, stack, build, context and breadcrumbs. Traceflow groups -occurrences and uses private source maps to show the original code in the dashboard. - -[Configure Traceflow](#configure-traceflow) · [Browser](#browser) · [Nuxt](#nuxt) · -[React Native](#react-native) · [SDK options](#sdk-options) · -[Source maps](#upload-source-maps) · [Troubleshooting](#troubleshooting) +Capture JavaScript errors from web and mobile apps, with context, breadcrumbs +and source-mapped stacks. The [README](README.md#error-monitoring) has the browser +quickstart, also suitable for JavaScript served by Laravel. ## Configure Traceflow -Install the package in your application: - -```sh -npm install @dev.smartpricing/traceflow-sdk -``` - -On the Traceflow backend, apply the client-error schema and configure ingestion -keys. The [backend setup](https://github.com/smartpricing/traceflow-service/blob/main/docs/CLIENT_ERRORS.md#enable-ingestion) -covers the schema, Redis and persistent source-map storage. The SDK does not create -keys or configure the service. - -Set `CLIENT_ERROR_INGEST_KEYS` on the API process to a JSON array such as: - -```json -[ - { - "key": "public-webapp-ingestion-key", - "source": "webapp", - "environment": "production", - "origins": ["https://app.example.com"], - "events_per_minute": 600 - }, - { - "key": "public-mobile-ingestion-key", - "source": "mobile", - "environment": "production", - "allow_originless": true, - "events_per_minute": 600 - } -] -``` - -Replace the example keys and origin. Browser origins must match exactly, including -scheme and port, with no path or trailing slash. React Native normally sends no -`Origin` header, so its key needs `allow_originless: true`. Use separate keys for -web and mobile. Keys for the same source/environment share one quota. - -Ingestion keys are public and can only submit errors for their source/environment. -They cannot read events or upload source maps. Keep the private `X-API-Key` in the -backend or CI environment. Origin restrictions do not establish a user's identity. - -## SDK options - -All three adapters take these options. They set `platform` automatically. - -| Required option | Example | Meaning | -| --- | --- | --- | -| `endpoint` | `https://traceflow.example.com/api/v1/client-errors` | Full ingestion URL, including the path | -| `ingestKey` | `public-webapp-ingestion-key` | Public key configured on the backend | -| `source` | `webapp` | Application name; must match the key | -| `environment` | `production` | Deployment environment; must match the key | -| `release` | `1.0.0` | Application release | -| `dist` | `android-build-42` | Unique identifier for the built files or OTA update | - -Keep `dist` stable for one build. Change it when the generated code changes, even -if `release` stays the same. The app and its source-map upload must use identical -values. A Git commit can serve as `dist` only if it identifies the exact artifact; -use distinct values when platform, build configuration or OTA output differs. - -| Optional option | Default | Use | -| --- | --- | --- | -| `context` | Diagnostic session ID only | Initial application context | -| `sampleRate` | `1` | Fraction captured, from `0` to `1` | -| `maxQueueSize` | `30` | In-memory event limit, from `1` to `100` | -| `beforeSend` | None | Synchronous function to redact or discard an event | -| `onDiagnostic` | None | Callback for delivery retries, drops and pauses | -| `fetch` | Runtime `fetch` | Custom transport with the same signature | - -Import the adapters from their dedicated entry points, which avoid Node/Kafka -dependencies. Do not import the Node tracing client from frontend code. - -## Browser +Configure a public ingestion key for each source/environment on the backend. +Browser keys need the exact allowed origin, including scheme and port. Mobile +keys need `allow_originless: true`. See [backend setup](https://github.com/smartpricing/traceflow-service/blob/main/docs/CLIENT_ERRORS.md#enable-ingestion) +for the schema, keys and source-map storage. -Create a module such as `src/error-monitoring.ts` and import it during client -startup. This also applies to browser assets served by Laravel. - -```ts -import { installBrowserErrors } from '@dev.smartpricing/traceflow-sdk/browser'; - -export const reporting = installBrowserErrors({ - endpoint: 'https://traceflow.example.com/api/v1/client-errors', - ingestKey: 'public-webapp-ingestion-key', - source: 'webapp', - environment: 'production', - release: '1.0.0', - dist: 'build-commit-or-ota-id', -}); - -reporting.client.setContext({ screen: 'quote-editor' }); -reporting.client.addBreadcrumb({ category: 'action', message: 'Save quote' }); - -// Call from your application's save-error handler. -export function reportQuoteSaveFailure(error: unknown, quoteId: string) { - reporting.client.captureException(error, { context: { quote_id: quoteId } }); -} -``` - -Install once, early in application startup. A second `installBrowserErrors` call -on the same window returns the existing installation instead of duplicating -listeners; dispose it first to reinstall with different options. Calling it where -no `window` exists throws. Global errors and unhandled promise -rejections are captured without suppressing the browser's handlers. Page URLs, -user agent and language are included; query strings, fragments and URL credentials -are removed. Popstate/hash changes add navigation breadcrumbs. No form values, -cookies, request bodies, console output or HTTP responses are intercepted. - -`dispose()` removes handlers and drops queued events. Call `await client.flush(2000)` -first if delivery is needed before disposal. A flush returns `false` if events -remain queued; it does not throw for transport failures. +The SDK uses a public `ingestKey`; private API keys stay in the backend or CI. +The ingestion URL must include `/api/v1/client-errors`. ## Nuxt -Declare the public client configuration in `nuxt.config.ts`: +Declare client configuration in `nuxt.config.ts`: ```ts export default defineNuxtConfig({ @@ -144,12 +38,10 @@ export default defineNuxtConfig({ Set `NUXT_PUBLIC_ERROR_MONITORING_INGEST_KEY`, `NUXT_PUBLIC_ERROR_MONITORING_RELEASE` and `NUXT_PUBLIC_ERROR_MONITORING_DIST` -in the deployment environment. Other fields use the same prefix, for example -`NUXT_PUBLIC_ERROR_MONITORING_ENDPOINT`. All these values are public. For a static -deployment, supply them when generating the app. See [Nuxt runtime configuration](https://nuxt.com/docs/4.x/guide/going-further/runtime-config). +in deployment, or at generation time for static apps. Other fields use the same +prefix. These values are public. -Create `plugins/traceflow.client.ts` under your Nuxt source directory (`app/` by -default in Nuxt 4): +Create `plugins/traceflow.client.ts` under your Nuxt source directory: ```ts import { installNuxtErrors } from '@dev.smartpricing/traceflow-sdk/nuxt'; @@ -162,19 +54,15 @@ export default defineNuxtPlugin((nuxtApp) => { }); ``` -In a component or composable, get the client through -`const { $errorReporting } = useNuxtApp()`. Its `captureException`, `setContext` -and `addBreadcrumb` methods are the same as the browser client's. - -The adapter adds `vue:error`, `app:error` and `page:finish` hooks to browser capture. -An Error observed by more than one hook within one second produces one occurrence. -The plugin is client-only; Nuxt SSR errors are not captured. The configuration -above generates hidden client source maps. -Upload them from the build output before deployment and exclude `.map` files from -public hosting. See [Nuxt error hooks](https://nuxt.com/docs/4.x/getting-started/error-handling). +Access the client with `const { $errorReporting } = useNuxtApp()`. +Browser, Vue and Nuxt client errors are captured; SSR errors are not. +A [runnable Nuxt example](https://github.com/smartpricing/traceflow-sdk/blob/main/js/examples/errors/README.md) +is included in the repository. ## React Native +Install before rendering the app, in a module such as `error-monitoring.ts`: + ```ts import { AppState, Platform } from 'react-native'; import { installReactNativeErrors } from '@dev.smartpricing/traceflow-sdk/react-native'; @@ -182,39 +70,27 @@ import { installReactNativeErrors } from '@dev.smartpricing/traceflow-sdk/react- export const reporting = installReactNativeErrors({ endpoint: 'https://traceflow.example.com/api/v1/client-errors', ingestKey: 'public-mobile-ingestion-key', - source: 'mobile', environment: 'production', - release: '1.0.0', dist: 'android-build-42', + source: 'mobile', + environment: 'production', + release: '1.0.0', + dist: 'android-build-42', context: { os: Platform.OS }, }, { appState: AppState, requireCoverage: ['globalErrors'] }); ``` -The adapter wraps the available `ErrorUtils` global handler, calls the previous -handler and restores it on disposal. `coverage.globalErrors` reports whether -that hook exists in the runtime. App state changes create breadcrumbs and request -a short flush when leaving the foreground. Set the current screen with -`client.setContext({ screen: routeName })` from your navigation listener. - -Promise rejection hooks differ across React Native versions and engines. Pass -`subscribeUnhandledRejection(listener)`, returning an unsubscribe function, from -your runtime integration when available. `coverage.unhandledRejections` is false -without it. Set `requireCoverage: ['globalErrors', 'unhandledRejections']` in -the hooks argument to reject startup configuration that lacks a required hook; -failed installation restores the previous handler and removes subscriptions. -The SDK does not replace Hermes's rejection tracker or provide automatic -coverage without a compatible subscription. Handled promise failures -can always use `captureException`. - -For render/lifecycle errors, use the [React boundary](#react-error-boundary). +`requireCoverage` throws if the global JS error hook is unavailable. The adapter +preserves the previous handler and flushes on leaving the foreground. -Hermes `address at` frames retain their zero-based bytecode offset. Upload the -final composed Metro/Hermes source map from the exact Android/iOS build. Use a -new `dist` for each build and OTA update; the release version alone is insufficient. -Native crashes, native symbol files and delivery after process termination are -outside this JS adapter. See [React Native release symbolication](https://reactnative.dev/docs/debugging-release-builds). +Unhandled promise rejections require a runtime-specific +`subscribeUnhandledRejection(listener)` hook returning an unsubscribe function. +Without it, `reporting.coverage.unhandledRejections` is false. Add +`'unhandledRejections'` to `requireCoverage` when supplying that hook. +Native crashes are not captured. Use a new `dist` for each platform build and OTA +update, even when the release version stays the same. -## React error boundary +### React error boundary -Wrap the relevant screen or application subtree. For React Native: +For render/lifecycle errors, wrap a screen or subtree (React 18+): ```tsx import type { ReactNode } from 'react'; @@ -235,238 +111,84 @@ export function MonitoredScreen({ children, screen }: { children: ReactNode; scr } ``` -React DOM uses the same boundary with a web fallback element. React 18+ is required. -The boundary records the component stack and shows `fallback` after a render or -lifecycle error. Changing `resetKey` renders the children again. Event-handler -errors and rejected promises need the runtime adapter or `captureException()`. +React DOM uses the same boundary with a web fallback. Changing `resetKey` retries +rendering. Event handlers and promise failures use the adapter or `captureException()`. ## Context and delivery -Use the installed client's methods from your application's handlers: +`client.setContext({ screen: 'quote-editor', user_id: '42' })` merges fields for +subsequent events. `captureException(error, { context: { quote_id: '12345' } })` +adds context to one occurrence. Call `client.clearContext()` on logout to clear +context and breadcrumbs and start a new diagnostic session. -```ts -import { reporting } from './error-monitoring'; - -reporting.client.setContext({ screen: 'quote-editor', user_id: '42' }); -reporting.client.addBreadcrumb({ - category: 'action', message: 'Save quote', data: { quote_id: '12345' }, -}); - -// In a handler that has caught an error: -export function reportSaveError(error: unknown) { - return reporting.client.captureException(error, { - context: { quote_id: '12345', operation: 'save' }, - }); -} +Optional adapter settings: -// On logout: -reporting.client.clearContext(); -``` - -`captureException()` returns an event ID when queued, or `undefined` when skipped -or dropped. The ID is not a persistence receipt. Context passed to this call -applies only to that occurrence. The same Error object captured again within one -second reuses its ID instead of adding another occurrence. - -`setContext` merges diagnostic fields; `clearContext` clears them and breadcrumbs -and starts a new diagnostic session ID. Call it on logout if user context was -added. The SDK does not infer a user identity. Breadcrumbs are limited to 50; -context values are depth/length bounded, circular-safe, and common secret keys -are replaced with `[Filtered]`. - -Use `beforeSend(event)` to remove application-specific fields or return `null` -to discard an event. Generic masking does not recognize every secret or personal -value in free text. The hook is synchronous; a thrown exception drops that event. -`sampleRate` is 0–1, default 1, and sampling happens before queueing. +| Option | Default | Purpose | +| --- | --- | --- | +| `context` | Session ID | Initial diagnostic fields | +| `sampleRate` | `1` | Capture fraction, from `0` to `1` | +| `maxQueueSize` | `30` | In-memory event limit, from `1` to `100` | +| `beforeSend(event)` | None | Redact data; return `null` to discard | +| `onDiagnostic(info)` | None | Observe drops, retries and auth pauses | +| `fetch` | Runtime `fetch` | Custom transport | -For example, add this option when installing the adapter: +Common secret keys are masked, but free text can still contain sensitive data. +Use synchronous `beforeSend` for application-specific redaction, including +messages and breadcrumbs. Do not attach complete requests or responses. -```ts -import type { ErrorClientOptions } from '@dev.smartpricing/traceflow-sdk/browser'; - -const beforeSend: ErrorClientOptions['beforeSend'] = (event) => { - delete event.context.guest_name; - delete event.context.guest_phone; - // Apply application-specific redaction to free text as well. - event.exception.message = event.exception.message.replace(/booking-reference=\S+/g, 'booking-reference=[Filtered]'); - return event; -}; -``` +Delivery retries network errors, `408`, `429` and `5xx` up to five attempts. +Events expire from the queue after five minutes. `401`/`403` drops queued events +and pauses capture for one minute, doubling on repeated rejection up to 32 minutes. +There is no persistent offline queue. -Redact breadcrumbs and stack text too if your app puts sensitive values there. -Do not send complete request/response objects as context. - -The in-memory queue holds 30 events by default (`maxQueueSize`, maximum 100). -An event above 60 KB first loses its breadcrumbs, then its context and most of -its raw stack; `context.truncated` records what was removed. Only an exception -that is still too large is dropped. A full queue drops the new event. Frames -passed to `captureException` are validated and limited to 100 entries. Delivery -retries network failures, `408`, `429` and `5xx` up to five attempts, preserving -exact bytes and ID. It respects `Retry-After` and uses exponential backoff. -Events older than five minutes are discarded; other `4xx` responses are terminal. -A `401` or `403` means the key or origin is rejected for every event: queued -events are dropped and capture pauses for one minute, doubling up to 32 minutes -while rejections continue (`getStats().paused`, diagnostic type `paused`). The -pause ends on its own; fix the ingestion key or origin configuration. -`getStats()` returns sent, dropped, retried and queued counts, plus `droppedByReason`. An optional -`onDiagnostic` callback reports drops, retries and pauses without event payloads. Its -exceptions are ignored and error capture inside that callback is suppressed to -avoid recursion. No delivery failure creates another telemetry error. - -Browser visibility changes request a flush with `keepalive`. Flush deadlines -apply even if a custom transport ignores abort; late replies are ignored. Closing -the client cancels its pending wait and accounts for queued events once. - -This is best-effort delivery. Closing a tab, terminating an app, prolonged offline -use or fatal JS errors can lose queued events. There is no persistent offline queue. -Do not put a private `X-API-Key` in these clients: ingestion uses its own public, -source/environment-scoped key configured in Traceflow. - -## Find and inspect errors - -In the dashboard, open the errors view and select the source, environment and -time interval. Open a group to see its occurrences, then an occurrence to inspect -the exception, original/mapped frames, context and breadcrumbs. Read queries -accept intervals of at most 31 days; retention is configured on the backend. - -A group is scoped to source/environment, platform, release and dist. The grouping -key uses the exception type and up to five generated stack frames. User IDs, -sessions and message text do not split groups when frames are available. Without -frames, normalized message text is used instead. Builds remain separate, so a -new `dist` creates new groups even for the same underlying bug. - -Counts are exact up to 1,000 occurrences in the selected period. Above that, the -dashboard shows **at least 1,000** and omits the first-seen timestamp; individual -occurrences remain pageable. Uploading maps later improves the displayed stack -without changing the group or replaying events. Cross-build merging, issue -resolution, notifications and native crash collection are not implemented. +`captureException()` returns a queued event ID, not a persistence receipt. +`await client.flush(2000)` attempts delivery; `true` means the queue is empty, +including any dropped events. Check `client.getStats()` for `sent`, `dropped`, +`retries` and `droppedByReason`. `reporting.dispose()` removes hooks and drops +pending events, so flush first if needed. ## Upload source maps -The package includes a Node CLI. Set `TRACEFLOW_API_KEY` as a private CI secret. -Upload after the build and before deploying its files: +Build with source maps, then upload each generated file's map before deployment. +Set `TRACEFLOW_API_KEY` as a private CI secret: ```sh npx traceflow-upload-map \ --url https://traceflow.example.com \ --source webapp --environment production \ --release 1.0.0 --dist build-commit-or-ota-id --platform browser \ - --filename /_nuxt/app.js --map .output/public/_nuxt/app.js.map + --filename /_nuxt/app.ABC.js --map .output/public/_nuxt/app.ABC.js.map ``` -Upload every generated chunk with its exact URL path. For mobile use -`--platform react-native --filename index.android.bundle` (or the filename in the -actual iOS stack) and the matching composed map. All identity fields must match -the event. Maps may be uploaded after an error; refreshing its detail resolves -the new map. Group membership remains build-scoped and unchanged. - -For all Nuxt chunks, the equivalent Bash build step is: - -```bash -set -euo pipefail -: "${TRACEFLOW_API_KEY:?Set the private upload key}" -: "${TRACEFLOW_RELEASE:?Set the same release as the app}" -: "${TRACEFLOW_DIST:?Set the same dist as the app}" - -find .output/public/_nuxt -type f -name '*.js.map' -print0 | - while IFS= read -r -d '' map; do - filename="${map#.output/public}" - npx traceflow-upload-map \ - --url https://traceflow.example.com \ - --source webapp --environment production --platform browser \ - --release "$TRACEFLOW_RELEASE" --dist "$TRACEFLOW_DIST" \ - --filename "${filename%.map}" --map "$map" - done -``` - -For example, `.output/public/_nuxt/app.ABC.js.map` belongs to -`/_nuxt/app.ABC.js`. A CDN URL uses its actual path without host, query or fragment; -include any deployment prefix. There is no basename or nearest-build fallback. - -Maps are immutable for a given build/file identity: uploading the same map again -is accepted, while different content returns `409`. Change `dist` for rebuilt -artifacts. The map limit is 10 MiB; indexed maps must embed their sections rather -than refer to external URLs. Include `sourcesContent` if you want code excerpts -in the dashboard. Keep maps private and retain them for builds still in use and -their event retention period. Event expiration does not delete source maps. - -## Verify an installation - -In a development or staging build, after installing the adapter: - -```ts -import { reporting } from './error-monitoring'; +Use the actual generated path, including any deployment prefix, without host or +query string. Source, environment, release, dist and platform must match the app. +Maps are limited to 10 MiB; replacing a different map under the same identity +returns `409`. Use a new `dist` when generated code changes. -const eventId = reporting.client.captureException(new Error('Error Monitoring setup check'), { - context: { check: 'installation' }, -}); -const drained = await reporting.client.flush(2000); -console.log({ eventId, drained, stats: reporting.client.getStats() }); -``` - -`drained: true` only means the queue is empty; rejected events also leave it. Check -that `sent` increased and find the occurrence in the dashboard using the configured -source/environment and current time interval. Then upload the matching map and -confirm a known source file and line. Trigger an unhandled JS error and promise -rejection separately to verify automatic capture. - -The [Nuxt example](https://github.com/smartpricing/traceflow-sdk/blob/main/js/examples/errors/README.md) has buttons for Vue errors, unhandled -rejections and manual capture. +For React Native, use `--platform react-native` and the bundle filename from the +stack. Upload the final composed Metro/Hermes map from that exact build. See +[React Native source maps](https://reactnative.dev/docs/debugging-release-builds). +Keep maps out of public hosting and retain them for builds still in use. +Late uploads become available on the next detail read; no event replay is needed. ## Troubleshooting -Start with `reporting.client.getStats()`. It exposes `queued`, `sent`, `dropped`, -`retries`, `paused` and `droppedByReason`. For status codes, pass a diagnostic -callback when installing the adapter: - -```ts -import type { ErrorClientOptions } from '@dev.smartpricing/traceflow-sdk/browser'; - -const onDiagnostic: ErrorClientOptions['onDiagnostic'] = (diagnostic) => { - console.warn('Traceflow Error Monitoring delivery', diagnostic); -}; -``` +Capture a test error, flush, confirm that `getStats().sent` increased, then find +it in the dashboard by source/environment and time. Upload its map and verify a +known source line. Test global errors, rejected promises and React render errors +separately on each supported Android/iOS release build, including OTA updates. +Fatal errors or process termination can prevent delivery. -This callback receives delivery metadata, not the error payload. Route it to your -existing health logging if needed; do not call `captureException()` from it. +Groups remain separate across builds. Counts above 1,000 show **at least 1,000**; +individual occurrences are still pageable. See [grouping behavior](https://github.com/smartpricing/traceflow-service/blob/main/docs/CLIENT_ERRORS.md#grouping-and-counts). | Symptom | Check | | --- | --- | -| `captureException()` returns `undefined` | Look at `droppedByReason`; check sampling, `beforeSend`, queue capacity, auth pause and disposal. A closed client does not capture. | -| `401` | The public ingestion key is missing or unknown. A private API key does not work here. | -| `403` | Compare source/environment with the key. Check the exact browser origin or mobile `allow_originless`. | -| `paused: true` | A `401`/`403` paused capture. Fix the configuration; the pause expires automatically, starting at one minute. Reinstall if changing client options. | -| `429` | The source/environment quota is exhausted. Retries respect `Retry-After`; the queue is bounded. | -| `503` or growing `retries` | Check network access and backend/Redis availability. A server operator can inspect the private diagnostics endpoint. | -| `400`, `409`, `410` or `413` | The event was rejected. Check custom `beforeSend` output, device clock, payload size and retained event age. These responses are terminal. | -| Browser network request blocked | Check CSP `connect-src`, HTTPS/mixed content, proxy CORS handling and extensions. Network failures may have no HTTP status. | -| Sent event absent from the view | Check source, environment and time filters. Group counts stop being exact above 1,000. | -| Map upload returns `409` | Different map content already exists for that build/file. Use the build's original map or a new `dist` for a new build. | -| Frame status `missing` | Check every identity field and the exact generated path; verify the map was uploaded and storage is mounted. | -| Frame status `unmapped` | A map exists but has no mapping at that coordinate. Check that it comes from the exact generated build. | -| Frame status `unavailable` | Map processing failed or was overloaded. Retry the detail read and check backend diagnostics. | -| No stack frames | Capture the original Error object where possible. A string message has no stack to map. | - -Backend operators can use `GET /api/v1/client-errors/diagnostics` with a private -API key. See [service operations](https://github.com/smartpricing/traceflow-service/blob/main/docs/CLIENT_ERRORS.md#operations) -for counters, source-map storage and cleanup. - -## Validate a mobile release - -Hook availability is not proof that a runtime emits every error. Before enabling -this as the only reporter, test on each supported Android/iOS release build: - -1. Trigger a global JS error, a rejected promise, and a React render failure. - Confirm the corresponding mechanism, context and one occurrence in Traceflow. -2. Upload that build's composed map and verify a known source filename and line. - Repeat after an OTA update with a different `dist`. -3. Test offline/reconnect, background/foreground, and a fatal JS error. The fatal - hook starts delivery before invoking the previous handler, but process exit can - still lose the event. Native crashes are not captured. -4. Verify required coverage flags, handler restoration on disposal, and delivery - diagnostics when ingestion returns `429` or `503`. - -The repository tests cover hook contracts, stack parsing, retries and cancellation. -They do not certify a specific React Native/Expo/Hermes version or physical device. -Run `npm run typecheck:errors`, `npm run test:errors`, and `npm run build:errors` -to check the frontend distributions without the optional native Kafka dependency. +| No event | Inspect `getStats().droppedByReason` and `paused`; check sampling, `beforeSend`, disposal and network access. | +| `401` / `403` | Public key, source/environment, exact browser origin or mobile `allow_originless`. Capture resumes after the auth pause. | +| `429` / `503` | Quota or service availability. The SDK retries within its queue limits. | +| Map upload `409` | Existing map differs. Use the original artifact or a new build ID. | +| Frame `missing` / `unmapped` | Exact build identity, generated filename and map coordinates. | +| Frame `unavailable` | Retry the detail read; check [backend diagnostics](https://github.com/smartpricing/traceflow-service/blob/main/docs/CLIENT_ERRORS.md#operations). | + +Use `onDiagnostic` to inspect delivery status codes without event payloads. diff --git a/js/README.md b/js/README.md index 0b05b4c..ea59c81 100644 --- a/js/README.md +++ b/js/README.md @@ -44,20 +44,13 @@ await sdk.shutdown(); `trace.fail(error)` and `step.fail(error)` record failures. Explicit trace and step IDs must be UUIDs; omit them to generate IDs. -For asynchronous request context, `runWithTrace(options, fn)` makes the trace -available through `sdk.getCurrentTrace()` and `sdk.startStep()`. It finishes the -trace when the callback returns, or fails it and rethrows when the callback throws. +`runWithTrace(options, fn)` provides request context, finishes on return and fails +on throw. `sdk.getTrace(traceId)` continues a trace; with `silentErrors: true`, a +returned handle does not prove the remote trace exists. -For another service to continue a trace, pass its ID in a request header and use -`sdk.getTrace(traceId)`. HTTP reads live state from the backend; with -`silentErrors: true`, a failed read can still return a local handle, so a handle -alone is not evidence that the trace exists. `sdk.heartbeat(traceId)` refreshes -activity for long-running work. - -HTTP middleware is available as -[`createExpressMiddleware`](src/middleware/express.ts) and -[`traceflowFastifyPlugin`](src/middleware/fastify.ts). Both accept `ignorePaths`. -Full method and option types are in [src/types.ts](src/types.ts). +HTTP middleware: [`createExpressMiddleware`](src/middleware/express.ts) and +[`traceflowFastifyPlugin`](src/middleware/fastify.ts). See [types](src/types.ts) +for their options and the full tracing API. ## Audit Log @@ -85,56 +78,38 @@ const event = audit.createEvent({ const receipt = await audit.record(event); ``` -Search in the dashboard by source, entity type and ID. `actor` identifies who -performed the action; `origin.channel` records `webapp`, `mobile` or another -channel. Set them from the authenticated request and known application context. +Search by source, entity type and ID. Set `actor` and `origin.channel` from the +authenticated request and known application context. | Action | Changes | | --- | --- | | `created` | `auditDiff({}, createdValues)` | | `updated` | `auditDiff(previousValues, currentValues)` | | `deleted` | `auditDiff(deletedValues, {})` | -| `exported` or another custom action | Omit `changes` if no values changed | - -To store complete selected values instead of a diff, use -`changes: { mode: 'snapshot', before: previousValues, after: currentValues }`. +| Custom action, e.g. `exported` | Omit `changes` if no values changed | -This example sends directly. For recovery after a process crash, store `event` -in your application's outbox in the same transaction as the business change. -The publisher must reuse that envelope on every attempt and remove it only after -`record()` succeeds. This package does not provide a Node database outbox. +`auditDiff()` distinguishes absent fields from `null`; nested objects and arrays +are compared as whole fields. Skip empty updates. For complete selected values, +use `changes: { mode: 'snapshot', before, after }`. -`recordAudit(input)` creates and sends an envelope in one call. An existing -`TraceFlowSDK` exposes the same methods through `sdk.audit` and `sdk.recordAudit()`. -Do not call `recordAudit()` again to retry an operation: that generates a new ID -unless one was supplied. +For crash recovery, save `event` in your database outbox in the same transaction +as the business change. Retry `audit.record(event)` with that saved envelope and +remove it only after success. The Node SDK does not provide a database outbox. +`recordAudit(input)` creates a new envelope each time; do not use it for retries. +Existing SDK instances expose `sdk.audit` and `sdk.recordAudit()`. -`AuditDeliveryError` exposes `eventId`, `retryable` and `statusCode`. Network -failures, `408`, `429` and `5xx` are retried with the same payload. A `409` payload -conflict is not retried. Audit delivery failures are thrown regardless of tracing -`silentErrors`; `enabled: false` also throws a retryable delivery error. +Delivery failures throw `AuditDeliveryError` with `eventId`, `retryable` and +`statusCode`, regardless of tracing `silentErrors`. Network errors, `408`, `429` +and `5xx` are retried; `409` payload conflicts are not. -Subject types and IDs are strings. `auditDiff()` distinguishes absent keys from -`null` and compares nested objects/arrays as whole fields. Skip updates whose -diff is empty. Creation uses `{}` before; deletion uses `{}` after. Custom -activities such as `exported` can omit changes. - -Select fields and redact metadata before creating the event. Serialize dates -explicitly; use strings for exact decimals and large integers. The payload limit -is 256 KiB with a maximum JSON depth of 32. See [audit types](src/audit.ts) for -optional context fields and [the backend contract](https://github.com/smartpricing/traceflow-service#audit-log) -for retention, responses and queries. +Select and redact fields before sending. Serialize dates, exact decimals and +large integers as strings. Payloads must fit 256 KiB and JSON depth 32. See +[audit types](src/audit.ts) and the [backend contract](https://github.com/smartpricing/traceflow-service#audit-log). ## Error Monitoring -Traceflow Error Monitoring captures errors from your web and mobile apps, groups -occurrences and reconstructs stacks with source maps. Install one adapter at startup; -use `captureException()` for handled errors. See the [integration guide](ERROR_MONITORING.md) -for backend configuration, SDK options and troubleshooting. - -### Browser - -Use this in browser JavaScript, including assets served by Laravel: +Capture application errors, context and breadcrumbs; inspect grouped occurrences +and readable stacks in Traceflow. Install once in browser startup code: ```ts import { installBrowserErrors } from '@dev.smartpricing/traceflow-sdk/browser'; @@ -158,88 +133,14 @@ export function reportQuoteSaveFailure(error: unknown, quoteId: string) { } ``` -Captures global JS errors and unhandled promise rejections. Requires `window`; -PHP errors are not captured. - -### Nuxt - -Create `plugins/traceflow.client.ts` (under `app/` if that is your Nuxt source -directory). This adapter includes browser capture and Nuxt/Vue error hooks: - -```ts -import { installNuxtErrors } from '@dev.smartpricing/traceflow-sdk/nuxt'; - -export default defineNuxtPlugin((nuxtApp) => { - const reporting = installNuxtErrors(nuxtApp, { - endpoint: 'https://traceflow.example.com/api/v1/client-errors', - ingestKey: 'public-webapp-ingestion-key', - source: 'webapp', - environment: 'production', - release: '1.0.0', - dist: 'build-commit-or-ota-id', - }); - if (import.meta.hot) import.meta.hot.dispose(() => reporting.dispose()); - return { provide: { errorReporting: reporting.client } }; -}); -``` - -In a component or composable, access the client with -`const { $errorReporting } = useNuxtApp()` and call -`$errorReporting.captureException(error)`. Nuxt SSR errors are outside this adapter. - -### React Native - -Install before rendering the app: - -```ts -import { AppState, Platform } from 'react-native'; -import { installReactNativeErrors } from '@dev.smartpricing/traceflow-sdk/react-native'; - -export const reporting = installReactNativeErrors({ - endpoint: 'https://traceflow.example.com/api/v1/client-errors', - ingestKey: 'public-mobile-ingestion-key', - source: 'mobile', - environment: 'production', - release: '1.0.0', - dist: 'android-build-42', - context: { os: Platform.OS }, -}, { appState: AppState, requireCoverage: ['globalErrors'] }); - -// Update from your navigation listener. -reporting.client.setContext({ screen: 'quote-editor' }); -``` - -`requireCoverage` throws if the global JS error hook is unavailable. Unhandled -promise rejections need a runtime-specific hook: see [React Native setup](ERROR_MONITORING.md#react-native). -Use a different `dist` for each Android/iOS build and OTA update. Native crashes -are not captured. - -For React render/lifecycle errors, wrap the app in `TraceflowErrorBoundary` from -`@dev.smartpricing/traceflow-sdk/react`, passing `client={reporting.client}`, -`fallback` and children (React 18+). - -### Readable stacks - -Generate source maps and upload them from CI with a private API key. For Nuxt, -set `sourcemap: { client: 'hidden' }` in `nuxt.config.ts`. For example: - -```sh -# TRACEFLOW_API_KEY is a private secret in the build environment. -npx traceflow-upload-map \ - --url https://traceflow.example.com \ - --source webapp --environment production \ - --release 1.0.0 --dist build-commit-or-ota-id --platform browser \ - --filename /_nuxt/app.js --map .output/public/_nuxt/app.js.map -``` - -Upload each chunk's map using its actual generated path. Source, environment, -release, dist and platform must match the event. Keep `.map` files out of public -hosting. For mobile, upload the matching composed Metro/Hermes map; see -[source-map uploads](ERROR_MONITORING.md#upload-source-maps). +Global JS errors and unhandled promise rejections are captured automatically. +Use `captureException()` for handled errors. The queue is in memory, so closing +a tab or terminating an app can lose pending events. -The error queue is in memory: closing a tab or terminating an app can lose events. -See [context and delivery](ERROR_MONITORING.md#context-and-delivery) for redaction with -`beforeSend`, logout cleanup, `flush()`, diagnostics and retry behavior. +See the Error Monitoring guide for [Nuxt](ERROR_MONITORING.md#nuxt), +[React Native](ERROR_MONITORING.md#react-native), +[source maps](ERROR_MONITORING.md#upload-source-maps) and +[troubleshooting](ERROR_MONITORING.md#troubleshooting). ## Configuration and delivery