@openreplay/tracker 17.2.9 → 17.2.11

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.
@@ -24,6 +24,12 @@ declare class CanvasRecorder {
24
24
  private isProcessingQueue;
25
25
  constructor(app: App, options: Options);
26
26
  startTracking(): void;
27
+ /**
28
+ * Reacts to a runtime sanitization change on a canvas: stop capturing if it
29
+ * just became masked, start if it just became visible. (Already-sent frames
30
+ * can't be retracted — escalation only stops future capture.)
31
+ */
32
+ resanitizeCanvas: (node: Node, id: number) => void;
27
33
  restartTracking: () => void;
28
34
  captureCanvas: (node: Node) => void;
29
35
  recordCanvas: (node: Node, id: number) => void;
@@ -8,7 +8,7 @@ import Nodes from './nodes/index.js';
8
8
  import type { Options as ObserverOptions } from './observer/top_observer.js';
9
9
  import Observer from './observer/top_observer.js';
10
10
  import type { Options as SanitizerOptions } from './sanitizer.js';
11
- import Sanitizer from './sanitizer.js';
11
+ import Sanitizer, { SanitizeLevel } from './sanitizer.js';
12
12
  import type { Options as SessOptions } from './session.js';
13
13
  import Session from './session.js';
14
14
  import Ticker from './ticker.js';
@@ -129,6 +129,7 @@ export default class App {
129
129
  readonly ticker: Ticker;
130
130
  readonly projectKey: string;
131
131
  readonly sanitizer: Sanitizer;
132
+ private readonly resanitizeCallbacks;
132
133
  readonly debug: Logger;
133
134
  readonly notify: Logger;
134
135
  readonly session: Session;
@@ -170,11 +171,72 @@ export default class App {
170
171
  /** used by child iframes for crossdomain only */
171
172
  parentActive: boolean;
172
173
  checkStatus: () => boolean;
174
+ /** child-side crossdomain debug state (only meaningful when insideIframe) */
175
+ private lastTokenReceived;
176
+ private lastParentMsgAt;
177
+ private lastSentToParentAt;
178
+ private iframeDebugInterval;
179
+ /** stamp every outbound post to the parent window, for the child debug snapshot */
180
+ private markSentToParent;
181
+ /**
182
+ * Child-side counterpart of emitCrossdomainDebug: once per minute an iframe posts an
183
+ * encoded snapshot of its own tracking state up to the parent, which records it as a
184
+ * console log. Posted directly (not via this.send) so it is reported even when the
185
+ * child is NOT active — an inactive/orphaned child is exactly what we want to catch.
186
+ */
187
+ private emitIframeDebug;
173
188
  parentCrossDomainFrameListener: (event: MessageEvent) => void;
174
189
  trackedFrames: string[];
190
+ /** every context that has been enrolled at least once, to tell an orphan (re-adopt) apart
191
+ * from a brand-new child still mid-enrollment (leave alone). */
192
+ private everTrackedFrames;
175
193
  private frameLastSeen;
194
+ /** crossdomain debug diagnostics, reported once per minute as an encoded console log */
195
+ private frameOrigin;
196
+ private frameAnyLastSeen;
197
+ private frameBatchLastSeen;
198
+ private frameLastSent;
199
+ private xdomainDebugInterval;
200
+ /** last time we re-adopted a given orphaned context, to avoid restart spam */
201
+ private reAdoptCooldown;
202
+ private readonly RE_ADOPT_COOLDOWN_MS;
203
+ /**
204
+ * Stable, collision-free frame-order allocation. Node ids are partitioned by
205
+ * (frameLevel, frameOrder) via pack() — every (level, order) owns its own id block, so
206
+ * two simultaneously-live frames sharing an order at the same level corrupt each other's
207
+ * node trees and one stops rendering. The previous `trackedFrames.findIndex+1` derived
208
+ * order from a mutable array index, and pruneStaleFrames()'s .filter() shifts those
209
+ * indices, so a newly enrolled frame could be handed an order still in use by a live
210
+ * (but pruned) frame. We instead assign each context a persistent order, unique among all
211
+ * non-recycled contexts at its level, freed only when the context is GC'd (truly gone).
212
+ */
213
+ private frameAlloc;
214
+ private usedOrdersByLevel;
215
+ private allocateFrameOrder;
216
+ private freeFrameOrder;
176
217
  private readonly FRAME_STALE_MS;
177
218
  private pruneStaleFrames;
219
+ /** records the last command/signal we posted to a given child iframe context (debug) */
220
+ private recordSentToFrame;
221
+ /**
222
+ * Self-heal for the "kill-then-prune orphan" race: a live child can fall out of
223
+ * `trackedFrames` (its 250ms poll was delayed past FRAME_STALE_MS during the parent's
224
+ * stop/start NotActive gap, so pruneStaleFrames evicted it). It keeps polling but the
225
+ * only re-enrollment path is an `iframeSignal`, which a stopped/active-but-orphaned
226
+ * child never re-emits — so it would record nothing forever. When we (the parent) are
227
+ * active and see a poll from an un-tracked context, push a `startIframe` so the child
228
+ * restarts, re-runs the full handshake and re-observes with a fresh rootId. Cooldowned
229
+ * so we don't spam restarts during the child's start window.
230
+ */
231
+ private reAdoptOrphanFrame;
232
+ /**
233
+ * Once per minute: emit an encoded console log from the parent tracker describing every
234
+ * tracked child iframe and the freshness of our two-way communication with it. Lets us
235
+ * see in replay which crossdomain iframe went silent and on which leg of the handshake.
236
+ */
237
+ /** drop debug entries for contexts we have neither heard from nor messaged in this long */
238
+ private readonly XDOMAIN_DEBUG_RETENTION_MS;
239
+ private emitCrossdomainDebug;
178
240
  crossDomainIframeListener: (event: MessageEvent) => void;
179
241
  /**
180
242
  * { command : [remaining iframes] }
@@ -292,6 +354,10 @@ export default class App {
292
354
  private userStartCallback?;
293
355
  private _start;
294
356
  restartCanvasTracking: () => void;
357
+ attachResanitizeCallback: (cb: (node: Node, id: number) => void) => void;
358
+ callResanitizeCallbacks: (node: Node, id: number) => void;
359
+ resanitize: (el?: Element) => void;
360
+ checkSanitization: (el: Node) => SanitizeLevel | undefined;
295
361
  flushBuffer: (buffer: Message[]) => Promise<unknown>;
296
362
  waitStart(): Promise<unknown>;
297
363
  waitStarted(): Promise<unknown>;
@@ -53,6 +53,16 @@ export default abstract class Observer {
53
53
  private commitNode;
54
54
  private commitNodes;
55
55
  protected observeRoot(node: Node, beforeCommit: (id?: number) => unknown, nodeToBind?: Node): void;
56
+ /**
57
+ * Re-evaluates sanitization for every tracked node in `root`'s subtree against
58
+ * the current DOM and re-emits whatever changed. Pass the highest node you
59
+ * changed (or the document root) so inherited levels propagate correctly.
60
+ */
61
+ resanitizeSubtree(root: Node): void;
62
+ private resanitizeNode;
63
+ private recreateSubtree;
64
+ private clearSubtreeRegistration;
65
+ private reemitNode;
56
66
  disconnect(): void;
57
67
  }
58
68
  export {};
@@ -35,8 +35,7 @@ export interface Options {
35
35
  }
36
36
  export declare const stringWiper: (input: string) => string;
37
37
  export default class Sanitizer {
38
- private readonly obscured;
39
- private readonly hidden;
38
+ private readonly levels;
40
39
  private readonly options;
41
40
  readonly privateMode: boolean;
42
41
  private readonly app;
@@ -44,7 +43,10 @@ export default class Sanitizer {
44
43
  app: App;
45
44
  options?: Partial<Options>;
46
45
  });
47
- handleNode(id: number, parentID: number, node: Node): Set<number> | undefined;
46
+ computeLevel(node: Node, parentLevel: SanitizeLevel): SanitizeLevel;
47
+ getLevel(id: number): SanitizeLevel;
48
+ setLevel(id: number, level: SanitizeLevel): SanitizeLevel;
49
+ handleNode(id: number, parentID: number, node: Node): void;
48
50
  sanitize(id: number, data: string): string;
49
51
  isObscured(id: number): boolean;
50
52
  isHidden(id: number): boolean;
@@ -47,6 +47,21 @@ export default class API {
47
47
  checkDoNotTrack: () => boolean | undefined;
48
48
  signalStartIssue: (reason: string, missingApi: string[]) => void;
49
49
  restartCanvasTracking: () => void;
50
+ /**
51
+ * Re-evaluates sanitization against the current DOM and re-emits whatever
52
+ * changed, updating already-recorded nodes mid-session. Call after toggling
53
+ * `data-openreplay-*` attributes or after changing whatever your `domSanitizer`
54
+ * keys on (class/id/etc).
55
+ *
56
+ * @param el - the highest node you changed; omit to re-scan the whole document;
57
+ * scanning the entire doc is O(dom size)
58
+ * */
59
+ resanitize: (el?: Element) => void;
60
+ /**
61
+ * Returns the sanitization level the tracker currently has for a node
62
+ * (0 = Plain, 1 = Obscured, 2 = Hidden), or undefined if it isn't tracked.
63
+ * */
64
+ checkSanitization: (el: Node) => import("./app/sanitizer.js").SanitizeLevel | undefined;
50
65
  use<T>(fn: (app: App | null, options?: Partial<Options>) => T): T;
51
66
  isActive(): boolean;
52
67
  /**
@@ -103,9 +118,9 @@ export default class API {
103
118
  }) => string | undefined;
104
119
  setUserID: (id: string) => void;
105
120
  identify: (id: string) => void;
106
- track: ((eventName: string, properties?: Record<string, any>, options?: {
121
+ track: (eventName: string, properties?: Record<string, any>, options?: {
107
122
  send_immediately: boolean;
108
- }) => void) | undefined;
123
+ }) => void | undefined;
109
124
  userID: (id: string) => void;
110
125
  setUserAnonymousID(id: string): void;
111
126
  userAnonymousID(id: string): void;
@@ -16,7 +16,7 @@ declare class TrackerSingleton {
16
16
  * (which can be used to stitch sessions together)
17
17
  * */
18
18
  stop(): string | undefined;
19
- setUserID(id: string): void;
19
+ setUserID: (id: string) => void;
20
20
  get analytics(): import("./entry.js").Analytics | null;
21
21
  identify: (id: string) => void;
22
22
  track: (eventName: string, properties?: Record<string, any>, options?: {
@@ -101,6 +101,30 @@ declare class TrackerSingleton {
101
101
  forceFlushBatch(): void;
102
102
  getSessionInfo(): import("./app/session.js").SessionInfo | null;
103
103
  getTabId(): string | null;
104
+ /**
105
+ * Re-evaluates sanitization against the current DOM and re-emits whatever
106
+ * changed, updating already-recorded nodes mid-session. Call after toggling
107
+ * `data-openreplay-*` attributes or after changing whatever your `domSanitizer`
108
+ * keys on (class/id/etc).
109
+ *
110
+ * @param el - the highest node you changed; omit to re-scan the whole document.
111
+ * */
112
+ resanitize(el?: Element): void;
113
+ /**
114
+ * Returns the sanitization level the tracker currently has for a node
115
+ * (0 = Plain, 1 = Obscured, 2 = Hidden), or undefined if it isn't tracked.
116
+ * */
117
+ checkSanitization(el: Node): import("./index.js").SanitizeLevel | undefined;
118
+ incident(options: {
119
+ label?: string;
120
+ startTime: number;
121
+ endTime?: number;
122
+ }): void;
123
+ /**
124
+ * Use custom token for analytics events without session recording
125
+ * */
126
+ setAnalyticsToken(token: string): void;
127
+ getAnalyticsToken(): string | undefined;
104
128
  }
105
129
  declare const tracker: TrackerSingleton;
106
130
  export default tracker;