@replayio/self-healing-capture 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,6 +3,15 @@
3
3
  Temporary browser capture support for Self Healing while Subtext gains the accessors needed for
4
4
  network exchanges, interactions, identity, metrics, and session context.
5
5
 
6
+ ## Updating an existing installation
7
+
8
+ **Agents: read [UPGRADING.md](./UPGRADING.md) before updating this dependency.**
9
+ It ships in the npm package at
10
+ `node_modules/@replayio/self-healing-capture/UPGRADING.md` and covers changes to
11
+ application-owned initialization and forwarding code, not just the lockfile.
12
+ No original installer conversation is needed. In particular, old installations
13
+ may contain a 256 KiB forwarding check that a dependency update cannot remove.
14
+
6
15
  **Installation and integration instructions live in the
7
16
  [Self Healing setup skill](https://self-healing.replay.io/api/v1/skills/setup-self-healing/SKILL.md).**
8
17
  Follow that skill for account provisioning, browser initialization, server forwarding, automatic reviews,
@@ -15,8 +24,9 @@ the Self Healing skills will describe the replacement and migration.
15
24
  ## Development and release
16
25
 
17
26
  The capture implementation lives in `packages/capture/src`. Change producers here and keep QA’s
18
- independent ingestion schemas and compatibility tests compatible. Installation instructions belong
19
- only in the Self Healing skills.
27
+ independent ingestion schemas and compatibility tests compatible. Initial installation instructions live in the Self Healing skills. Every release
28
+ that changes application integration requirements must add a versioned migration
29
+ to UPGRADING.md and include verification steps.
20
30
 
21
31
  Publish locally with interactive npm authentication and 2FA:
22
32
 
@@ -32,9 +42,10 @@ artifact. npm owns the browser login/security-key/2FA prompts. Credentials and O
32
42
  arguments or repository secrets. Follow npm's authentication prompt when it appears.
33
43
 
34
44
  Use `npm run capture:publish -- --dry-run` to test building and packaging without login or publication.
35
- Registry errors fail the version check; already-published versions are skipped. After publishing,
36
- the script checks that the version is visible on npm. If visibility verification fails, rerun: an
37
- existing version is skipped, never overwritten. Temporary tarballs are removed when the script exits.
45
+ Registry errors fail the version check; already-published versions are skipped. After npm accepts publication,
46
+ the script briefly checks registry visibility. Delayed processing or registry read failures report
47
+ visibility as pending without treating the accepted publication as a failure. Use the printed
48
+ `npm view` command to check later; do not republish. Existing versions are skipped, never overwritten. Temporary tarballs are removed when the script exits.
38
49
 
39
50
  For later releases, update the package version and emitted producer metadata together, update the
40
51
  root lockfile, and run the script from the reviewed revision. The producer test checks that metadata
@@ -50,3 +61,5 @@ An upload failure is reported through `onError` and retained for retry on the ne
50
61
  explicit `flush()`. Later batches are still attempted. Retries preserve event IDs for server deduplication;
51
62
  current session context and metrics are sent after retries so an older batch cannot leave stale counters.
52
63
  `flush()` rejects while any batch remains undelivered, and succeeds once they have all been accepted.
64
+
65
+ Release tests and builds print concise progress; their full output is shown only if they fail.
package/UPGRADING.md ADDED
@@ -0,0 +1,72 @@
1
+ # Upgrading an existing Self Healing integration
2
+
3
+ This guide ships with `@replayio/self-healing-capture`. Coding agents should read it
4
+ when asked to update the package and reconcile the application integration as well
5
+ as its dependency. The original installation conversation is not required.
6
+
7
+ ## Upgrade procedure
8
+
9
+ 1. Read the installed package version and the version being installed. Use the
10
+ application's package manager to update the dependency and lockfile together.
11
+ Apply every migration below between those versions. If the old version is
12
+ unknown, inspect the integration against all migrations.
13
+ 2. Locate the existing `initCapture` call, identity hook, configured same-origin
14
+ capture endpoint, and server forwarding handler. Search for
15
+ `@replayio/self-healing-capture`, `initCapture`, `SELF_HEALING_URL`, and
16
+ `/api/v1/connection/sessions`. Follow the actual configured endpoint rather than
17
+ assuming the example route name.
18
+ 3. Update application-owned installation code as described below. Keep the existing
19
+ account, project connection, credentials, identity integration, and application
20
+ access controls. Do not repeat provisioning or rotate credentials for an upgrade.
21
+ 4. Run the application's relevant checks. Verify through its actual forwarding
22
+ route that captured network and interaction artifacts reach Self Healing.
23
+ Once deployed, exercise a real browser session and confirm HTTP 200 with
24
+ `status: "stored"` and a nonempty `session_id`. An initial metadata-only upload
25
+ or a FullStory recording alone does not verify auxiliary capture.
26
+ 5. Report the dependency version, application-code changes, checks, and session
27
+ delivery evidence. Distinguish local verification from deployed verification.
28
+ Follow the application's normal review/deployment process; do not claim the
29
+ deployed integration is updated until it has been verified.
30
+
31
+ For the current complete setup contract, see
32
+ https://self-healing.replay.io/api/v1/skills/setup-self-healing/SKILL.md.
33
+ Use it to reconcile the existing integration; do not treat an upgrade as a new setup.
34
+
35
+ ## 0.1.3 — Packaged upgrade instructions
36
+
37
+ No runtime integration changes beyond the migrations below. This release includes
38
+ this guide so upgrades can be performed without access to the installer conversation.
39
+
40
+ ## 0.1.2 — One configurable limit per network exchange
41
+
42
+ - The embedder option is `maxNetworkCaptureBytes`, defaulting to 1,000,000 bytes.
43
+ It measures the UTF-8 JSON encoding of each exchange, including both bodies,
44
+ headers, and event metadata. Omit the option to use the default; preserve an
45
+ intentional application-specific setting. If installation code uses the
46
+ pre-release `maxBatchBytes` option, replace it with this per-exchange policy.
47
+ - Oversized exchanges are skipped and counted in `dropped_network_count`; later
48
+ captures continue. Batching is internal and includes space for envelope overhead.
49
+ A full-size exchange therefore produces an upload slightly larger than its limit.
50
+ - Earlier setup examples installed a **256 KiB request-size check in the app's
51
+ forwarding handler**. Inspect for `256 * 1024`, `262144`, `413`, or the message
52
+ `Capture exceeds Self Healing request limit` along the capture route. Remove that
53
+ capture-specific check if present. Do not replace it with a 1 MB upload check.
54
+ Inspect capture-route body-parser configuration for the same obsolete restriction;
55
+ do not change unrelated application routes.
56
+ - Forward the original JSON body to `/api/v1/connection/sessions` using server-held
57
+ Self Healing credentials and return the upstream status/body. Do not truncate
58
+ capture fields, reimplement batching, or expose credentials in the browser.
59
+ - Verify an exchange with a body larger than 256 KiB but below the configured
60
+ exchange limit (for example, 500 KB under the default) is actually delivered
61
+ through the forwarding route with its body intact. Also verify a later small
62
+ exchange after an oversized exchange, and check the dropped counter increases.
63
+
64
+ Updating the npm dependency cannot modify an application's existing forwarding
65
+ handler. These application-code migrations are part of the package upgrade.
66
+
67
+ ## 0.1.1 — Capture decoding and retry fixes
68
+
69
+ No application-code migration is required. Keep capture initialization and the
70
+ existing identity hook. Failed uploads remain retryable; later batches continue.
71
+ Binary, invalid UTF-8, and NUL-containing bodies are represented as unavailable
72
+ while their exchange metadata remains captured.
package/dist/index.d.ts CHANGED
@@ -2,6 +2,8 @@ export interface CaptureOptions {
2
2
  orgId: string;
3
3
  /** Same-origin POST route holding the server-side credential. */
4
4
  endpoint?: string;
5
+ /** Maximum UTF-8 JSON bytes per network exchange (bodies, headers and metadata). Defaults to 1,000,000. */
6
+ maxNetworkCaptureBytes?: number;
5
7
  onError?: (error: Error) => void;
6
8
  }
7
9
  export interface CaptureController {
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { FullStory, init } from "@fullstory/browser";
2
- import { decodeCaptureBody, splitBatches } from "./transport.js";
2
+ import { decodeCaptureBody, splitBatches, DEFAULT_MAX_NETWORK_CAPTURE_BYTES, } from "./transport.js";
3
3
  let active;
4
4
  let activeOptions;
5
5
  const CAPTURED_SESSION_INTERACTION_EVENTS = [
@@ -17,7 +17,6 @@ function countsAsSessionInteraction(event) {
17
17
  event.type === "keydown" &&
18
18
  ["Enter", "Escape"].includes(event.key)));
19
19
  }
20
- const MAX_BODY_BYTES = 1_000_000;
21
20
  const MAX_CAPTURE_BYTES = 8_000_000;
22
21
  const MAX_CAPTURED_INTERACTIONS = 5_000;
23
22
  const ACTIONABLE_SELECTOR = 'button, a[href], input, select, textarea, summary, [role="button"], [role="link"], [role="checkbox"], [role="menuitem"], [role="option"], [role="radio"], [role="switch"], [role="tab"], [contenteditable="true"]';
@@ -40,9 +39,16 @@ function identifyFullStoryUser(user) {
40
39
  export function initCapture(options) {
41
40
  if (typeof window === "undefined")
42
41
  throw new Error("initCapture must run in the browser");
42
+ const maxNetworkCaptureBytes = options.maxNetworkCaptureBytes ?? DEFAULT_MAX_NETWORK_CAPTURE_BYTES;
43
+ if (!Number.isSafeInteger(maxNetworkCaptureBytes) ||
44
+ maxNetworkCaptureBytes <= 0)
45
+ throw new Error("maxNetworkCaptureBytes must be a positive safe integer");
43
46
  if (active) {
44
47
  if (options.orgId !== activeOptions?.orgId ||
45
- options.endpoint !== activeOptions?.endpoint) {
48
+ options.endpoint !== activeOptions?.endpoint ||
49
+ maxNetworkCaptureBytes !==
50
+ (activeOptions?.maxNetworkCaptureBytes ??
51
+ DEFAULT_MAX_NETWORK_CAPTURE_BYTES)) {
46
52
  throw new Error("Capture is already initialized with different options");
47
53
  }
48
54
  return active;
@@ -138,10 +144,8 @@ export function initCapture(options) {
138
144
  session.userEmail = email;
139
145
  uploadCapture(session);
140
146
  };
141
- async function boundedBody(value) {
147
+ async function captureBody(value) {
142
148
  const bytes = await value.clone().arrayBuffer();
143
- if (bytes.byteLength > MAX_BODY_BYTES)
144
- return null;
145
149
  return decodeCaptureBody(bytes);
146
150
  }
147
151
  async function sendCaptureBatch(body) {
@@ -192,7 +196,7 @@ export function initCapture(options) {
192
196
  namespace: "session",
193
197
  key: "capture-producer",
194
198
  schema_version: 1,
195
- payload: { name: "@replayio/self-healing-capture", version: "0.1.1" },
199
+ payload: { name: "@replayio/self-healing-capture", version: "0.1.3" },
196
200
  },
197
201
  ...(context !== session.queuedContext || session.pendingBatches.length > 0
198
202
  ? [
@@ -273,7 +277,7 @@ export function initCapture(options) {
273
277
  batches = splitBatches({
274
278
  session_url: session.sessionUrl,
275
279
  auxiliary_data: auxiliaryData,
276
- });
280
+ }, maxNetworkCaptureBytes);
277
281
  }
278
282
  catch (error) {
279
283
  reportError(error);
@@ -448,7 +452,7 @@ export function initCapture(options) {
448
452
  const capturedAt = performance.timeOrigin + sourceTimestamp;
449
453
  const exchangeId = crypto.randomUUID();
450
454
  const requestBody = request.method !== "GET" && request.method !== "HEAD"
451
- ? boundedBody(request).catch(() => null)
455
+ ? captureBody(request).catch(() => null)
452
456
  : Promise.resolve(null);
453
457
  const responsePromise = nativeFetch(request);
454
458
  inFlight.add(responsePromise);
@@ -481,16 +485,20 @@ export function initCapture(options) {
481
485
  request_headers: Object.fromEntries(request.headers.entries()),
482
486
  request_body: capturedRequestBody,
483
487
  response_headers: Object.fromEntries(clone.headers.entries()),
484
- response_body: responseBytes && responseBytes.byteLength <= MAX_BODY_BYTES
485
- ? decodeCaptureBody(responseBytes)
486
- : null,
488
+ response_body: responseBytes ? decodeCaptureBody(responseBytes) : null,
487
489
  ...(startedBeforeReady &&
488
490
  (request.method === "GET" || request.method === "HEAD") &&
489
- responseBytes &&
490
- responseBytes.byteLength <= MAX_BODY_BYTES
491
+ responseBytes
491
492
  ? { startup_body: responseBytes, status_text: clone.statusText }
492
493
  : {}),
493
494
  };
495
+ const { startup_body: _startup, status_text: _status, ...uploadedExchange } = exchange;
496
+ if (new TextEncoder().encode(JSON.stringify(uploadedExchange)).byteLength >
497
+ maxNetworkCaptureBytes) {
498
+ session.droppedNetworkCount++;
499
+ uploadCapture(session);
500
+ return;
501
+ }
494
502
  session.capturedBytes += exchangeBytes;
495
503
  session.capturedExchanges.push(exchange);
496
504
  uploadCapture(session);
@@ -9,8 +9,10 @@ export interface CaptureBatch {
9
9
  session_url: string;
10
10
  auxiliary_data: Artifact[];
11
11
  }
12
- export declare const MAX_BATCH_BYTES: number;
12
+ export declare const DEFAULT_MAX_NETWORK_CAPTURE_BYTES = 1000000;
13
+ /** Derive the upload budget from the capture limit, allowing one full exchange plus its envelope. */
14
+ export declare function uploadBatchBytes(sessionUrl: string, maxNetworkCaptureBytes?: number): number;
13
15
  /** Split between whole events, preserving IDs and payloads for identical retries. */
14
- export declare function splitBatches(input: CaptureBatch, maxBytes?: number): string[];
16
+ export declare function splitBatches(input: CaptureBatch, maxNetworkCaptureBytes?: number): string[];
15
17
  /** Binary or NUL-containing bodies cannot be represented in the text-only capture schema. */
16
18
  export declare function decodeCaptureBody(bytes: ArrayBuffer): string | null;
package/dist/transport.js CHANGED
@@ -1,6 +1,22 @@
1
- export const MAX_BATCH_BYTES = 256 * 1024;
1
+ export const DEFAULT_MAX_NETWORK_CAPTURE_BYTES = 1_000_000;
2
+ /** Derive the upload budget from the capture limit, allowing one full exchange plus its envelope. */
3
+ export function uploadBatchBytes(sessionUrl, maxNetworkCaptureBytes = DEFAULT_MAX_NETWORK_CAPTURE_BYTES) {
4
+ return (maxNetworkCaptureBytes +
5
+ new TextEncoder().encode(JSON.stringify({
6
+ session_url: sessionUrl,
7
+ auxiliary_data: [
8
+ {
9
+ namespace: "network",
10
+ key: "captured-exchanges",
11
+ schema_version: 1,
12
+ payload: { version: 1, exchanges: [] },
13
+ },
14
+ ],
15
+ })).byteLength);
16
+ }
2
17
  /** Split between whole events, preserving IDs and payloads for identical retries. */
3
- export function splitBatches(input, maxBytes = MAX_BATCH_BYTES) {
18
+ export function splitBatches(input, maxNetworkCaptureBytes = DEFAULT_MAX_NETWORK_CAPTURE_BYTES) {
19
+ const maxBytes = uploadBatchBytes(input.session_url, maxNetworkCaptureBytes);
4
20
  const encode = (artifacts) => JSON.stringify({
5
21
  session_url: input.session_url,
6
22
  auxiliary_data: artifacts,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@replayio/self-healing-capture",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "FullStory auxiliary capture for Self Healing and compatible session ingesters",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -13,7 +13,8 @@
13
13
  },
14
14
  "files": [
15
15
  "dist",
16
- "README.md"
16
+ "README.md",
17
+ "UPGRADING.md"
17
18
  ],
18
19
  "sideEffects": false,
19
20
  "license": "UNLICENSED",