@newtalaria/browser 0.1.5 → 0.1.7
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 +34 -2
- package/dist/client.d.ts +12 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +252 -22
- package/dist/index.js.map +3 -3
- package/dist/replay/capture_outcome.d.ts +18 -0
- package/dist/replay/capture_outcome.d.ts.map +1 -0
- package/dist/replay/hooks.d.ts +35 -2
- package/dist/replay/hooks.d.ts.map +1 -1
- package/dist/replay/segment_buffer.d.ts +2 -2
- package/dist/replay/segment_buffer.d.ts.map +1 -1
- package/dist/talaria.browser.iife.js +250 -22
- package/dist/talaria.browser.iife.js.map +3 -3
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Durable tags on error events when an error-clip replay was attempted. */
|
|
2
|
+
export declare const REPLAY_CAPTURE_TAG = "replay.capture";
|
|
3
|
+
export declare const REPLAY_CAPTURE_REASON_TAG = "replay.capture_reason";
|
|
4
|
+
export type ReplayCaptureStatus = 'ok' | 'failed' | 'skipped';
|
|
5
|
+
export type ReplayCaptureReason = 'oversized_full_snapshot' | 'no_full_snapshot' | 'upload_failed' | 'not_sampled' | 'buffer_empty';
|
|
6
|
+
export interface ReplayCaptureFailure {
|
|
7
|
+
reason: ReplayCaptureReason;
|
|
8
|
+
details?: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
export interface ReplayCaptureOutcome {
|
|
11
|
+
status: ReplayCaptureStatus;
|
|
12
|
+
reason?: ReplayCaptureReason;
|
|
13
|
+
details?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
/** Merge replay capture tags into event tags (error-clip path only). */
|
|
16
|
+
export declare function applyReplayCaptureTags(tags: Record<string, string>, outcome: ReplayCaptureOutcome | null): Record<string, string>;
|
|
17
|
+
export declare function mergeReplayCaptureExtra(extra: Record<string, unknown> | undefined, outcome: ReplayCaptureOutcome | null): Record<string, unknown> | undefined;
|
|
18
|
+
//# sourceMappingURL=capture_outcome.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capture_outcome.d.ts","sourceRoot":"","sources":["../../src/replay/capture_outcome.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,eAAO,MAAM,kBAAkB,mBAAmB,CAAC;AACnD,eAAO,MAAM,yBAAyB,0BAA0B,CAAC;AAEjE,MAAM,MAAM,mBAAmB,GAAG,IAAI,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE9D,MAAM,MAAM,mBAAmB,GAC3B,yBAAyB,GACzB,kBAAkB,GAClB,eAAe,GACf,aAAa,GACb,cAAc,CAAC;AAEnB,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,mBAAmB,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,mBAAmB,CAAC;IAC5B,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,wEAAwE;AACxE,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,oBAAoB,GAAG,IAAI,GACnC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQxB;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC1C,OAAO,EAAE,oBAAoB,GAAG,IAAI,GACnC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAarC"}
|
package/dist/replay/hooks.d.ts
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
|
+
import type { FailedRequestStatusCode } from '../types.js';
|
|
1
2
|
export type Teardown = () => void;
|
|
3
|
+
export interface NetworkMeta {
|
|
4
|
+
method: string;
|
|
5
|
+
url: string;
|
|
6
|
+
status?: number;
|
|
7
|
+
durationMs?: number;
|
|
8
|
+
ok?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface NetworkHookOptions {
|
|
11
|
+
/** Still emit rrweb breadcrumbs for all requests. */
|
|
12
|
+
onNetwork?: (meta: NetworkMeta) => void;
|
|
13
|
+
/**
|
|
14
|
+
* Called when a completed request should become a Talaria event.
|
|
15
|
+
* Network transport errors (no status) are not promoted here — they rethrow as usual.
|
|
16
|
+
*/
|
|
17
|
+
onFailedRequest?: (meta: NetworkMeta & {
|
|
18
|
+
status: number;
|
|
19
|
+
}) => void;
|
|
20
|
+
captureFailedRequests?: boolean;
|
|
21
|
+
failedRequestStatusCodes?: FailedRequestStatusCode[];
|
|
22
|
+
failedRequestIgnoreUrls?: string[];
|
|
23
|
+
/** SDK base URL — requests under this host + /events|/replays are never promoted. */
|
|
24
|
+
talariaBaseUrl?: string;
|
|
25
|
+
}
|
|
2
26
|
/** Mirror console output into rrweb as `talaria-console` custom events. */
|
|
3
27
|
export declare function installConsoleHook(): Teardown;
|
|
4
|
-
|
|
5
|
-
export declare function
|
|
28
|
+
export declare function statusMatches(status: number, codes: FailedRequestStatusCode[]): boolean;
|
|
29
|
+
export declare function shouldPromoteFailedRequest(meta: NetworkMeta, opts: {
|
|
30
|
+
captureFailedRequests: boolean;
|
|
31
|
+
failedRequestStatusCodes: FailedRequestStatusCode[];
|
|
32
|
+
failedRequestIgnoreUrls: string[];
|
|
33
|
+
talariaBaseUrl?: string;
|
|
34
|
+
}): meta is NetworkMeta & {
|
|
35
|
+
status: number;
|
|
36
|
+
};
|
|
37
|
+
/** Capture fetch / XHR metadata (no bodies, no auth headers); optionally promote failures. */
|
|
38
|
+
export declare function installNetworkHook(options?: NetworkHookOptions): Teardown;
|
|
6
39
|
//# sourceMappingURL=hooks.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../src/replay/hooks.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../src/replay/hooks.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAE3D,MAAM,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC;AAElC,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,EAAE,CAAC,EAAE,OAAO,CAAC;CACd;AAED,MAAM,WAAW,kBAAkB;IACjC,qDAAqD;IACrD,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,CAAC;IACxC;;;OAGG;IACH,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,GAAG;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACnE,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,wBAAwB,CAAC,EAAE,uBAAuB,EAAE,CAAC;IACrD,uBAAuB,CAAC,EAAE,MAAM,EAAE,CAAC;IACnC,qFAAqF;IACrF,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAiBD,2EAA2E;AAC3E,wBAAgB,kBAAkB,IAAI,QAAQ,CA2B7C;AAcD,wBAAgB,aAAa,CAC3B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,uBAAuB,EAAE,GAC/B,OAAO,CAUT;AAED,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,WAAW,EACjB,IAAI,EAAE;IACJ,qBAAqB,EAAE,OAAO,CAAC;IAC/B,wBAAwB,EAAE,uBAAuB,EAAE,CAAC;IACpD,uBAAuB,EAAE,MAAM,EAAE,CAAC;IAClC,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,GACA,IAAI,IAAI,WAAW,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAoB1C;AAED,8FAA8F;AAC9F,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,kBAAuB,GAAG,QAAQ,CA+F7E"}
|
|
@@ -8,13 +8,13 @@ export interface RrwebEvent {
|
|
|
8
8
|
* Soft uncompressed take size before gzip fitting.
|
|
9
9
|
* Keep in sync with ReplayLimits.maxUncompressedSegmentBytes intent.
|
|
10
10
|
*/
|
|
11
|
-
export declare const SEGMENT_SIZE_BYTES =
|
|
11
|
+
export declare const SEGMENT_SIZE_BYTES = 800000;
|
|
12
12
|
/**
|
|
13
13
|
* Server hard cap — keep in sync with ReplayLimits.maxCompressedSegmentBytes.
|
|
14
14
|
* Restart Serverpod after changing this constant pair.
|
|
15
15
|
*/
|
|
16
16
|
export declare const MAX_COMPRESSED_SEGMENT_BYTES: number;
|
|
17
|
-
/** Pack under hard cap minus margin (never disagree with a live
|
|
17
|
+
/** Pack under hard cap minus margin (never disagree with a live 1MiB server). */
|
|
18
18
|
export declare const TARGET_COMPRESSED_SEGMENT_BYTES: number;
|
|
19
19
|
/**
|
|
20
20
|
* Absolute max segments per replay (session / long error mode).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"segment_buffer.d.ts","sourceRoot":"","sources":["../../src/replay/segment_buffer.ts"],"names":[],"mappings":"AAEA,oDAAoD;AACpD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,eAAO,MAAM,kBAAkB,SAAU,CAAC;AAC1C;;;GAGG;AACH,eAAO,MAAM,4BAA4B,
|
|
1
|
+
{"version":3,"file":"segment_buffer.d.ts","sourceRoot":"","sources":["../../src/replay/segment_buffer.ts"],"names":[],"mappings":"AAEA,oDAAoD;AACpD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,eAAO,MAAM,kBAAkB,SAAU,CAAC;AAC1C;;;GAGG;AACH,eAAO,MAAM,4BAA4B,QAAc,CAAC;AACxD,iFAAiF;AACjF,eAAO,MAAM,+BAA+B,QACF,CAAC;AAC3C;;;GAGG;AACH,eAAO,MAAM,uBAAuB,MAAM,CAAC;AAC3C;;GAEG;AACH,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAC1C,uEAAuE;AACvE,eAAO,MAAM,+BAA+B,QAAkB,CAAC;AAC/D,8DAA8D;AAC9D,eAAO,MAAM,yBAAyB,OAAQ,CAAC;AAC/C,+BAA+B;AAC/B,eAAO,MAAM,gBAAgB,OAAQ,CAAC;AACtC,oEAAoE;AACpE,eAAO,MAAM,cAAc,QAAS,CAAC;AACrC,6DAA6D;AAC7D,eAAO,MAAM,qBAAqB,UAAY,CAAC;AAC/C,2DAA2D;AAC3D,eAAO,MAAM,qBAAqB,QAAS,CAAC;AAC5C;;;GAGG;AACH,eAAO,MAAM,sBAAsB,QAAgB,CAAC;AAEpD,mCAAmC;AACnC,eAAO,MAAM,mBAAmB,IAAI,CAAC;AACrC,2BAA2B;AAC3B,eAAO,MAAM,UAAU,IAAI,CAAC;AAE5B,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,YAAY,CAAK;IAEzB,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAK7B,eAAe,IAAI,OAAO;IAI1B;;;OAGG;IACH,kBAAkB,IAAI,OAAO;IAc7B;;;OAGG;IACH,QAAQ,CAAC,GAAG,SAAa,EAAE,QAAQ,SAAwB,GAAG,IAAI;IAiBlE,iBAAiB,IAAI,OAAO;IAI5B;;;;OAIG;IACH,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,EAAE;IAyBpD,OAAO,IAAI,UAAU,EAAE;IAOvB,6EAA6E;IAC7E,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,IAAI;IAMnC,KAAK,IAAI,IAAI;IAKb,SAAS,IAAI;QAAE,SAAS,EAAE,IAAI,CAAC;QAAC,OAAO,EAAE,IAAI,CAAA;KAAE,GAAG,IAAI;IAUtD,OAAO,CAAC,cAAc;CAGvB"}
|
|
@@ -123,9 +123,9 @@
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
// src/replay/segment_buffer.ts
|
|
126
|
-
var SEGMENT_SIZE_BYTES =
|
|
127
|
-
var MAX_COMPRESSED_SEGMENT_BYTES =
|
|
128
|
-
var TARGET_COMPRESSED_SEGMENT_BYTES = MAX_COMPRESSED_SEGMENT_BYTES -
|
|
126
|
+
var SEGMENT_SIZE_BYTES = 8e5;
|
|
127
|
+
var MAX_COMPRESSED_SEGMENT_BYTES = 1024 * 1024;
|
|
128
|
+
var TARGET_COMPRESSED_SEGMENT_BYTES = MAX_COMPRESSED_SEGMENT_BYTES - 64 * 1024;
|
|
129
129
|
var MAX_SEGMENTS_PER_REPLAY = 200;
|
|
130
130
|
var MAX_SEGMENTS_ERROR_CLIP = 12;
|
|
131
131
|
var MAX_ERROR_CLIP_COMPRESSED_BYTES = 2 * 1024 * 1024;
|
|
@@ -357,6 +357,33 @@
|
|
|
357
357
|
return null;
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
+
// src/replay/capture_outcome.ts
|
|
361
|
+
var REPLAY_CAPTURE_TAG = "replay.capture";
|
|
362
|
+
var REPLAY_CAPTURE_REASON_TAG = "replay.capture_reason";
|
|
363
|
+
function applyReplayCaptureTags(tags, outcome) {
|
|
364
|
+
if (!outcome) return tags;
|
|
365
|
+
const next = { ...tags };
|
|
366
|
+
next[REPLAY_CAPTURE_TAG] = outcome.status;
|
|
367
|
+
if (outcome.reason) {
|
|
368
|
+
next[REPLAY_CAPTURE_REASON_TAG] = outcome.reason;
|
|
369
|
+
}
|
|
370
|
+
return next;
|
|
371
|
+
}
|
|
372
|
+
function mergeReplayCaptureExtra(extra, outcome) {
|
|
373
|
+
if (!outcome || outcome.status === "ok") {
|
|
374
|
+
return extra;
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
...extra ?? {},
|
|
378
|
+
replayCapture: {
|
|
379
|
+
attempted: outcome.status === "failed",
|
|
380
|
+
status: outcome.status,
|
|
381
|
+
reason: outcome.reason,
|
|
382
|
+
...outcome.details ?? {}
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
360
387
|
// ../../node_modules/rrweb/dist/rrweb.js
|
|
361
388
|
var __defProp = Object.defineProperty;
|
|
362
389
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
@@ -13216,11 +13243,52 @@
|
|
|
13216
13243
|
} catch {
|
|
13217
13244
|
}
|
|
13218
13245
|
}
|
|
13219
|
-
function
|
|
13246
|
+
function statusMatches(status, codes) {
|
|
13247
|
+
for (const entry of codes) {
|
|
13248
|
+
if (typeof entry === "number") {
|
|
13249
|
+
if (status === entry) return true;
|
|
13250
|
+
} else if (Array.isArray(entry) && entry.length >= 2) {
|
|
13251
|
+
const [min, max] = entry;
|
|
13252
|
+
if (status >= min && status <= max) return true;
|
|
13253
|
+
}
|
|
13254
|
+
}
|
|
13255
|
+
return false;
|
|
13256
|
+
}
|
|
13257
|
+
function shouldPromoteFailedRequest(meta, opts) {
|
|
13258
|
+
if (!opts.captureFailedRequests) return false;
|
|
13259
|
+
if (typeof meta.status !== "number" || Number.isNaN(meta.status)) return false;
|
|
13260
|
+
if (!statusMatches(meta.status, opts.failedRequestStatusCodes)) return false;
|
|
13261
|
+
const url = meta.url || "";
|
|
13262
|
+
const ignore = [...opts.failedRequestIgnoreUrls];
|
|
13263
|
+
const base = (opts.talariaBaseUrl ?? "").replace(/\/+$/, "");
|
|
13264
|
+
if (base) {
|
|
13265
|
+
ignore.push(`${base}/events/`, `${base}/replays/`);
|
|
13266
|
+
}
|
|
13267
|
+
ignore.push("/events/ingest", "/events/ingestBatch", "/replays/");
|
|
13268
|
+
const lower = url.toLowerCase();
|
|
13269
|
+
for (const part of ignore) {
|
|
13270
|
+
if (part && lower.includes(part.toLowerCase())) return false;
|
|
13271
|
+
}
|
|
13272
|
+
return true;
|
|
13273
|
+
}
|
|
13274
|
+
function installNetworkHook(options = {}) {
|
|
13220
13275
|
const originalFetch = typeof fetch === "function" ? fetch.bind(globalThis) : null;
|
|
13221
13276
|
const XHR = typeof XMLHttpRequest !== "undefined" ? XMLHttpRequest : null;
|
|
13222
13277
|
const originalOpen = XHR?.prototype.open;
|
|
13223
13278
|
const originalSend = XHR?.prototype.send;
|
|
13279
|
+
const matchOpts = {
|
|
13280
|
+
captureFailedRequests: options.captureFailedRequests ?? true,
|
|
13281
|
+
failedRequestStatusCodes: options.failedRequestStatusCodes ?? [[500, 599]],
|
|
13282
|
+
failedRequestIgnoreUrls: options.failedRequestIgnoreUrls ?? [],
|
|
13283
|
+
talariaBaseUrl: options.talariaBaseUrl
|
|
13284
|
+
};
|
|
13285
|
+
const handleMeta = (meta) => {
|
|
13286
|
+
emitNetwork(meta);
|
|
13287
|
+
options.onNetwork?.(meta);
|
|
13288
|
+
if (shouldPromoteFailedRequest(meta, matchOpts)) {
|
|
13289
|
+
options.onFailedRequest?.(meta);
|
|
13290
|
+
}
|
|
13291
|
+
};
|
|
13224
13292
|
if (originalFetch) {
|
|
13225
13293
|
globalThis.fetch = async (input2, init) => {
|
|
13226
13294
|
const started = Date.now();
|
|
@@ -13228,7 +13296,7 @@
|
|
|
13228
13296
|
const url = typeof input2 === "string" ? input2 : input2 instanceof URL ? input2.toString() : input2.url;
|
|
13229
13297
|
try {
|
|
13230
13298
|
const response = await originalFetch(input2, init);
|
|
13231
|
-
|
|
13299
|
+
handleMeta({
|
|
13232
13300
|
method,
|
|
13233
13301
|
url,
|
|
13234
13302
|
status: response.status,
|
|
@@ -13237,7 +13305,7 @@
|
|
|
13237
13305
|
});
|
|
13238
13306
|
return response;
|
|
13239
13307
|
} catch (error) {
|
|
13240
|
-
|
|
13308
|
+
handleMeta({
|
|
13241
13309
|
method,
|
|
13242
13310
|
url,
|
|
13243
13311
|
durationMs: Date.now() - started,
|
|
@@ -13256,7 +13324,7 @@
|
|
|
13256
13324
|
XHR.prototype.send = function(body) {
|
|
13257
13325
|
this.__talariaStarted = Date.now();
|
|
13258
13326
|
const onDone = () => {
|
|
13259
|
-
|
|
13327
|
+
handleMeta({
|
|
13260
13328
|
method: this.__talariaMethod ?? "GET",
|
|
13261
13329
|
url: this.__talariaUrl ?? "",
|
|
13262
13330
|
status: this.status,
|
|
@@ -13300,7 +13368,10 @@
|
|
|
13300
13368
|
blockSelector: options.blockSelector ?? "",
|
|
13301
13369
|
userId: options.userId,
|
|
13302
13370
|
tags: options.tags,
|
|
13303
|
-
disableDefaultIntegrations: options.disableDefaultIntegrations ?? false
|
|
13371
|
+
disableDefaultIntegrations: options.disableDefaultIntegrations ?? false,
|
|
13372
|
+
captureFailedRequests: options.captureFailedRequests ?? true,
|
|
13373
|
+
failedRequestStatusCodes: options.failedRequestStatusCodes ?? [[500, 599]],
|
|
13374
|
+
failedRequestIgnoreUrls: options.failedRequestIgnoreUrls ?? []
|
|
13304
13375
|
};
|
|
13305
13376
|
}
|
|
13306
13377
|
function clamp01(n2) {
|
|
@@ -13357,6 +13428,8 @@
|
|
|
13357
13428
|
* attach it to the error event after budget/timer finish resets buffer mode.
|
|
13358
13429
|
*/
|
|
13359
13430
|
this.linkableReplayId = null;
|
|
13431
|
+
/** Last error-clip paint/upload failure (cleared on successful segment 0). */
|
|
13432
|
+
this.lastReplayCaptureFailure = null;
|
|
13360
13433
|
}
|
|
13361
13434
|
init(options) {
|
|
13362
13435
|
if (this.options) {
|
|
@@ -13378,6 +13451,7 @@
|
|
|
13378
13451
|
this.uploadStartedAtMs = null;
|
|
13379
13452
|
this.errorClipDeadlineMs = null;
|
|
13380
13453
|
this.linkableReplayId = null;
|
|
13454
|
+
this.lastReplayCaptureFailure = null;
|
|
13381
13455
|
this.buffer = new SegmentBuffer();
|
|
13382
13456
|
this.uploadChain = Promise.resolve();
|
|
13383
13457
|
this.sessionSampled = Math.random() < this.options.replaysSessionSampleRate;
|
|
@@ -13388,7 +13462,35 @@
|
|
|
13388
13462
|
blockSelector: this.options.blockSelector,
|
|
13389
13463
|
onEvent: (event) => this.onRrwebEvent(event)
|
|
13390
13464
|
});
|
|
13391
|
-
this.teardowns.push(
|
|
13465
|
+
this.teardowns.push(
|
|
13466
|
+
installConsoleHook(),
|
|
13467
|
+
installNetworkHook({
|
|
13468
|
+
captureFailedRequests: this.options.captureFailedRequests,
|
|
13469
|
+
failedRequestStatusCodes: this.options.failedRequestStatusCodes,
|
|
13470
|
+
failedRequestIgnoreUrls: this.options.failedRequestIgnoreUrls,
|
|
13471
|
+
talariaBaseUrl: this.options.baseUrl,
|
|
13472
|
+
onFailedRequest: (meta) => {
|
|
13473
|
+
const status = meta.status;
|
|
13474
|
+
const method = meta.method || "GET";
|
|
13475
|
+
const url = meta.url || "(unknown url)";
|
|
13476
|
+
void this.captureMessage(
|
|
13477
|
+
`HTTP ${status}: ${method} ${url}`,
|
|
13478
|
+
status >= 500 ? "error" : "warning",
|
|
13479
|
+
{
|
|
13480
|
+
tags: {
|
|
13481
|
+
"http.status_code": String(status),
|
|
13482
|
+
"http.method": method
|
|
13483
|
+
},
|
|
13484
|
+
extra: {
|
|
13485
|
+
url,
|
|
13486
|
+
durationMs: meta.durationMs,
|
|
13487
|
+
ok: meta.ok
|
|
13488
|
+
}
|
|
13489
|
+
}
|
|
13490
|
+
);
|
|
13491
|
+
}
|
|
13492
|
+
})
|
|
13493
|
+
);
|
|
13392
13494
|
if (!this.options.disableDefaultIntegrations) {
|
|
13393
13495
|
this.installGlobalHandlers();
|
|
13394
13496
|
}
|
|
@@ -13511,6 +13613,7 @@
|
|
|
13511
13613
|
this.uploadChain = Promise.resolve();
|
|
13512
13614
|
this.closed = false;
|
|
13513
13615
|
this.linkableReplayId = null;
|
|
13616
|
+
this.lastReplayCaptureFailure = null;
|
|
13514
13617
|
}
|
|
13515
13618
|
onRrwebEvent(event) {
|
|
13516
13619
|
if (this.closed || !this.options) return;
|
|
@@ -13536,15 +13639,22 @@
|
|
|
13536
13639
|
throw new Error("@newtalaria/browser: call Talaria.init() first");
|
|
13537
13640
|
}
|
|
13538
13641
|
const isErrorLike = args.level === "error" || args.level === "fatal";
|
|
13642
|
+
let errorClipOutcome = null;
|
|
13643
|
+
let attemptedErrorClip = false;
|
|
13539
13644
|
if (isErrorLike && !this.sessionSampled) {
|
|
13540
13645
|
if (!this.uploadEnabled) {
|
|
13541
13646
|
if (Math.random() < this.options.replaysOnErrorSampleRate) {
|
|
13647
|
+
attemptedErrorClip = true;
|
|
13648
|
+
this.lastReplayCaptureFailure = null;
|
|
13542
13649
|
this.uploadEnabled = true;
|
|
13543
13650
|
this.checkoutFullSnapshot();
|
|
13544
13651
|
this.markUploadStarted();
|
|
13545
13652
|
this.scheduleErrorClipEnd();
|
|
13653
|
+
} else {
|
|
13654
|
+
errorClipOutcome = { status: "skipped", reason: "not_sampled" };
|
|
13546
13655
|
}
|
|
13547
13656
|
} else {
|
|
13657
|
+
attemptedErrorClip = true;
|
|
13548
13658
|
this.scheduleErrorClipEnd();
|
|
13549
13659
|
}
|
|
13550
13660
|
}
|
|
@@ -13553,15 +13663,49 @@
|
|
|
13553
13663
|
await this.flush({ reason: "capture", keepalive: args.keepalive });
|
|
13554
13664
|
} catch (error) {
|
|
13555
13665
|
console.warn("@newtalaria/browser: replay flush failed", error);
|
|
13556
|
-
|
|
13666
|
+
this.lastReplayCaptureFailure = {
|
|
13667
|
+
reason: "upload_failed",
|
|
13668
|
+
details: {
|
|
13669
|
+
message: error instanceof Error ? error.message : String(error)
|
|
13670
|
+
}
|
|
13671
|
+
};
|
|
13672
|
+
if (!this.sessionSampled && this.segmentIndex === 0) {
|
|
13673
|
+
await this.abortUnusableClip("upload_failed", {
|
|
13674
|
+
message: error instanceof Error ? error.message : String(error)
|
|
13675
|
+
});
|
|
13676
|
+
}
|
|
13557
13677
|
}
|
|
13558
13678
|
}
|
|
13559
|
-
|
|
13560
|
-
|
|
13561
|
-
|
|
13562
|
-
}
|
|
13563
|
-
|
|
13564
|
-
|
|
13679
|
+
let replayId = this.uploadEnabled && this.segmentIndex > 0 ? this.replayId : this.linkableReplayId;
|
|
13680
|
+
if (this.lastReplayCaptureFailure && this.segmentIndex === 0) {
|
|
13681
|
+
replayId = null;
|
|
13682
|
+
}
|
|
13683
|
+
if (isErrorLike && !this.sessionSampled) {
|
|
13684
|
+
if (replayId) {
|
|
13685
|
+
errorClipOutcome = { status: "ok" };
|
|
13686
|
+
this.lastReplayCaptureFailure = null;
|
|
13687
|
+
} else if (errorClipOutcome?.status === "skipped") {
|
|
13688
|
+
} else if (this.lastReplayCaptureFailure) {
|
|
13689
|
+
errorClipOutcome = {
|
|
13690
|
+
status: "failed",
|
|
13691
|
+
reason: this.lastReplayCaptureFailure.reason,
|
|
13692
|
+
details: this.lastReplayCaptureFailure.details
|
|
13693
|
+
};
|
|
13694
|
+
} else if (attemptedErrorClip) {
|
|
13695
|
+
errorClipOutcome = { status: "failed", reason: "buffer_empty" };
|
|
13696
|
+
}
|
|
13697
|
+
}
|
|
13698
|
+
const tags = applyReplayCaptureTags(
|
|
13699
|
+
{
|
|
13700
|
+
...this.options.tags ?? {},
|
|
13701
|
+
...args.context?.tags ?? {}
|
|
13702
|
+
},
|
|
13703
|
+
errorClipOutcome
|
|
13704
|
+
);
|
|
13705
|
+
const extra = mergeReplayCaptureExtra(
|
|
13706
|
+
args.context?.extra,
|
|
13707
|
+
errorClipOutcome
|
|
13708
|
+
);
|
|
13565
13709
|
try {
|
|
13566
13710
|
await ingestEvent(this.transport, {
|
|
13567
13711
|
message: args.message,
|
|
@@ -13573,7 +13717,7 @@
|
|
|
13573
13717
|
release: this.options.release,
|
|
13574
13718
|
userId: args.context?.userId ?? this.options.userId,
|
|
13575
13719
|
sessionId: this.sessionId ?? void 0,
|
|
13576
|
-
replayId,
|
|
13720
|
+
replayId: replayId ?? void 0,
|
|
13577
13721
|
url: typeof location !== "undefined" ? location.href : void 0,
|
|
13578
13722
|
tags: Object.keys(tags).length ? tags : void 0,
|
|
13579
13723
|
extraJson: extra ? JSON.stringify(extra) : void 0,
|
|
@@ -13718,9 +13862,10 @@
|
|
|
13718
13862
|
/**
|
|
13719
13863
|
* Ensure segment 0 begins at Meta+FullSnapshot. Orphan increments alone
|
|
13720
13864
|
* produce a blank rrweb player.
|
|
13865
|
+
* @returns false when no FullSnapshot is available.
|
|
13721
13866
|
*/
|
|
13722
13867
|
prepareBufferForNewReplay() {
|
|
13723
|
-
if (this.segmentIndex !== 0) return;
|
|
13868
|
+
if (this.segmentIndex !== 0) return true;
|
|
13724
13869
|
if (!this.buffer.hasFullSnapshot()) {
|
|
13725
13870
|
this.checkoutFullSnapshot();
|
|
13726
13871
|
}
|
|
@@ -13728,6 +13873,40 @@
|
|
|
13728
13873
|
console.warn(
|
|
13729
13874
|
"@newtalaria/browser: replay has no FullSnapshot; player may show a blank screen"
|
|
13730
13875
|
);
|
|
13876
|
+
return false;
|
|
13877
|
+
}
|
|
13878
|
+
return true;
|
|
13879
|
+
}
|
|
13880
|
+
/**
|
|
13881
|
+
* Stop an error clip that cannot paint (missing/oversized FullSnapshot).
|
|
13882
|
+
* Does not set linkableReplayId — the error event should not open a blank player.
|
|
13883
|
+
*/
|
|
13884
|
+
async abortUnusableClip(reason, details) {
|
|
13885
|
+
this.lastReplayCaptureFailure = { reason, details };
|
|
13886
|
+
console.warn("@newtalaria/browser: aborting unusable replay clip", {
|
|
13887
|
+
reason,
|
|
13888
|
+
...details
|
|
13889
|
+
});
|
|
13890
|
+
this.buffer.clear();
|
|
13891
|
+
this.linkableReplayId = null;
|
|
13892
|
+
if (this.startedOnServer && !this.finishedOnServer && this.transport && this.replayId) {
|
|
13893
|
+
try {
|
|
13894
|
+
await finishReplay(this.transport, {
|
|
13895
|
+
replayId: this.replayId,
|
|
13896
|
+
reason: `capture_failed:${reason}`,
|
|
13897
|
+
keepalive: false
|
|
13898
|
+
});
|
|
13899
|
+
this.finishedOnServer = true;
|
|
13900
|
+
} catch (error) {
|
|
13901
|
+
console.warn("@newtalaria/browser: replays/finish failed after abort", error);
|
|
13902
|
+
}
|
|
13903
|
+
}
|
|
13904
|
+
if (!this.sessionSampled) {
|
|
13905
|
+
this.resetToBufferMode();
|
|
13906
|
+
} else {
|
|
13907
|
+
this.uploadEnabled = false;
|
|
13908
|
+
this.clearErrorClipTimer();
|
|
13909
|
+
this.clearMaxDurationTimer();
|
|
13731
13910
|
}
|
|
13732
13911
|
}
|
|
13733
13912
|
stopUploadingAfterLimit() {
|
|
@@ -13788,7 +13967,10 @@
|
|
|
13788
13967
|
async uploadPendingSegments(opts) {
|
|
13789
13968
|
if (!this.options || !this.transport || !this.replayId) return;
|
|
13790
13969
|
if (!this.startedOnServer || this.finishedOnServer) return;
|
|
13791
|
-
this.prepareBufferForNewReplay()
|
|
13970
|
+
if (!this.prepareBufferForNewReplay()) {
|
|
13971
|
+
await this.abortUnusableClip("no_full_snapshot");
|
|
13972
|
+
return;
|
|
13973
|
+
}
|
|
13792
13974
|
while (this.buffer.length > 0) {
|
|
13793
13975
|
if (this.isPastMaxDuration()) {
|
|
13794
13976
|
break;
|
|
@@ -13828,7 +14010,17 @@
|
|
|
13828
14010
|
}
|
|
13829
14011
|
const fitted = await this.takeFittedSegment();
|
|
13830
14012
|
if (!fitted) break;
|
|
14013
|
+
if (fitted.abortReason) {
|
|
14014
|
+
await this.abortUnusableClip(fitted.abortReason, fitted.abortDetails);
|
|
14015
|
+
return;
|
|
14016
|
+
}
|
|
13831
14017
|
const { events, gzip } = fitted;
|
|
14018
|
+
if (this.segmentIndex === 0 && !events.some((e) => e.type === RRWEB_FULL_SNAPSHOT)) {
|
|
14019
|
+
await this.abortUnusableClip("no_full_snapshot", {
|
|
14020
|
+
segmentEventTypes: events.map((e) => e.type)
|
|
14021
|
+
});
|
|
14022
|
+
return;
|
|
14023
|
+
}
|
|
13832
14024
|
if (this.isErrorClipMode() && this.uploadedCompressedBytes + gzip.length > MAX_ERROR_CLIP_COMPRESSED_BYTES && this.segmentIndex > 0) {
|
|
13833
14025
|
this.buffer.clear();
|
|
13834
14026
|
await this.finishOnServer({
|
|
@@ -13853,6 +14045,9 @@
|
|
|
13853
14045
|
});
|
|
13854
14046
|
this.segmentIndex = index2 + 1;
|
|
13855
14047
|
this.uploadedCompressedBytes += gzip.length;
|
|
14048
|
+
if (index2 === 0) {
|
|
14049
|
+
this.lastReplayCaptureFailure = null;
|
|
14050
|
+
}
|
|
13856
14051
|
} catch (error) {
|
|
13857
14052
|
if (isTerminalReplayLimitError(error)) {
|
|
13858
14053
|
console.warn(
|
|
@@ -13869,10 +14064,17 @@
|
|
|
13869
14064
|
if (isOversizedSegmentError(error)) {
|
|
13870
14065
|
const plan = planOversizedRetry(events);
|
|
13871
14066
|
if (plan.action === "drop") {
|
|
14067
|
+
const droppedType = events[0]?.type;
|
|
13872
14068
|
console.warn(
|
|
13873
14069
|
"@newtalaria/browser: dropping rrweb event rejected as oversized",
|
|
13874
|
-
{ type:
|
|
14070
|
+
{ type: droppedType }
|
|
13875
14071
|
);
|
|
14072
|
+
if (this.segmentIndex === 0 && droppedType === RRWEB_FULL_SNAPSHOT) {
|
|
14073
|
+
await this.abortUnusableClip("oversized_full_snapshot", {
|
|
14074
|
+
source: "server_reject"
|
|
14075
|
+
});
|
|
14076
|
+
return;
|
|
14077
|
+
}
|
|
13876
14078
|
continue;
|
|
13877
14079
|
}
|
|
13878
14080
|
this.buffer.prepend(plan.right);
|
|
@@ -13881,6 +14083,11 @@
|
|
|
13881
14083
|
}
|
|
13882
14084
|
this.buffer.prepend(events);
|
|
13883
14085
|
console.warn("@newtalaria/browser: replays/ingestSegment failed", error);
|
|
14086
|
+
if (this.segmentIndex === 0) {
|
|
14087
|
+
await this.abortUnusableClip("upload_failed", {
|
|
14088
|
+
message: error instanceof Error ? error.message : String(error)
|
|
14089
|
+
});
|
|
14090
|
+
}
|
|
13884
14091
|
break;
|
|
13885
14092
|
}
|
|
13886
14093
|
if (opts.keepalive) break;
|
|
@@ -13895,7 +14102,8 @@
|
|
|
13895
14102
|
}
|
|
13896
14103
|
/**
|
|
13897
14104
|
* Pull the largest event prefix that gzips under the target compressed size.
|
|
13898
|
-
* Drops a single event that can never fit
|
|
14105
|
+
* Drops a single event that can never fit — except FullSnapshot on segment 0
|
|
14106
|
+
* (abort the clip instead of uploading a blank orphan stream).
|
|
13899
14107
|
*/
|
|
13900
14108
|
async takeFittedSegment() {
|
|
13901
14109
|
const chunk = this.buffer.takeByEstimatedBytes(SEGMENT_SIZE_BYTES);
|
|
@@ -13907,12 +14115,32 @@
|
|
|
13907
14115
|
MAX_COMPRESSED_SEGMENT_BYTES
|
|
13908
14116
|
);
|
|
13909
14117
|
if (!fitted) {
|
|
14118
|
+
const dropped = chunk[0];
|
|
13910
14119
|
console.warn(
|
|
13911
14120
|
"@newtalaria/browser: dropping rrweb event that exceeds max segment size",
|
|
13912
|
-
{ type:
|
|
14121
|
+
{ type: dropped?.type }
|
|
13913
14122
|
);
|
|
14123
|
+
if (this.segmentIndex === 0 && dropped?.type === RRWEB_FULL_SNAPSHOT) {
|
|
14124
|
+
return {
|
|
14125
|
+
events: [],
|
|
14126
|
+
gzip: new Uint8Array(),
|
|
14127
|
+
abortReason: "oversized_full_snapshot",
|
|
14128
|
+
abortDetails: { source: "fit_compressed_prefix" }
|
|
14129
|
+
};
|
|
14130
|
+
}
|
|
14131
|
+
if (chunk.length > 1) {
|
|
14132
|
+
this.buffer.prepend(chunk.slice(1));
|
|
14133
|
+
}
|
|
13914
14134
|
return this.takeFittedSegment();
|
|
13915
14135
|
}
|
|
14136
|
+
if (this.segmentIndex === 0 && chunk.some((e) => e.type === RRWEB_FULL_SNAPSHOT) && !fitted.events.some((e) => e.type === RRWEB_FULL_SNAPSHOT)) {
|
|
14137
|
+
return {
|
|
14138
|
+
events: [],
|
|
14139
|
+
gzip: new Uint8Array(),
|
|
14140
|
+
abortReason: "oversized_full_snapshot",
|
|
14141
|
+
abortDetails: { source: "fit_dropped_full_snapshot" }
|
|
14142
|
+
};
|
|
14143
|
+
}
|
|
13916
14144
|
if (fitted.remainder.length > 0) {
|
|
13917
14145
|
this.buffer.prepend(fitted.remainder);
|
|
13918
14146
|
}
|