@tracelog/lib 2.8.5 → 2.9.0-rc.108.13

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.
@@ -301,6 +301,33 @@ interface CustomEventData {
301
301
  /** Additional event metadata */
302
302
  metadata?: Record<string, MetadataType> | Record<string, MetadataType>[];
303
303
  }
304
+ /**
305
+ * Optional flags for `tracelog.event()`.
306
+ */
307
+ interface EventOptions {
308
+ /**
309
+ * If `true`, the event queue is flushed via `navigator.sendBeacon()`
310
+ * immediately after this event is tracked. The browser guarantees the
311
+ * request is queued for delivery even if the page is about to unload
312
+ * (e.g., `window.location.href = '/thanks'` right after tracking a
313
+ * purchase). Async fetch would be cancelled by the navigation;
314
+ * sendBeacon survives.
315
+ *
316
+ * Use for high-value events where loss is unacceptable (Purchase, Signup,
317
+ * AddPaymentInfo). The critical event plus any previously queued events
318
+ * are sent in a single request — this does not bypass the queue.
319
+ *
320
+ * **Limitations** (inherited from `sendBeacon`):
321
+ * - 64KB payload cap. If the combined queue + critical event exceeds it,
322
+ * the failed batch is persisted to localStorage and recovered on next
323
+ * `init()` via its idempotency token.
324
+ * - No retry on failure (fire-and-forget).
325
+ * - Custom headers are not applied (browser API limitation).
326
+ *
327
+ * @default false
328
+ */
329
+ critical?: boolean;
330
+ }
304
331
  /**
305
332
  * Web performance metrics data
306
333
  */
@@ -318,6 +345,8 @@ interface ErrorData {
318
345
  type: ErrorType;
319
346
  /** Error message text */
320
347
  message: string;
348
+ /** Error constructor name (TypeError, ReferenceError, etc.) when available */
349
+ name?: string;
321
350
  /** Source file where error occurred */
322
351
  filename?: string;
323
352
  /** Line number in source file */
@@ -406,6 +435,22 @@ interface EventData {
406
435
  /** Campaign tracking parameters */
407
436
  utm?: UTM;
408
437
  }
438
+ /**
439
+ * Internal queue entry: an `EventData` enriched with the session ID frozen at
440
+ * `track()` time. Survives session renewal — when the user is idle past the
441
+ * timeout, `state.sessionId` is nulled but events already in the queue keep
442
+ * their original `_session_id`, so `EventManager.buildBatchesWithIds()` can
443
+ * still attribute them correctly instead of emitting `session_id: null` to the
444
+ * wire.
445
+ *
446
+ * **Internal only.** The leading underscore mirrors `EventsQueue._metadata` —
447
+ * this field is stripped from `events[]` at batch construction time, since the
448
+ * backend's `EventDto` uses `forbidNonWhitelisted: true` and the wrapper
449
+ * `EventsQueue.session_id` is the contract.
450
+ */
451
+ interface QueuedEvent extends EventData {
452
+ _session_id: string;
453
+ }
409
454
 
410
455
  /**
411
456
  * Web Vitals filtering mode
@@ -449,6 +494,20 @@ interface Config {
449
494
  webVitalsThresholds?: Partial<Record<WebVitalType, number>>;
450
495
  /** Interval in milliseconds between event batch sends. @default 10000 (10 seconds) */
451
496
  sendIntervalMs?: number;
497
+ /**
498
+ * If true, the event queue is flushed automatically after every SPA navigation
499
+ * (`pushState`, `replaceState`, `popstate`, `hashchange`). Prevents batch loss when the
500
+ * user closes the tab mid-buffer between SPA route changes. No-op for MPAs (no SPA nav).
501
+ * @default true
502
+ */
503
+ flushOnSpaNavigation?: boolean;
504
+ /**
505
+ * If true, the event queue is flushed when `document.hidden` becomes `true`
506
+ * (tab switch, lock screen, app backgrounding). Especially relevant on mobile Safari
507
+ * where `pagehide`/`beforeunload` may not fire reliably.
508
+ * @default true
509
+ */
510
+ flushOnPageHidden?: boolean;
452
511
  /** Optional configuration for third-party integrations. */
453
512
  integrations?: {
454
513
  /** TraceLog integration options. */
@@ -1320,6 +1379,7 @@ declare class EventManager extends StateManager {
1320
1379
  private rateLimitCounter;
1321
1380
  private rateLimitWindowStart;
1322
1381
  private lastSessionId;
1382
+ private pendingSyncFlush;
1323
1383
  private sessionEventCounts;
1324
1384
  private readonly saveSessionCountsDebounced;
1325
1385
  /**
@@ -1527,6 +1587,44 @@ declare class EventManager extends StateManager {
1527
1587
  * @see src/managers/README.md (lines 5-75) for flush details
1528
1588
  */
1529
1589
  flushImmediatelySync(): boolean;
1590
+ /**
1591
+ * Sends ONLY the most recently queued event via `navigator.sendBeacon()` in
1592
+ * a dedicated single-event batch.
1593
+ *
1594
+ * **Purpose**: Guarantee delivery of an event that *must* survive an
1595
+ * imminent page unload (purchase confirmation, signup completion, etc.),
1596
+ * independent of the main queue's size or in-flight async send state.
1597
+ *
1598
+ * **Why a dedicated batch**: `flushImmediatelySync()` serialises the entire
1599
+ * queue into one `sendBeacon` call. If the queue is heavy (>64KB) the
1600
+ * beacon fails and the batch is persisted to `localStorage`, which is only
1601
+ * recovered on the next `init()` — useless for one-shot conversion events
1602
+ * where the user never returns.
1603
+ *
1604
+ * **Behaviour**:
1605
+ * - Reads the last entry of `eventsQueue` (the just-tracked event).
1606
+ * - Wraps it in its own `EventsQueue` and dispatches synchronously through
1607
+ * every `SenderManager` via `sendEventsQueueSync()`.
1608
+ * - Leaves the main queue untouched; the same event will be re-delivered by
1609
+ * the next periodic / unload flush. Idempotency is the caller-side
1610
+ * contract: the backend MUST deduplicate by `event.id` (e.g. unique index
1611
+ * on the ingestion collection) — same guarantee the library already
1612
+ * relies on for the persisted-events recovery path.
1613
+ *
1614
+ * **Use it after** `track()` so the event is in the queue. Caller is
1615
+ * responsible for verifying that `track()` actually queued the event
1616
+ * (it can be dropped by rate limiting / sampling / `beforeSend`).
1617
+ *
1618
+ * **Standalone mode** (no senders configured): returns `false` without
1619
+ * emitting. The subsequent main-queue drain (`flushImmediatelySync()`) is
1620
+ * responsible for delivering the event to local listeners — emitting here
1621
+ * too would surface the same event twice to a `tracelog.on('queue', ...)`
1622
+ * subscriber.
1623
+ *
1624
+ * @returns `true` if at least one sender delivered the beacon successfully,
1625
+ * `false` otherwise (including standalone mode — the drain handles it).
1626
+ */
1627
+ flushLastEventSync(): boolean;
1530
1628
  /**
1531
1629
  * Sets the custom headers provider callback for the custom integration.
1532
1630
  * Only affects requests to custom backend (not TraceLog SaaS).
@@ -1628,9 +1726,67 @@ declare class EventManager extends StateManager {
1628
1726
  flushPendingEvents(): void;
1629
1727
  private clearSendTimeout;
1630
1728
  private isSuccessfulResult;
1729
+ /**
1730
+ * Groups the queue by frozen `_session_id`, preserving insertion order.
1731
+ * Single pass — `buildBatchesWithIds()` builds one batch + one eventIds list
1732
+ * per group, so the grouping cost is O(N) per flush regardless of session
1733
+ * count.
1734
+ *
1735
+ * **Self-heal**: any entry missing `_session_id` (an internal invariant
1736
+ * violation — `buildEventPayload` always stamps it) is removed from the
1737
+ * queue rather than left behind, otherwise a single corrupted entry would
1738
+ * keep `eventsQueue.length > 0` forever and re-trigger periodic sends.
1739
+ */
1740
+ private groupQueuedEventsBySession;
1741
+ /**
1742
+ * Builds a parallel list of `(batch, eventIds)` for sending. The eventIds are
1743
+ * the original `_session_id`-tagged event IDs in the queue that map to this
1744
+ * batch — used for optimistic removal. We can't read them off the wrapper's
1745
+ * `events[]` because dedup may have removed some signatures.
1746
+ */
1747
+ private buildBatchesWithIds;
1631
1748
  private flushEvents;
1749
+ /**
1750
+ * Re-runs a sync flush that was deferred while an async send was in flight.
1751
+ *
1752
+ * Called from the `finally` blocks of `flushEvents(false)` and
1753
+ * `sendEventsQueue()`. If `pendingSyncFlush` is set, clears the flag and
1754
+ * invokes `flushImmediatelySync()` synchronously so any events that arrived
1755
+ * after the deferred sync call are delivered before the next event loop
1756
+ * tick. Critical for high-stakes events tracked mid-async-send.
1757
+ */
1758
+ private drainPendingSyncFlush;
1759
+ /**
1760
+ * Sends one batch synchronously across all integrations (sendBeacon path).
1761
+ * Optimistic removal: if any integration succeeds, we remove the batch's
1762
+ * events from the queue and emit it locally. Failures persist per-integration.
1763
+ */
1764
+ private sendBatchSync;
1765
+ /**
1766
+ * Sends one batch asynchronously across all integrations (fetch path).
1767
+ */
1768
+ private sendBatchAsync;
1632
1769
  private sendEventsQueue;
1633
- private buildEventsPayload;
1770
+ /**
1771
+ * Builds a single batch from a per-session group: dedup by signature,
1772
+ * SESSION_START first, then timestamp order, strip `_session_id`, apply
1773
+ * `beforeBatch` transformer when running standalone.
1774
+ *
1775
+ * **Why N batches per flush**: events freeze their `_session_id` at `track()`
1776
+ * time. If the session was renewed (idle timeout) between two `track()`
1777
+ * calls, the queue contains events from multiple sessions. `buildBatchesWithIds()`
1778
+ * emits one batch per session so the backend's `EventsQueueDto.session_id`
1779
+ * remains the single source of truth and stays consistent with the events it
1780
+ * carries.
1781
+ *
1782
+ * **Strip**: `_session_id` is removed from each event in the wrapper's
1783
+ * `events[]` because the backend uses `forbidNonWhitelisted: true` and would
1784
+ * reject the batch if the field leaked through.
1785
+ *
1786
+ * **Transformer note**: `beforeBatch` is invoked **once per session-batch**,
1787
+ * not once per flush. A queue spanning N sessions triggers N invocations.
1788
+ */
1789
+ private buildBatchFromGroup;
1634
1790
  private buildEventPayload;
1635
1791
  private isDuplicateEvent;
1636
1792
  private pruneOldFingerprints;
@@ -2333,6 +2489,11 @@ type BeforeSendTransformer = (event: EventData) => EventData | null;
2333
2489
  * Transform entire batch before sending to backend.
2334
2490
  * Applied per-batch (10s interval or 50 events), can filter by returning null.
2335
2491
  *
2492
+ * **Multi-batch flushes**: when the queue spans more than one session (e.g.
2493
+ * after an idle-timeout renewal), one flush produces one batch per session,
2494
+ * and this transformer is invoked **once per session-batch**. Avoid stateful
2495
+ * transformers that assume a single invocation per flush.
2496
+ *
2336
2497
  * @param batch - The batch to transform
2337
2498
  * @returns Transformed batch or null to filter
2338
2499
  */
@@ -2384,8 +2545,8 @@ interface TraceLogTestBridge {
2384
2545
  readonly initialized: boolean;
2385
2546
  init(config?: Config): Promise<InitResult>;
2386
2547
  destroy(force?: boolean): void;
2387
- sendCustomEvent(name: string, data?: Record<string, unknown> | Record<string, unknown>[]): void;
2388
- event(name: string, metadata?: Record<string, unknown> | Record<string, unknown>[]): void;
2548
+ sendCustomEvent(name: string, data?: Record<string, unknown> | Record<string, unknown>[], options?: EventOptions): void;
2549
+ event(name: string, metadata?: Record<string, unknown> | Record<string, unknown>[], options?: EventOptions): void;
2389
2550
  on(event: string, callback: (data: any) => void): void;
2390
2551
  off(event: string, callback: (data: any) => void): void;
2391
2552
  get<T extends keyof State>(key: T): State[T];
@@ -2572,7 +2733,7 @@ declare const getWebVitalsThresholds: (mode?: WebVitalsMode) => Record<WebVitalT
2572
2733
 
2573
2734
  declare const tracelog: {
2574
2735
  init: (config?: Config) => Promise<InitResult>;
2575
- event: (name: string, metadata?: Record<string, MetadataType> | Record<string, MetadataType>[]) => void;
2736
+ event: (name: string, metadata?: Record<string, MetadataType> | Record<string, MetadataType>[], options?: EventOptions) => void;
2576
2737
  on: <K extends keyof EmitterMap>(event: K, callback: EmitterCallback<EmitterMap[K]>) => void;
2577
2738
  off: <K extends keyof EmitterMap>(event: K, callback: EmitterCallback<EmitterMap[K]>) => void;
2578
2739
  setTransformer: typeof setTransformer;
@@ -2587,6 +2748,8 @@ declare const tracelog: {
2587
2748
  mergeGlobalMetadata: (metadata: Record<string, MetadataType>) => void;
2588
2749
  identify: (userId: string, traits?: Record<string, string>) => void;
2589
2750
  resetIdentity: () => Promise<void>;
2751
+ flushImmediately: () => Promise<boolean>;
2752
+ flushImmediatelySync: () => boolean;
2590
2753
  };
2591
2754
 
2592
- export { AppConfigValidationError, type BeforeBatchTransformer, type BeforeSendTransformer, type ClickCoordinates, type ClickData, type ClickTrackingElementData, type Config, type CustomEventData, type CustomHeadersProvider, DEFAULT_SESSION_TIMEOUT, DEFAULT_WEB_VITALS_MODE, type DeviceInfo, DeviceType, type EmitterCallback, EmitterEvent, type EmitterMap, type ErrorData, ErrorType, type EventData, EventType, type EventTypeName, type EventsQueue, type IdentifyData, type InitResult, InitializationTimeoutError, IntegrationValidationError, MAX_ARRAY_LENGTH, MAX_CUSTOM_EVENT_ARRAY_SIZE, MAX_CUSTOM_EVENT_KEYS, MAX_CUSTOM_EVENT_NAME_LENGTH, MAX_CUSTOM_EVENT_STRING_SIZE, MAX_NESTED_OBJECT_KEYS, MAX_STRING_LENGTH, MAX_STRING_LENGTH_IN_ARRAY, type MetadataType, Mode, PII_PATTERNS, type PageViewData, PermanentError, type PersistedEventsQueue, type PrimaryScrollEvent, type QueueMetadata, RateLimitError, SamplingRateValidationError, type ScrollData, ScrollDirection, type SecondaryScrollEvent, type SessionEventCounts, SessionTimeoutValidationError, SpecialApiUrl, type State, TimeoutError, type TraceLogTestBridge, TraceLogValidationError, type TransformerHook, type TransformerMap, type UTM, type ViewportConfig, type ViewportElement, type ViewportEventData, WEB_VITALS_GOOD_THRESHOLDS, WEB_VITALS_NEEDS_IMPROVEMENT_THRESHOLDS, WEB_VITALS_POOR_THRESHOLDS, type WebVitalType, type WebVitalsData, type WebVitalsMode, getWebVitalsThresholds, isPrimaryScrollEvent, isSecondaryScrollEvent, tracelog };
2755
+ export { AppConfigValidationError, type BeforeBatchTransformer, type BeforeSendTransformer, type ClickCoordinates, type ClickData, type ClickTrackingElementData, type Config, type CustomEventData, type CustomHeadersProvider, DEFAULT_SESSION_TIMEOUT, DEFAULT_WEB_VITALS_MODE, type DeviceInfo, DeviceType, type EmitterCallback, EmitterEvent, type EmitterMap, type ErrorData, ErrorType, type EventData, type EventOptions, EventType, type EventTypeName, type EventsQueue, type IdentifyData, type InitResult, InitializationTimeoutError, IntegrationValidationError, MAX_ARRAY_LENGTH, MAX_CUSTOM_EVENT_ARRAY_SIZE, MAX_CUSTOM_EVENT_KEYS, MAX_CUSTOM_EVENT_NAME_LENGTH, MAX_CUSTOM_EVENT_STRING_SIZE, MAX_NESTED_OBJECT_KEYS, MAX_STRING_LENGTH, MAX_STRING_LENGTH_IN_ARRAY, type MetadataType, Mode, PII_PATTERNS, type PageViewData, PermanentError, type PersistedEventsQueue, type PrimaryScrollEvent, type QueueMetadata, type QueuedEvent, RateLimitError, SamplingRateValidationError, type ScrollData, ScrollDirection, type SecondaryScrollEvent, type SessionEventCounts, SessionTimeoutValidationError, SpecialApiUrl, type State, TimeoutError, type TraceLogTestBridge, TraceLogValidationError, type TransformerHook, type TransformerMap, type UTM, type ViewportConfig, type ViewportElement, type ViewportEventData, WEB_VITALS_GOOD_THRESHOLDS, WEB_VITALS_NEEDS_IMPROVEMENT_THRESHOLDS, WEB_VITALS_POOR_THRESHOLDS, type WebVitalType, type WebVitalsData, type WebVitalsMode, getWebVitalsThresholds, isPrimaryScrollEvent, isSecondaryScrollEvent, tracelog };
@@ -301,6 +301,33 @@ interface CustomEventData {
301
301
  /** Additional event metadata */
302
302
  metadata?: Record<string, MetadataType> | Record<string, MetadataType>[];
303
303
  }
304
+ /**
305
+ * Optional flags for `tracelog.event()`.
306
+ */
307
+ interface EventOptions {
308
+ /**
309
+ * If `true`, the event queue is flushed via `navigator.sendBeacon()`
310
+ * immediately after this event is tracked. The browser guarantees the
311
+ * request is queued for delivery even if the page is about to unload
312
+ * (e.g., `window.location.href = '/thanks'` right after tracking a
313
+ * purchase). Async fetch would be cancelled by the navigation;
314
+ * sendBeacon survives.
315
+ *
316
+ * Use for high-value events where loss is unacceptable (Purchase, Signup,
317
+ * AddPaymentInfo). The critical event plus any previously queued events
318
+ * are sent in a single request — this does not bypass the queue.
319
+ *
320
+ * **Limitations** (inherited from `sendBeacon`):
321
+ * - 64KB payload cap. If the combined queue + critical event exceeds it,
322
+ * the failed batch is persisted to localStorage and recovered on next
323
+ * `init()` via its idempotency token.
324
+ * - No retry on failure (fire-and-forget).
325
+ * - Custom headers are not applied (browser API limitation).
326
+ *
327
+ * @default false
328
+ */
329
+ critical?: boolean;
330
+ }
304
331
  /**
305
332
  * Web performance metrics data
306
333
  */
@@ -318,6 +345,8 @@ interface ErrorData {
318
345
  type: ErrorType;
319
346
  /** Error message text */
320
347
  message: string;
348
+ /** Error constructor name (TypeError, ReferenceError, etc.) when available */
349
+ name?: string;
321
350
  /** Source file where error occurred */
322
351
  filename?: string;
323
352
  /** Line number in source file */
@@ -406,6 +435,22 @@ interface EventData {
406
435
  /** Campaign tracking parameters */
407
436
  utm?: UTM;
408
437
  }
438
+ /**
439
+ * Internal queue entry: an `EventData` enriched with the session ID frozen at
440
+ * `track()` time. Survives session renewal — when the user is idle past the
441
+ * timeout, `state.sessionId` is nulled but events already in the queue keep
442
+ * their original `_session_id`, so `EventManager.buildBatchesWithIds()` can
443
+ * still attribute them correctly instead of emitting `session_id: null` to the
444
+ * wire.
445
+ *
446
+ * **Internal only.** The leading underscore mirrors `EventsQueue._metadata` —
447
+ * this field is stripped from `events[]` at batch construction time, since the
448
+ * backend's `EventDto` uses `forbidNonWhitelisted: true` and the wrapper
449
+ * `EventsQueue.session_id` is the contract.
450
+ */
451
+ interface QueuedEvent extends EventData {
452
+ _session_id: string;
453
+ }
409
454
 
410
455
  /**
411
456
  * Web Vitals filtering mode
@@ -449,6 +494,20 @@ interface Config {
449
494
  webVitalsThresholds?: Partial<Record<WebVitalType, number>>;
450
495
  /** Interval in milliseconds between event batch sends. @default 10000 (10 seconds) */
451
496
  sendIntervalMs?: number;
497
+ /**
498
+ * If true, the event queue is flushed automatically after every SPA navigation
499
+ * (`pushState`, `replaceState`, `popstate`, `hashchange`). Prevents batch loss when the
500
+ * user closes the tab mid-buffer between SPA route changes. No-op for MPAs (no SPA nav).
501
+ * @default true
502
+ */
503
+ flushOnSpaNavigation?: boolean;
504
+ /**
505
+ * If true, the event queue is flushed when `document.hidden` becomes `true`
506
+ * (tab switch, lock screen, app backgrounding). Especially relevant on mobile Safari
507
+ * where `pagehide`/`beforeunload` may not fire reliably.
508
+ * @default true
509
+ */
510
+ flushOnPageHidden?: boolean;
452
511
  /** Optional configuration for third-party integrations. */
453
512
  integrations?: {
454
513
  /** TraceLog integration options. */
@@ -1320,6 +1379,7 @@ declare class EventManager extends StateManager {
1320
1379
  private rateLimitCounter;
1321
1380
  private rateLimitWindowStart;
1322
1381
  private lastSessionId;
1382
+ private pendingSyncFlush;
1323
1383
  private sessionEventCounts;
1324
1384
  private readonly saveSessionCountsDebounced;
1325
1385
  /**
@@ -1527,6 +1587,44 @@ declare class EventManager extends StateManager {
1527
1587
  * @see src/managers/README.md (lines 5-75) for flush details
1528
1588
  */
1529
1589
  flushImmediatelySync(): boolean;
1590
+ /**
1591
+ * Sends ONLY the most recently queued event via `navigator.sendBeacon()` in
1592
+ * a dedicated single-event batch.
1593
+ *
1594
+ * **Purpose**: Guarantee delivery of an event that *must* survive an
1595
+ * imminent page unload (purchase confirmation, signup completion, etc.),
1596
+ * independent of the main queue's size or in-flight async send state.
1597
+ *
1598
+ * **Why a dedicated batch**: `flushImmediatelySync()` serialises the entire
1599
+ * queue into one `sendBeacon` call. If the queue is heavy (>64KB) the
1600
+ * beacon fails and the batch is persisted to `localStorage`, which is only
1601
+ * recovered on the next `init()` — useless for one-shot conversion events
1602
+ * where the user never returns.
1603
+ *
1604
+ * **Behaviour**:
1605
+ * - Reads the last entry of `eventsQueue` (the just-tracked event).
1606
+ * - Wraps it in its own `EventsQueue` and dispatches synchronously through
1607
+ * every `SenderManager` via `sendEventsQueueSync()`.
1608
+ * - Leaves the main queue untouched; the same event will be re-delivered by
1609
+ * the next periodic / unload flush. Idempotency is the caller-side
1610
+ * contract: the backend MUST deduplicate by `event.id` (e.g. unique index
1611
+ * on the ingestion collection) — same guarantee the library already
1612
+ * relies on for the persisted-events recovery path.
1613
+ *
1614
+ * **Use it after** `track()` so the event is in the queue. Caller is
1615
+ * responsible for verifying that `track()` actually queued the event
1616
+ * (it can be dropped by rate limiting / sampling / `beforeSend`).
1617
+ *
1618
+ * **Standalone mode** (no senders configured): returns `false` without
1619
+ * emitting. The subsequent main-queue drain (`flushImmediatelySync()`) is
1620
+ * responsible for delivering the event to local listeners — emitting here
1621
+ * too would surface the same event twice to a `tracelog.on('queue', ...)`
1622
+ * subscriber.
1623
+ *
1624
+ * @returns `true` if at least one sender delivered the beacon successfully,
1625
+ * `false` otherwise (including standalone mode — the drain handles it).
1626
+ */
1627
+ flushLastEventSync(): boolean;
1530
1628
  /**
1531
1629
  * Sets the custom headers provider callback for the custom integration.
1532
1630
  * Only affects requests to custom backend (not TraceLog SaaS).
@@ -1628,9 +1726,67 @@ declare class EventManager extends StateManager {
1628
1726
  flushPendingEvents(): void;
1629
1727
  private clearSendTimeout;
1630
1728
  private isSuccessfulResult;
1729
+ /**
1730
+ * Groups the queue by frozen `_session_id`, preserving insertion order.
1731
+ * Single pass — `buildBatchesWithIds()` builds one batch + one eventIds list
1732
+ * per group, so the grouping cost is O(N) per flush regardless of session
1733
+ * count.
1734
+ *
1735
+ * **Self-heal**: any entry missing `_session_id` (an internal invariant
1736
+ * violation — `buildEventPayload` always stamps it) is removed from the
1737
+ * queue rather than left behind, otherwise a single corrupted entry would
1738
+ * keep `eventsQueue.length > 0` forever and re-trigger periodic sends.
1739
+ */
1740
+ private groupQueuedEventsBySession;
1741
+ /**
1742
+ * Builds a parallel list of `(batch, eventIds)` for sending. The eventIds are
1743
+ * the original `_session_id`-tagged event IDs in the queue that map to this
1744
+ * batch — used for optimistic removal. We can't read them off the wrapper's
1745
+ * `events[]` because dedup may have removed some signatures.
1746
+ */
1747
+ private buildBatchesWithIds;
1631
1748
  private flushEvents;
1749
+ /**
1750
+ * Re-runs a sync flush that was deferred while an async send was in flight.
1751
+ *
1752
+ * Called from the `finally` blocks of `flushEvents(false)` and
1753
+ * `sendEventsQueue()`. If `pendingSyncFlush` is set, clears the flag and
1754
+ * invokes `flushImmediatelySync()` synchronously so any events that arrived
1755
+ * after the deferred sync call are delivered before the next event loop
1756
+ * tick. Critical for high-stakes events tracked mid-async-send.
1757
+ */
1758
+ private drainPendingSyncFlush;
1759
+ /**
1760
+ * Sends one batch synchronously across all integrations (sendBeacon path).
1761
+ * Optimistic removal: if any integration succeeds, we remove the batch's
1762
+ * events from the queue and emit it locally. Failures persist per-integration.
1763
+ */
1764
+ private sendBatchSync;
1765
+ /**
1766
+ * Sends one batch asynchronously across all integrations (fetch path).
1767
+ */
1768
+ private sendBatchAsync;
1632
1769
  private sendEventsQueue;
1633
- private buildEventsPayload;
1770
+ /**
1771
+ * Builds a single batch from a per-session group: dedup by signature,
1772
+ * SESSION_START first, then timestamp order, strip `_session_id`, apply
1773
+ * `beforeBatch` transformer when running standalone.
1774
+ *
1775
+ * **Why N batches per flush**: events freeze their `_session_id` at `track()`
1776
+ * time. If the session was renewed (idle timeout) between two `track()`
1777
+ * calls, the queue contains events from multiple sessions. `buildBatchesWithIds()`
1778
+ * emits one batch per session so the backend's `EventsQueueDto.session_id`
1779
+ * remains the single source of truth and stays consistent with the events it
1780
+ * carries.
1781
+ *
1782
+ * **Strip**: `_session_id` is removed from each event in the wrapper's
1783
+ * `events[]` because the backend uses `forbidNonWhitelisted: true` and would
1784
+ * reject the batch if the field leaked through.
1785
+ *
1786
+ * **Transformer note**: `beforeBatch` is invoked **once per session-batch**,
1787
+ * not once per flush. A queue spanning N sessions triggers N invocations.
1788
+ */
1789
+ private buildBatchFromGroup;
1634
1790
  private buildEventPayload;
1635
1791
  private isDuplicateEvent;
1636
1792
  private pruneOldFingerprints;
@@ -2333,6 +2489,11 @@ type BeforeSendTransformer = (event: EventData) => EventData | null;
2333
2489
  * Transform entire batch before sending to backend.
2334
2490
  * Applied per-batch (10s interval or 50 events), can filter by returning null.
2335
2491
  *
2492
+ * **Multi-batch flushes**: when the queue spans more than one session (e.g.
2493
+ * after an idle-timeout renewal), one flush produces one batch per session,
2494
+ * and this transformer is invoked **once per session-batch**. Avoid stateful
2495
+ * transformers that assume a single invocation per flush.
2496
+ *
2336
2497
  * @param batch - The batch to transform
2337
2498
  * @returns Transformed batch or null to filter
2338
2499
  */
@@ -2384,8 +2545,8 @@ interface TraceLogTestBridge {
2384
2545
  readonly initialized: boolean;
2385
2546
  init(config?: Config): Promise<InitResult>;
2386
2547
  destroy(force?: boolean): void;
2387
- sendCustomEvent(name: string, data?: Record<string, unknown> | Record<string, unknown>[]): void;
2388
- event(name: string, metadata?: Record<string, unknown> | Record<string, unknown>[]): void;
2548
+ sendCustomEvent(name: string, data?: Record<string, unknown> | Record<string, unknown>[], options?: EventOptions): void;
2549
+ event(name: string, metadata?: Record<string, unknown> | Record<string, unknown>[], options?: EventOptions): void;
2389
2550
  on(event: string, callback: (data: any) => void): void;
2390
2551
  off(event: string, callback: (data: any) => void): void;
2391
2552
  get<T extends keyof State>(key: T): State[T];
@@ -2572,7 +2733,7 @@ declare const getWebVitalsThresholds: (mode?: WebVitalsMode) => Record<WebVitalT
2572
2733
 
2573
2734
  declare const tracelog: {
2574
2735
  init: (config?: Config) => Promise<InitResult>;
2575
- event: (name: string, metadata?: Record<string, MetadataType> | Record<string, MetadataType>[]) => void;
2736
+ event: (name: string, metadata?: Record<string, MetadataType> | Record<string, MetadataType>[], options?: EventOptions) => void;
2576
2737
  on: <K extends keyof EmitterMap>(event: K, callback: EmitterCallback<EmitterMap[K]>) => void;
2577
2738
  off: <K extends keyof EmitterMap>(event: K, callback: EmitterCallback<EmitterMap[K]>) => void;
2578
2739
  setTransformer: typeof setTransformer;
@@ -2587,6 +2748,8 @@ declare const tracelog: {
2587
2748
  mergeGlobalMetadata: (metadata: Record<string, MetadataType>) => void;
2588
2749
  identify: (userId: string, traits?: Record<string, string>) => void;
2589
2750
  resetIdentity: () => Promise<void>;
2751
+ flushImmediately: () => Promise<boolean>;
2752
+ flushImmediatelySync: () => boolean;
2590
2753
  };
2591
2754
 
2592
- export { AppConfigValidationError, type BeforeBatchTransformer, type BeforeSendTransformer, type ClickCoordinates, type ClickData, type ClickTrackingElementData, type Config, type CustomEventData, type CustomHeadersProvider, DEFAULT_SESSION_TIMEOUT, DEFAULT_WEB_VITALS_MODE, type DeviceInfo, DeviceType, type EmitterCallback, EmitterEvent, type EmitterMap, type ErrorData, ErrorType, type EventData, EventType, type EventTypeName, type EventsQueue, type IdentifyData, type InitResult, InitializationTimeoutError, IntegrationValidationError, MAX_ARRAY_LENGTH, MAX_CUSTOM_EVENT_ARRAY_SIZE, MAX_CUSTOM_EVENT_KEYS, MAX_CUSTOM_EVENT_NAME_LENGTH, MAX_CUSTOM_EVENT_STRING_SIZE, MAX_NESTED_OBJECT_KEYS, MAX_STRING_LENGTH, MAX_STRING_LENGTH_IN_ARRAY, type MetadataType, Mode, PII_PATTERNS, type PageViewData, PermanentError, type PersistedEventsQueue, type PrimaryScrollEvent, type QueueMetadata, RateLimitError, SamplingRateValidationError, type ScrollData, ScrollDirection, type SecondaryScrollEvent, type SessionEventCounts, SessionTimeoutValidationError, SpecialApiUrl, type State, TimeoutError, type TraceLogTestBridge, TraceLogValidationError, type TransformerHook, type TransformerMap, type UTM, type ViewportConfig, type ViewportElement, type ViewportEventData, WEB_VITALS_GOOD_THRESHOLDS, WEB_VITALS_NEEDS_IMPROVEMENT_THRESHOLDS, WEB_VITALS_POOR_THRESHOLDS, type WebVitalType, type WebVitalsData, type WebVitalsMode, getWebVitalsThresholds, isPrimaryScrollEvent, isSecondaryScrollEvent, tracelog };
2755
+ export { AppConfigValidationError, type BeforeBatchTransformer, type BeforeSendTransformer, type ClickCoordinates, type ClickData, type ClickTrackingElementData, type Config, type CustomEventData, type CustomHeadersProvider, DEFAULT_SESSION_TIMEOUT, DEFAULT_WEB_VITALS_MODE, type DeviceInfo, DeviceType, type EmitterCallback, EmitterEvent, type EmitterMap, type ErrorData, ErrorType, type EventData, type EventOptions, EventType, type EventTypeName, type EventsQueue, type IdentifyData, type InitResult, InitializationTimeoutError, IntegrationValidationError, MAX_ARRAY_LENGTH, MAX_CUSTOM_EVENT_ARRAY_SIZE, MAX_CUSTOM_EVENT_KEYS, MAX_CUSTOM_EVENT_NAME_LENGTH, MAX_CUSTOM_EVENT_STRING_SIZE, MAX_NESTED_OBJECT_KEYS, MAX_STRING_LENGTH, MAX_STRING_LENGTH_IN_ARRAY, type MetadataType, Mode, PII_PATTERNS, type PageViewData, PermanentError, type PersistedEventsQueue, type PrimaryScrollEvent, type QueueMetadata, type QueuedEvent, RateLimitError, SamplingRateValidationError, type ScrollData, ScrollDirection, type SecondaryScrollEvent, type SessionEventCounts, SessionTimeoutValidationError, SpecialApiUrl, type State, TimeoutError, type TraceLogTestBridge, TraceLogValidationError, type TransformerHook, type TransformerMap, type UTM, type ViewportConfig, type ViewportElement, type ViewportEventData, WEB_VITALS_GOOD_THRESHOLDS, WEB_VITALS_NEEDS_IMPROVEMENT_THRESHOLDS, WEB_VITALS_POOR_THRESHOLDS, type WebVitalType, type WebVitalsData, type WebVitalsMode, getWebVitalsThresholds, isPrimaryScrollEvent, isSecondaryScrollEvent, tracelog };