@hexclave/next 1.0.20 → 1.0.22

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.
Files changed (92) hide show
  1. package/README.md +187 -7
  2. package/dist/components/elements/sidebar-layout.js +1 -1
  3. package/dist/components-page/oauth-callback.js +14 -19
  4. package/dist/components-page/oauth-callback.js.map +1 -1
  5. package/dist/components-page/oauth-callback.test.d.ts +1 -0
  6. package/dist/components-page/oauth-callback.test.js +90 -0
  7. package/dist/components-page/oauth-callback.test.js.map +1 -0
  8. package/dist/esm/components/elements/sidebar-layout.js +1 -1
  9. package/dist/esm/components-page/oauth-callback.js +14 -19
  10. package/dist/esm/components-page/oauth-callback.js.map +1 -1
  11. package/dist/esm/components-page/oauth-callback.test.d.ts +1 -0
  12. package/dist/esm/components-page/oauth-callback.test.js +89 -0
  13. package/dist/esm/components-page/oauth-callback.test.js.map +1 -0
  14. package/dist/esm/generated/quetzal-translations.d.ts +2 -2
  15. package/dist/esm/lib/auth.d.ts.map +1 -1
  16. package/dist/esm/lib/auth.js +32 -11
  17. package/dist/esm/lib/auth.js.map +1 -1
  18. package/dist/esm/lib/auth.test.js +25 -10
  19. package/dist/esm/lib/auth.test.js.map +1 -1
  20. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +1 -0
  21. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
  22. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.js +1 -0
  23. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.js.map +1 -1
  24. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +54 -0
  25. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js.map +1 -1
  26. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +4 -1
  27. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
  28. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js +28 -4
  29. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
  30. package/dist/esm/lib/hexclave-app/apps/implementations/common.js +1 -1
  31. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.d.ts +1 -0
  32. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.d.ts.map +1 -1
  33. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.js +17 -13
  34. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.js.map +1 -1
  35. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.test.js +4 -8
  36. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.test.js.map +1 -1
  37. package/dist/esm/lib/hexclave-app/apps/implementations/server-app-impl.d.ts +1 -1
  38. package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.d.ts +3 -1
  39. package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.d.ts.map +1 -1
  40. package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.js +19 -13
  41. package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.js.map +1 -1
  42. package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.test.js +4 -9
  43. package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.test.js.map +1 -1
  44. package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.d.ts +1 -0
  45. package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.d.ts.map +1 -1
  46. package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.js.map +1 -1
  47. package/dist/generated/quetzal-translations.d.ts +2 -2
  48. package/dist/lib/auth.d.ts.map +1 -1
  49. package/dist/lib/auth.js +31 -10
  50. package/dist/lib/auth.js.map +1 -1
  51. package/dist/lib/auth.test.js +23 -8
  52. package/dist/lib/auth.test.js.map +1 -1
  53. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +1 -0
  54. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
  55. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.js +1 -0
  56. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.js.map +1 -1
  57. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +54 -0
  58. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js.map +1 -1
  59. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +4 -1
  60. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
  61. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js +27 -3
  62. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
  63. package/dist/lib/hexclave-app/apps/implementations/common.js +1 -1
  64. package/dist/lib/hexclave-app/apps/implementations/event-tracker.d.ts +1 -0
  65. package/dist/lib/hexclave-app/apps/implementations/event-tracker.d.ts.map +1 -1
  66. package/dist/lib/hexclave-app/apps/implementations/event-tracker.js +16 -12
  67. package/dist/lib/hexclave-app/apps/implementations/event-tracker.js.map +1 -1
  68. package/dist/lib/hexclave-app/apps/implementations/event-tracker.test.js +4 -8
  69. package/dist/lib/hexclave-app/apps/implementations/event-tracker.test.js.map +1 -1
  70. package/dist/lib/hexclave-app/apps/implementations/server-app-impl.d.ts +1 -1
  71. package/dist/lib/hexclave-app/apps/implementations/session-replay.d.ts +3 -1
  72. package/dist/lib/hexclave-app/apps/implementations/session-replay.d.ts.map +1 -1
  73. package/dist/lib/hexclave-app/apps/implementations/session-replay.js +19 -12
  74. package/dist/lib/hexclave-app/apps/implementations/session-replay.js.map +1 -1
  75. package/dist/lib/hexclave-app/apps/implementations/session-replay.test.js +4 -9
  76. package/dist/lib/hexclave-app/apps/implementations/session-replay.test.js.map +1 -1
  77. package/dist/lib/hexclave-app/apps/interfaces/admin-app.d.ts +1 -0
  78. package/dist/lib/hexclave-app/apps/interfaces/admin-app.d.ts.map +1 -1
  79. package/dist/lib/hexclave-app/apps/interfaces/admin-app.js.map +1 -1
  80. package/package.json +4 -4
  81. package/src/components-page/oauth-callback.test.tsx +109 -0
  82. package/src/components-page/oauth-callback.tsx +14 -19
  83. package/src/lib/auth.test.ts +32 -10
  84. package/src/lib/auth.ts +41 -7
  85. package/src/lib/hexclave-app/apps/implementations/admin-app-impl.ts +2 -1
  86. package/src/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.ts +66 -0
  87. package/src/lib/hexclave-app/apps/implementations/client-app-impl.ts +39 -3
  88. package/src/lib/hexclave-app/apps/implementations/event-tracker.test.ts +5 -13
  89. package/src/lib/hexclave-app/apps/implementations/event-tracker.ts +19 -14
  90. package/src/lib/hexclave-app/apps/implementations/session-replay.test.ts +4 -20
  91. package/src/lib/hexclave-app/apps/implementations/session-replay.ts +19 -12
  92. package/src/lib/hexclave-app/apps/interfaces/admin-app.ts +1 -0
@@ -17,7 +17,7 @@ let ____________generated_env_js = require("../../../../generated/env.js");
17
17
  let ______url_targets_js = require("../../url-targets.js");
18
18
 
19
19
  //#region src/lib/hexclave-app/apps/implementations/common.ts
20
- const clientVersion = "js @hexclave/next@1.0.20";
20
+ const clientVersion = "js @hexclave/next@1.0.22";
21
21
  if (clientVersion.startsWith("STACK_COMPILE_TIME")) throw new _hexclave_shared_dist_utils_errors.HexclaveAssertionError("Client version was not replaced. Something went wrong during build!");
22
22
  const replaceHexclavePortPrefix = (input) => {
23
23
  if (!input) return input;
@@ -49,6 +49,7 @@ declare class EventTracker {
49
49
  private _setupPageHideListeners;
50
50
  private _teardown;
51
51
  private _flush;
52
+ private _disable;
52
53
  private _tick;
53
54
  }
54
55
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"event-tracker.d.ts","names":[],"sources":["../../../../../src/lib/hexclave-app/apps/implementations/event-tracker.ts"],"mappings":";;;KAoGY,gBAAA;EACV,SAAA;EACA,SAAA,GAAY,IAAA,UAAc,OAAA;IAAW,SAAA;EAAA,MAAyB,OAAA,CAAQ,MAAA,CAAO,QAAA,EAAU,KAAA;AAAA;AAAA,cAS5E,YAAA;EAAA,QACH,QAAA;EAAA,QACA,UAAA;EAAA,QACA,SAAA;EAAA,QACA,gBAAA;EAAA,QACA,WAAA;EAAA,QACA,OAAA;EAAA,QACA,YAAA;EAAA,QACA,QAAA;EAAA,iBACS,uBAAA;EAAA,iBACA,KAAA;EAAA,QAET,kBAAA;EAAA,QACA,qBAAA;EAAA,QAEA,eAAA;EAAA,QACA,0BAAA;EAAA,QAGA,mBAAA;EAAA,QACA,iBAAA;EAAA,QACA,eAAA;EAAA,QACA,yBAAA;EAAA,QACA,yBAAA;cAEI,IAAA,EAAM,gBAAA;EAKlB,KAAA,CAAA;EAsBA,IAAA,CAAA;EAUA,WAAA,CAAA;EAAA,QAMQ,UAAA;EAAA,QASA,gBAAA;EAAA,QA4BA,qBAAA;EAAA,iBA4BS,WAAA;EAAA,QAIT,cAAA;EAAA,QA6CA,sBAAA;EAAA,iBAWS,eAAA;EAAA,QAiDT,kBAAA;EAAA,iBAIS,kBAAA;EAAA,iBAIA,2BAAA;EAAA,iBAIA,4BAAA;EAAA,QAIT,wBAAA;EAAA,QA2BA,gBAAA;EAAA,QAoBA,2BAAA;EAAA,iBAeS,WAAA;EAAA,QAIT,uBAAA;EAAA,QASA,SAAA;EAAA,QA2BM,MAAA;EAAA,QAuDN,KAAA;AAAA"}
1
+ {"version":3,"file":"event-tracker.d.ts","names":[],"sources":["../../../../../src/lib/hexclave-app/apps/implementations/event-tracker.ts"],"mappings":";;;KAwGY,gBAAA;EACV,SAAA;EACA,SAAA,GAAY,IAAA,UAAc,OAAA;IAAW,SAAA;EAAA,MAAyB,OAAA,CAAQ,MAAA,CAAO,QAAA,EAAU,KAAA;AAAA;AAAA,cAS5E,YAAA;EAAA,QACH,QAAA;EAAA,QACA,UAAA;EAAA,QACA,SAAA;EAAA,QACA,gBAAA;EAAA,QACA,WAAA;EAAA,QACA,OAAA;EAAA,QACA,YAAA;EAAA,QACA,QAAA;EAAA,iBACS,uBAAA;EAAA,iBACA,KAAA;EAAA,QAET,kBAAA;EAAA,QACA,qBAAA;EAAA,QAEA,eAAA;EAAA,QACA,0BAAA;EAAA,QAGA,mBAAA;EAAA,QACA,iBAAA;EAAA,QACA,eAAA;EAAA,QACA,yBAAA;EAAA,QACA,yBAAA;cAEI,IAAA,EAAM,gBAAA;EAKlB,KAAA,CAAA;EAsBA,IAAA,CAAA;EAUA,WAAA,CAAA;EAAA,QAMQ,UAAA;EAAA,QASA,gBAAA;EAAA,QA4BA,qBAAA;EAAA,iBA4BS,WAAA;EAAA,QAIT,cAAA;EAAA,QA6CA,sBAAA;EAAA,iBAWS,eAAA;EAAA,QAiDT,kBAAA;EAAA,iBAIS,kBAAA;EAAA,iBAIA,2BAAA;EAAA,iBAIA,4BAAA;EAAA,QAIT,wBAAA;EAAA,QA2BA,gBAAA;EAAA,QAoBA,2BAAA;EAAA,iBAeS,WAAA;EAAA,QAIT,uBAAA;EAAA,QASA,SAAA;EAAA,QA2BM,MAAA;EAAA,QA+CN,QAAA;EAAA,QASA,KAAA;AAAA"}
@@ -21,6 +21,9 @@ function hasHistoryMethods(value) {
21
21
  if (!("pushState" in value) || !("replaceState" in value)) return false;
22
22
  return typeof value.pushState === "function" && typeof value.replaceState === "function";
23
23
  }
24
+ function getTextSnippet(textContent) {
25
+ return textContent == null ? "" : textContent.trim().substring(0, 200);
26
+ }
24
27
  const CLICKMAP_SCALE_FACTOR = 16;
25
28
  const DEAD_CLICK_SCROLL_THRESHOLD_MS = 100;
26
29
  const DEAD_CLICK_SELECTION_CHANGED_THRESHOLD_MS = 100;
@@ -85,7 +88,7 @@ var EventTracker = class {
85
88
  event_at_ms: Date.now(),
86
89
  data: {
87
90
  tag_name: target.tagName.toLowerCase(),
88
- text: target.textContent.trim().substring(0, 200),
91
+ text: getTextSnippet(target.textContent),
89
92
  href: this._findNearestAnchorHref(target),
90
93
  selector: this._buildSelector(target),
91
94
  elements_chain: (0, _hexclave_shared_dist_utils_elements_chain.buildElementsChain)(target),
@@ -333,21 +336,22 @@ var EventTracker = class {
333
336
  };
334
337
  const res = await this._deps.sendBatch(JSON.stringify(payload), { keepalive: options.keepalive });
335
338
  if (res.status === "error") {
339
+ if ((0, __session_replay_js.isAnalyticsNotEnabledError)(res.error)) {
340
+ this._disable();
341
+ return;
342
+ }
336
343
  console.warn("EventTracker flush failed:", res.error);
337
344
  return;
338
345
  }
339
- if (!res.data.ok) {
340
- if ((res.data.headers.get("x-hexclave-known-error") ?? res.data.headers.get("x-stack-known-error")) === "ANALYTICS_NOT_ENABLED") {
341
- this._disabled = true;
342
- if (this._flushTimer !== null) {
343
- clearInterval(this._flushTimer);
344
- this._flushTimer = null;
345
- }
346
- this._teardown();
347
- return;
348
- }
349
- console.warn("EventTracker flush failed:", res.data.status, await res.data.text());
346
+ if (!res.data.ok) console.warn("EventTracker flush failed:", res.data.status, await res.data.text());
347
+ }
348
+ _disable() {
349
+ this._disabled = true;
350
+ if (this._flushTimer !== null) {
351
+ clearInterval(this._flushTimer);
352
+ this._flushTimer = null;
350
353
  }
354
+ this._teardown();
351
355
  }
352
356
  _tick() {
353
357
  if (this._cancelled) return;
@@ -1 +1 @@
1
- {"version":3,"file":"event-tracker.js","names":["ELEMENTS_CHAIN_MAX_DEPTH","DEV_TOOL_ROOT_ID","CLICKMAP_ROOT_ID","cssEscapeIdent"],"sources":["../../../../../src/lib/hexclave-app/apps/implementations/event-tracker.ts"],"sourcesContent":["\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template\n//===========================================\nimport { isBrowserLike } from \"@hexclave/shared/dist/utils/env\";\nimport { CLICKMAP_ROOT_ID, DEV_TOOL_ROOT_ID } from \"@hexclave/shared/dist/utils/dev-tool\";\nimport { cssEscapeIdent } from \"@hexclave/shared/dist/utils/dom\";\nimport { buildElementsChain, ELEMENTS_CHAIN_MAX_DEPTH } from \"@hexclave/shared/dist/utils/elements-chain\";\nimport { runAsynchronously } from \"@hexclave/shared/dist/utils/promises\";\nimport { Result } from \"@hexclave/shared/dist/utils/results\";\nimport { generateUuid } from \"./session-replay\";\n\nconst FLUSH_INTERVAL_MS = 10_000;\nconst MAX_EVENTS_PER_BATCH = 50;\nconst MAX_APPROX_BYTES_PER_BATCH = 64_000;\n\nfunction hasScreenDimensions(value: unknown): value is { width: number, height: number } {\n if (value == null || typeof value !== \"object\") {\n return false;\n }\n if (!(\"width\" in value) || !(\"height\" in value)) {\n return false;\n }\n return typeof value.width === \"number\" && typeof value.height === \"number\";\n}\n\nfunction hasHistoryMethods(value: unknown): value is { pushState: History[\"pushState\"], replaceState: History[\"replaceState\"] } {\n if (value == null || typeof value !== \"object\") {\n return false;\n }\n if (!(\"pushState\" in value) || !(\"replaceState\" in value)) {\n return false;\n }\n return typeof value.pushState === \"function\" && typeof value.replaceState === \"function\";\n}\n\n// Pixel quantization factor for x/y/viewport in stored click events. Matches the\n// SCALE_FACTOR used by the ClickHouse clickmap_events MV — keep them in sync.\nconst CLICKMAP_SCALE_FACTOR = 16;\n\n// Dead-click detection (PostHog-style). Whether an element has a click handler\n// is unknowable from page script, so a click is classified by its observable\n// consequences instead: it is \"alive\" if the page scrolled, the text selection\n// changed, or the tab visibility changed (a new tab opened) almost\n// immediately, or if the DOM mutated within a couple of seconds — and \"dead\"\n// if none of that happened by the absolute timeout.\n//\n// The $click event is buffered immediately like any other event (so\n// event_at_ms, ordering, and every query are untouched) and the sweep sets\n// data.dead=1 on it in place if nothing observable happened. _flush holds\n// back clicks that are still unclassified — classification always finishes\n// well within one FLUSH_INTERVAL_MS, so a held click rides the next flush at\n// the latest. A keepalive flush (pagehide/stop) sends them unmarked: a click\n// still pending when the page unloads led to that navigation, alive by\n// definition.\n//\n// NOTE — blocker for any future real-time / \"live clicks\" view: a click that\n// is still unclassified when its natural flush fires arrives up to one extra\n// FLUSH_INTERVAL_MS late. A surface showing clicks as they happen must either\n// accept that lag or emit a provisional $click plus a later dead-click\n// reconciliation event.\nconst DEAD_CLICK_SCROLL_THRESHOLD_MS = 100;\nconst DEAD_CLICK_SELECTION_CHANGED_THRESHOLD_MS = 100;\nconst DEAD_CLICK_VISIBILITY_CHANGE_THRESHOLD_MS = 100;\nconst DEAD_CLICK_MUTATION_THRESHOLD_MS = 2_500;\n// 1.1x the mutation threshold, mirroring posthog-js: every signal window has\n// closed before a click is declared dead.\nconst DEAD_CLICK_ABSOLUTE_TIMEOUT_MS = 2_750;\nconst DEAD_CLICK_CHECK_INTERVAL_MS = 1_000;\n// Backstop against click storms (e.g. rage clicks on a dead element): past the\n// cap, clicks are simply not classified rather than not recorded.\nconst DEAD_CLICK_MAX_PENDING = 50;\n\nfunction isPointerTargetFixed(element: Element): boolean {\n let current: Element | null = element;\n let depth = 0;\n while (current != null && depth < ELEMENTS_CHAIN_MAX_DEPTH * 2) {\n const style = window.getComputedStyle(current);\n if (style.position === \"fixed\" || style.position === \"sticky\") {\n return true;\n }\n current = current.parentElement;\n depth += 1;\n }\n return false;\n}\n\n// Clicks on Hexclave's own in-page UI (the dev tool and the standalone\n// clickmap overlay) must never be ingested as analytics events.\nfunction isInsideHexclaveUi(element: Element): boolean {\n return element.closest(`#${cssEscapeIdent(DEV_TOOL_ROOT_ID)}, #${cssEscapeIdent(CLICKMAP_ROOT_ID)}`) != null;\n}\n\n// Mutation-record targets can be text/comment nodes; resolve to the nearest\n// element before asking whether the mutation came from Hexclave's own UI.\nfunction isInsideHexclaveUiNode(node: Node | null): boolean {\n const element = node instanceof Element ? node : node?.parentElement ?? null;\n return element != null && isInsideHexclaveUi(element);\n}\n\nexport type EventTrackerDeps = {\n projectId: string,\n sendBatch: (body: string, options: { keepalive: boolean }) => Promise<Result<Response, Error>>,\n};\n\ntype TrackedEvent = {\n event_type: \"$page-view\" | \"$click\",\n event_at_ms: number,\n data: Record<string, unknown>,\n};\n\nexport class EventTracker {\n private _started = false;\n private _cancelled = false;\n private _disabled = false;\n private _detachListeners: (() => void) | null = null;\n private _flushTimer: ReturnType<typeof setInterval> | null = null;\n private _events: TrackedEvent[] = [];\n private _approxBytes = 0;\n private _lastUrl: string | null = null;\n private readonly _sessionReplaySegmentId: string;\n private readonly _deps: EventTrackerDeps;\n\n private _originalPushState: History[\"pushState\"] | null = null;\n private _originalReplaceState: History[\"replaceState\"] | null = null;\n\n private _deadClickTimer: ReturnType<typeof setInterval> | null = null;\n private _deadClickMutationObserver: MutationObserver | null = null;\n // Buffered $click events still awaiting dead-click classification. Always a\n // subset of _events — _flush holds these back until the sweep resolves them.\n private _unclassifiedClicks = new Set<TrackedEvent>();\n private _lastMutationAtMs: number | null = null;\n private _lastScrollAtMs: number | null = null;\n private _lastSelectionChangedAtMs: number | null = null;\n private _lastVisibilityChangeAtMs: number | null = null;\n\n constructor(deps: EventTrackerDeps) {\n this._deps = deps;\n this._sessionReplaySegmentId = generateUuid();\n }\n\n start() {\n if (this._started) return;\n if (!isBrowserLike()) return;\n if (\n typeof window.addEventListener !== \"function\"\n || typeof window.removeEventListener !== \"function\"\n || typeof document.addEventListener !== \"function\"\n || typeof document.removeEventListener !== \"function\"\n || !hasScreenDimensions(window.screen)\n ) {\n return;\n }\n this._started = true;\n\n this._setupPageViewCapture();\n this._setupClickCapture();\n this._setupDeadClickDetection();\n this._setupPageHideListeners();\n\n this._flushTimer = setInterval(() => this._tick(), FLUSH_INTERVAL_MS);\n }\n\n stop() {\n this._cancelled = true;\n if (this._flushTimer !== null) {\n clearInterval(this._flushTimer);\n this._flushTimer = null;\n }\n runAsynchronously(() => this._flush({ keepalive: true }));\n this._teardown();\n }\n\n clearBuffer() {\n this._events = [];\n this._approxBytes = 0;\n this._unclassifiedClicks.clear();\n }\n\n private _pushEvent(event: TrackedEvent) {\n if (this._disabled) return;\n this._events.push(event);\n this._approxBytes += JSON.stringify(event).length;\n if (this._events.length >= MAX_EVENTS_PER_BATCH || this._approxBytes >= MAX_APPROX_BYTES_PER_BATCH) {\n runAsynchronously(() => this._flush({ keepalive: false }));\n }\n }\n\n private _capturePageView(entryType: \"initial\" | \"push\" | \"replace\" | \"pop\") {\n const screenObject = window.screen;\n if (!hasScreenDimensions(screenObject)) {\n return;\n }\n\n const url = window.location.href;\n if (url === this._lastUrl && entryType !== \"initial\") return;\n this._lastUrl = url;\n\n this._pushEvent({\n event_type: \"$page-view\",\n event_at_ms: Date.now(),\n data: {\n url,\n path: window.location.pathname,\n referrer: document.referrer,\n title: document.title,\n entry_type: entryType,\n viewport_width: window.innerWidth,\n viewport_height: window.innerHeight,\n screen_width: screenObject.width,\n screen_height: screenObject.height,\n user_agent: typeof navigator !== \"undefined\" ? navigator.userAgent : null,\n },\n });\n }\n\n private _setupPageViewCapture() {\n // Fire initial page-view\n this._capturePageView(\"initial\");\n const historyObject = window.history;\n if (!hasHistoryMethods(historyObject)) {\n return;\n }\n const originalPushState = historyObject.pushState;\n const originalReplaceState = historyObject.replaceState;\n\n // Monkey-patch history.pushState\n this._originalPushState = (...args: Parameters<History[\"pushState\"]>) => originalPushState.apply(historyObject, args);\n historyObject.pushState = (...args: Parameters<History[\"pushState\"]>) => {\n this._originalPushState!(...args);\n this._capturePageView(\"push\");\n };\n\n // Monkey-patch history.replaceState\n this._originalReplaceState = (...args: Parameters<History[\"replaceState\"]>) => originalReplaceState.apply(historyObject, args);\n historyObject.replaceState = (...args: Parameters<History[\"replaceState\"]>) => {\n this._originalReplaceState!(...args);\n this._capturePageView(\"replace\");\n };\n\n // Listen for popstate (back/forward navigation)\n window.addEventListener(\"popstate\", this._onPopState);\n }\n\n private readonly _onPopState = () => {\n this._capturePageView(\"pop\");\n };\n\n private _buildSelector(element: Element): string {\n const parts: string[] = [];\n let current: Element | null = element;\n let depth = 0;\n\n while (current && depth < 8 && current !== document.documentElement) {\n let part = current.tagName.toLowerCase();\n let testIdAttr = \"data-testid\";\n let testId = current.getAttribute(\"data-testid\");\n if (testId == null) {\n testIdAttr = \"data-test-id\";\n testId = current.getAttribute(\"data-test-id\");\n }\n if (testId != null && testId.trim() !== \"\") {\n part += `[${testIdAttr}=\"${testId.replace(/\"/g, '\\\\\"')}\"]`;\n parts.unshift(part);\n break;\n }\n if (current.id !== \"\") {\n part += `#${cssEscapeIdent(current.id)}`;\n parts.unshift(part);\n break;\n }\n if (current.className && typeof current.className === \"string\") {\n const classes = current.className.trim().split(/\\s+/).filter(Boolean).slice(0, 4);\n if (classes.length > 0) {\n part += `.${classes.map(cssEscapeIdent).join(\".\")}`;\n }\n }\n const parent: Element | null = current.parentElement;\n if (parent != null) {\n const tagName = current.tagName;\n const siblings = Array.from(parent.children).filter((child) => child.tagName === tagName);\n if (siblings.length > 1) {\n part += `:nth-of-type(${siblings.indexOf(current) + 1})`;\n }\n }\n parts.unshift(part);\n current = parent;\n depth++;\n }\n\n return parts.join(\" > \");\n }\n\n private _findNearestAnchorHref(element: Element): string | null {\n let current: Element | null = element;\n while (current) {\n if (current.tagName === \"A\" && current.hasAttribute(\"href\")) {\n return current.getAttribute(\"href\");\n }\n current = current.parentElement;\n }\n return null;\n }\n\n private readonly _onClickCapture = (event: MouseEvent) => {\n const target = event.target;\n if (!(target instanceof Element)) return;\n if (isInsideHexclaveUi(target)) return;\n\n const viewportWidth = window.innerWidth;\n const viewportHeight = window.innerHeight;\n const pointerTargetFixed = isPointerTargetFixed(target);\n // Pre-scale at ingest so old + new rows land in identical buckets in CH.\n const xScaled = Math.round(event.pageX / CLICKMAP_SCALE_FACTOR);\n const yScaled = Math.round(event.pageY / CLICKMAP_SCALE_FACTOR);\n const clientYScaled = Math.round(event.clientY / CLICKMAP_SCALE_FACTOR);\n const relativeX = viewportWidth > 0 ? event.clientX / viewportWidth : 0;\n\n const clickEvent: TrackedEvent = {\n event_type: \"$click\",\n event_at_ms: Date.now(),\n data: {\n tag_name: target.tagName.toLowerCase(),\n text: target.textContent.trim().substring(0, 200),\n href: this._findNearestAnchorHref(target),\n selector: this._buildSelector(target),\n elements_chain: buildElementsChain(target),\n pointer_target_fixed: pointerTargetFixed ? 1 : 0,\n url: window.location.href,\n path: window.location.pathname,\n title: document.title,\n x: event.clientX,\n y: event.clientY,\n page_x: event.pageX,\n page_y: event.pageY,\n x_scaled: xScaled,\n y_scaled: yScaled,\n client_y_scaled: clientYScaled,\n pointer_relative_x: relativeX,\n viewport_width: viewportWidth,\n viewport_height: viewportHeight,\n scale_factor: CLICKMAP_SCALE_FACTOR,\n },\n };\n\n // Register for dead-click classification before buffering, so a\n // size-triggered flush from this very push already holds the click back.\n if (this._deadClickTimer !== null && this._unclassifiedClicks.size < DEAD_CLICK_MAX_PENDING) {\n this._unclassifiedClicks.add(clickEvent);\n }\n this._pushEvent(clickEvent);\n };\n\n private _setupClickCapture() {\n document.addEventListener(\"click\", this._onClickCapture, { capture: true });\n }\n\n private readonly _onDeadClickScroll = () => {\n this._lastScrollAtMs = Date.now();\n };\n\n private readonly _onDeadClickSelectionChange = () => {\n this._lastSelectionChangedAtMs = Date.now();\n };\n\n private readonly _onDeadClickVisibilityChange = () => {\n this._lastVisibilityChangeAtMs = Date.now();\n };\n\n private _setupDeadClickDetection() {\n if (typeof MutationObserver !== \"function\") return;\n\n this._deadClickMutationObserver = new MutationObserver((mutations) => {\n // The dev tool and the clickmap overlay rewrite their own DOM constantly\n // while open; their mutations must not mark host-page clicks as alive.\n if (mutations.every((mutation) => isInsideHexclaveUiNode(mutation.target))) {\n return;\n }\n this._lastMutationAtMs = Date.now();\n });\n this._deadClickMutationObserver.observe(document.documentElement, {\n childList: true,\n attributes: true,\n characterData: true,\n subtree: true,\n });\n\n // Capture phase so scrolls inside nested scroll containers count, not just\n // the document itself (scroll events don't bubble).\n document.addEventListener(\"scroll\", this._onDeadClickScroll, { capture: true, passive: true });\n document.addEventListener(\"selectionchange\", this._onDeadClickSelectionChange);\n document.addEventListener(\"visibilitychange\", this._onDeadClickVisibilityChange);\n\n this._deadClickTimer = setInterval(() => this._checkDeadClicks(), DEAD_CLICK_CHECK_INTERVAL_MS);\n }\n\n private _checkDeadClicks() {\n const nowMs = Date.now();\n for (const click of this._unclassifiedClicks) {\n const signalWithin = (signalAtMs: number | null, thresholdMs: number) =>\n signalAtMs != null && signalAtMs >= click.event_at_ms && signalAtMs - click.event_at_ms < thresholdMs;\n\n const isAlive = signalWithin(this._lastScrollAtMs, DEAD_CLICK_SCROLL_THRESHOLD_MS)\n || signalWithin(this._lastSelectionChangedAtMs, DEAD_CLICK_SELECTION_CHANGED_THRESHOLD_MS)\n || signalWithin(this._lastVisibilityChangeAtMs, DEAD_CLICK_VISIBILITY_CHANGE_THRESHOLD_MS)\n || signalWithin(this._lastMutationAtMs, DEAD_CLICK_MUTATION_THRESHOLD_MS);\n if (isAlive) {\n this._unclassifiedClicks.delete(click);\n } else if (nowMs - click.event_at_ms >= DEAD_CLICK_ABSOLUTE_TIMEOUT_MS) {\n // The already-buffered event is marked in place — no second event.\n click.data.dead = 1;\n this._unclassifiedClicks.delete(click);\n }\n }\n }\n\n private _teardownDeadClickDetection() {\n if (this._deadClickTimer !== null) {\n clearInterval(this._deadClickTimer);\n this._deadClickTimer = null;\n }\n if (this._deadClickMutationObserver !== null) {\n this._deadClickMutationObserver.disconnect();\n this._deadClickMutationObserver = null;\n }\n document.removeEventListener(\"scroll\", this._onDeadClickScroll, { capture: true });\n document.removeEventListener(\"selectionchange\", this._onDeadClickSelectionChange);\n document.removeEventListener(\"visibilitychange\", this._onDeadClickVisibilityChange);\n this._unclassifiedClicks.clear();\n }\n\n private readonly _onPageHide = () => {\n runAsynchronously(() => this._flush({ keepalive: true }));\n };\n\n private _setupPageHideListeners() {\n window.addEventListener(\"pagehide\", this._onPageHide);\n document.addEventListener(\"visibilitychange\", this._onPageHide);\n this._detachListeners = () => {\n window.removeEventListener(\"pagehide\", this._onPageHide);\n document.removeEventListener(\"visibilitychange\", this._onPageHide);\n };\n }\n\n private _teardown() {\n if (this._detachListeners) {\n this._detachListeners();\n this._detachListeners = null;\n }\n\n // Restore history methods\n const historyObject = window.history;\n if (hasHistoryMethods(historyObject)) {\n if (this._originalPushState) {\n historyObject.pushState = this._originalPushState;\n }\n if (this._originalReplaceState) {\n historyObject.replaceState = this._originalReplaceState;\n }\n }\n this._originalPushState = null;\n this._originalReplaceState = null;\n\n window.removeEventListener(\"popstate\", this._onPopState);\n document.removeEventListener(\"click\", this._onClickCapture, { capture: true });\n this._teardownDeadClickDetection();\n\n this._events = [];\n this._approxBytes = 0;\n }\n\n private async _flush(options: { keepalive: boolean }) {\n if (this._disabled) return;\n\n // A keepalive flush means the page is unloading — a click still awaiting\n // dead-click classification led to that unload, so it is alive by\n // definition and ships unmarked.\n if (options.keepalive) {\n this._unclassifiedClicks.clear();\n }\n\n // Clicks still awaiting classification stay buffered so the sweep can\n // mark them dead in place; classification finishes well within one flush\n // interval, so they ride the next flush at the latest.\n const events = this._events.filter((event) => !this._unclassifiedClicks.has(event));\n if (events.length === 0) return;\n this._events = this._events.filter((event) => this._unclassifiedClicks.has(event));\n this._approxBytes = this._events.reduce((total, event) => total + JSON.stringify(event).length, 0);\n\n const nowMs = Date.now();\n\n const batchId = generateUuid();\n const payload = {\n session_replay_segment_id: this._sessionReplaySegmentId,\n batch_id: batchId,\n sent_at_ms: nowMs,\n events,\n };\n\n const res = await this._deps.sendBatch(\n JSON.stringify(payload),\n { keepalive: options.keepalive },\n );\n\n if (res.status === \"error\") {\n console.warn(\"EventTracker flush failed:\", res.error);\n return;\n }\n\n if (!res.data.ok) {\n // If the server tells us analytics is not enabled for this project,\n // silently disable the tracker — no point retrying or warning the user.\n const knownError = res.data.headers.get(\"x-hexclave-known-error\") ?? res.data.headers.get(\"x-stack-known-error\");\n if (knownError === \"ANALYTICS_NOT_ENABLED\") {\n this._disabled = true;\n if (this._flushTimer !== null) {\n clearInterval(this._flushTimer);\n this._flushTimer = null;\n }\n this._teardown();\n return;\n }\n console.warn(\"EventTracker flush failed:\", res.data.status, await res.data.text());\n }\n }\n\n private _tick() {\n if (this._cancelled) return;\n if (this._events.length > 0) {\n runAsynchronously(() => this._flush({ keepalive: false }));\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"event-tracker.js","names":["ELEMENTS_CHAIN_MAX_DEPTH","DEV_TOOL_ROOT_ID","CLICKMAP_ROOT_ID","cssEscapeIdent"],"sources":["../../../../../src/lib/hexclave-app/apps/implementations/event-tracker.ts"],"sourcesContent":["\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template\n//===========================================\nimport { isBrowserLike } from \"@hexclave/shared/dist/utils/env\";\nimport { CLICKMAP_ROOT_ID, DEV_TOOL_ROOT_ID } from \"@hexclave/shared/dist/utils/dev-tool\";\nimport { cssEscapeIdent } from \"@hexclave/shared/dist/utils/dom\";\nimport { buildElementsChain, ELEMENTS_CHAIN_MAX_DEPTH } from \"@hexclave/shared/dist/utils/elements-chain\";\nimport { runAsynchronously } from \"@hexclave/shared/dist/utils/promises\";\nimport { Result } from \"@hexclave/shared/dist/utils/results\";\nimport { generateUuid, isAnalyticsNotEnabledError } from \"./session-replay\";\n\nconst FLUSH_INTERVAL_MS = 10_000;\nconst MAX_EVENTS_PER_BATCH = 50;\nconst MAX_APPROX_BYTES_PER_BATCH = 64_000;\n\nfunction hasScreenDimensions(value: unknown): value is { width: number, height: number } {\n if (value == null || typeof value !== \"object\") {\n return false;\n }\n if (!(\"width\" in value) || !(\"height\" in value)) {\n return false;\n }\n return typeof value.width === \"number\" && typeof value.height === \"number\";\n}\n\nfunction hasHistoryMethods(value: unknown): value is { pushState: History[\"pushState\"], replaceState: History[\"replaceState\"] } {\n if (value == null || typeof value !== \"object\") {\n return false;\n }\n if (!(\"pushState\" in value) || !(\"replaceState\" in value)) {\n return false;\n }\n return typeof value.pushState === \"function\" && typeof value.replaceState === \"function\";\n}\n\nfunction getTextSnippet(textContent: string | null): string {\n return textContent == null ? \"\" : textContent.trim().substring(0, 200);\n}\n\n// Pixel quantization factor for x/y/viewport in stored click events. Matches the\n// SCALE_FACTOR used by the ClickHouse clickmap_events MV — keep them in sync.\nconst CLICKMAP_SCALE_FACTOR = 16;\n\n// Dead-click detection (PostHog-style). Whether an element has a click handler\n// is unknowable from page script, so a click is classified by its observable\n// consequences instead: it is \"alive\" if the page scrolled, the text selection\n// changed, or the tab visibility changed (a new tab opened) almost\n// immediately, or if the DOM mutated within a couple of seconds — and \"dead\"\n// if none of that happened by the absolute timeout.\n//\n// The $click event is buffered immediately like any other event (so\n// event_at_ms, ordering, and every query are untouched) and the sweep sets\n// data.dead=1 on it in place if nothing observable happened. _flush holds\n// back clicks that are still unclassified — classification always finishes\n// well within one FLUSH_INTERVAL_MS, so a held click rides the next flush at\n// the latest. A keepalive flush (pagehide/stop) sends them unmarked: a click\n// still pending when the page unloads led to that navigation, alive by\n// definition.\n//\n// NOTE — blocker for any future real-time / \"live clicks\" view: a click that\n// is still unclassified when its natural flush fires arrives up to one extra\n// FLUSH_INTERVAL_MS late. A surface showing clicks as they happen must either\n// accept that lag or emit a provisional $click plus a later dead-click\n// reconciliation event.\nconst DEAD_CLICK_SCROLL_THRESHOLD_MS = 100;\nconst DEAD_CLICK_SELECTION_CHANGED_THRESHOLD_MS = 100;\nconst DEAD_CLICK_VISIBILITY_CHANGE_THRESHOLD_MS = 100;\nconst DEAD_CLICK_MUTATION_THRESHOLD_MS = 2_500;\n// 1.1x the mutation threshold, mirroring posthog-js: every signal window has\n// closed before a click is declared dead.\nconst DEAD_CLICK_ABSOLUTE_TIMEOUT_MS = 2_750;\nconst DEAD_CLICK_CHECK_INTERVAL_MS = 1_000;\n// Backstop against click storms (e.g. rage clicks on a dead element): past the\n// cap, clicks are simply not classified rather than not recorded.\nconst DEAD_CLICK_MAX_PENDING = 50;\n\nfunction isPointerTargetFixed(element: Element): boolean {\n let current: Element | null = element;\n let depth = 0;\n while (current != null && depth < ELEMENTS_CHAIN_MAX_DEPTH * 2) {\n const style = window.getComputedStyle(current);\n if (style.position === \"fixed\" || style.position === \"sticky\") {\n return true;\n }\n current = current.parentElement;\n depth += 1;\n }\n return false;\n}\n\n// Clicks on Hexclave's own in-page UI (the dev tool and the standalone\n// clickmap overlay) must never be ingested as analytics events.\nfunction isInsideHexclaveUi(element: Element): boolean {\n return element.closest(`#${cssEscapeIdent(DEV_TOOL_ROOT_ID)}, #${cssEscapeIdent(CLICKMAP_ROOT_ID)}`) != null;\n}\n\n// Mutation-record targets can be text/comment nodes; resolve to the nearest\n// element before asking whether the mutation came from Hexclave's own UI.\nfunction isInsideHexclaveUiNode(node: Node | null): boolean {\n const element = node instanceof Element ? node : node?.parentElement ?? null;\n return element != null && isInsideHexclaveUi(element);\n}\n\nexport type EventTrackerDeps = {\n projectId: string,\n sendBatch: (body: string, options: { keepalive: boolean }) => Promise<Result<Response, Error>>,\n};\n\ntype TrackedEvent = {\n event_type: \"$page-view\" | \"$click\",\n event_at_ms: number,\n data: Record<string, unknown>,\n};\n\nexport class EventTracker {\n private _started = false;\n private _cancelled = false;\n private _disabled = false;\n private _detachListeners: (() => void) | null = null;\n private _flushTimer: ReturnType<typeof setInterval> | null = null;\n private _events: TrackedEvent[] = [];\n private _approxBytes = 0;\n private _lastUrl: string | null = null;\n private readonly _sessionReplaySegmentId: string;\n private readonly _deps: EventTrackerDeps;\n\n private _originalPushState: History[\"pushState\"] | null = null;\n private _originalReplaceState: History[\"replaceState\"] | null = null;\n\n private _deadClickTimer: ReturnType<typeof setInterval> | null = null;\n private _deadClickMutationObserver: MutationObserver | null = null;\n // Buffered $click events still awaiting dead-click classification. Always a\n // subset of _events — _flush holds these back until the sweep resolves them.\n private _unclassifiedClicks = new Set<TrackedEvent>();\n private _lastMutationAtMs: number | null = null;\n private _lastScrollAtMs: number | null = null;\n private _lastSelectionChangedAtMs: number | null = null;\n private _lastVisibilityChangeAtMs: number | null = null;\n\n constructor(deps: EventTrackerDeps) {\n this._deps = deps;\n this._sessionReplaySegmentId = generateUuid();\n }\n\n start() {\n if (this._started) return;\n if (!isBrowserLike()) return;\n if (\n typeof window.addEventListener !== \"function\"\n || typeof window.removeEventListener !== \"function\"\n || typeof document.addEventListener !== \"function\"\n || typeof document.removeEventListener !== \"function\"\n || !hasScreenDimensions(window.screen)\n ) {\n return;\n }\n this._started = true;\n\n this._setupPageViewCapture();\n this._setupClickCapture();\n this._setupDeadClickDetection();\n this._setupPageHideListeners();\n\n this._flushTimer = setInterval(() => this._tick(), FLUSH_INTERVAL_MS);\n }\n\n stop() {\n this._cancelled = true;\n if (this._flushTimer !== null) {\n clearInterval(this._flushTimer);\n this._flushTimer = null;\n }\n runAsynchronously(() => this._flush({ keepalive: true }));\n this._teardown();\n }\n\n clearBuffer() {\n this._events = [];\n this._approxBytes = 0;\n this._unclassifiedClicks.clear();\n }\n\n private _pushEvent(event: TrackedEvent) {\n if (this._disabled) return;\n this._events.push(event);\n this._approxBytes += JSON.stringify(event).length;\n if (this._events.length >= MAX_EVENTS_PER_BATCH || this._approxBytes >= MAX_APPROX_BYTES_PER_BATCH) {\n runAsynchronously(() => this._flush({ keepalive: false }));\n }\n }\n\n private _capturePageView(entryType: \"initial\" | \"push\" | \"replace\" | \"pop\") {\n const screenObject = window.screen;\n if (!hasScreenDimensions(screenObject)) {\n return;\n }\n\n const url = window.location.href;\n if (url === this._lastUrl && entryType !== \"initial\") return;\n this._lastUrl = url;\n\n this._pushEvent({\n event_type: \"$page-view\",\n event_at_ms: Date.now(),\n data: {\n url,\n path: window.location.pathname,\n referrer: document.referrer,\n title: document.title,\n entry_type: entryType,\n viewport_width: window.innerWidth,\n viewport_height: window.innerHeight,\n screen_width: screenObject.width,\n screen_height: screenObject.height,\n user_agent: typeof navigator !== \"undefined\" ? navigator.userAgent : null,\n },\n });\n }\n\n private _setupPageViewCapture() {\n // Fire initial page-view\n this._capturePageView(\"initial\");\n const historyObject = window.history;\n if (!hasHistoryMethods(historyObject)) {\n return;\n }\n const originalPushState = historyObject.pushState;\n const originalReplaceState = historyObject.replaceState;\n\n // Monkey-patch history.pushState\n this._originalPushState = (...args: Parameters<History[\"pushState\"]>) => originalPushState.apply(historyObject, args);\n historyObject.pushState = (...args: Parameters<History[\"pushState\"]>) => {\n this._originalPushState!(...args);\n this._capturePageView(\"push\");\n };\n\n // Monkey-patch history.replaceState\n this._originalReplaceState = (...args: Parameters<History[\"replaceState\"]>) => originalReplaceState.apply(historyObject, args);\n historyObject.replaceState = (...args: Parameters<History[\"replaceState\"]>) => {\n this._originalReplaceState!(...args);\n this._capturePageView(\"replace\");\n };\n\n // Listen for popstate (back/forward navigation)\n window.addEventListener(\"popstate\", this._onPopState);\n }\n\n private readonly _onPopState = () => {\n this._capturePageView(\"pop\");\n };\n\n private _buildSelector(element: Element): string {\n const parts: string[] = [];\n let current: Element | null = element;\n let depth = 0;\n\n while (current && depth < 8 && current !== document.documentElement) {\n let part = current.tagName.toLowerCase();\n let testIdAttr = \"data-testid\";\n let testId = current.getAttribute(\"data-testid\");\n if (testId == null) {\n testIdAttr = \"data-test-id\";\n testId = current.getAttribute(\"data-test-id\");\n }\n if (testId != null && testId.trim() !== \"\") {\n part += `[${testIdAttr}=\"${testId.replace(/\"/g, '\\\\\"')}\"]`;\n parts.unshift(part);\n break;\n }\n if (current.id !== \"\") {\n part += `#${cssEscapeIdent(current.id)}`;\n parts.unshift(part);\n break;\n }\n if (current.className && typeof current.className === \"string\") {\n const classes = current.className.trim().split(/\\s+/).filter(Boolean).slice(0, 4);\n if (classes.length > 0) {\n part += `.${classes.map(cssEscapeIdent).join(\".\")}`;\n }\n }\n const parent: Element | null = current.parentElement;\n if (parent != null) {\n const tagName = current.tagName;\n const siblings = Array.from(parent.children).filter((child) => child.tagName === tagName);\n if (siblings.length > 1) {\n part += `:nth-of-type(${siblings.indexOf(current) + 1})`;\n }\n }\n parts.unshift(part);\n current = parent;\n depth++;\n }\n\n return parts.join(\" > \");\n }\n\n private _findNearestAnchorHref(element: Element): string | null {\n let current: Element | null = element;\n while (current) {\n if (current.tagName === \"A\" && current.hasAttribute(\"href\")) {\n return current.getAttribute(\"href\");\n }\n current = current.parentElement;\n }\n return null;\n }\n\n private readonly _onClickCapture = (event: MouseEvent) => {\n const target = event.target;\n if (!(target instanceof Element)) return;\n if (isInsideHexclaveUi(target)) return;\n\n const viewportWidth = window.innerWidth;\n const viewportHeight = window.innerHeight;\n const pointerTargetFixed = isPointerTargetFixed(target);\n // Pre-scale at ingest so old + new rows land in identical buckets in CH.\n const xScaled = Math.round(event.pageX / CLICKMAP_SCALE_FACTOR);\n const yScaled = Math.round(event.pageY / CLICKMAP_SCALE_FACTOR);\n const clientYScaled = Math.round(event.clientY / CLICKMAP_SCALE_FACTOR);\n const relativeX = viewportWidth > 0 ? event.clientX / viewportWidth : 0;\n\n const clickEvent: TrackedEvent = {\n event_type: \"$click\",\n event_at_ms: Date.now(),\n data: {\n tag_name: target.tagName.toLowerCase(),\n text: getTextSnippet(target.textContent),\n href: this._findNearestAnchorHref(target),\n selector: this._buildSelector(target),\n elements_chain: buildElementsChain(target),\n pointer_target_fixed: pointerTargetFixed ? 1 : 0,\n url: window.location.href,\n path: window.location.pathname,\n title: document.title,\n x: event.clientX,\n y: event.clientY,\n page_x: event.pageX,\n page_y: event.pageY,\n x_scaled: xScaled,\n y_scaled: yScaled,\n client_y_scaled: clientYScaled,\n pointer_relative_x: relativeX,\n viewport_width: viewportWidth,\n viewport_height: viewportHeight,\n scale_factor: CLICKMAP_SCALE_FACTOR,\n },\n };\n\n // Register for dead-click classification before buffering, so a\n // size-triggered flush from this very push already holds the click back.\n if (this._deadClickTimer !== null && this._unclassifiedClicks.size < DEAD_CLICK_MAX_PENDING) {\n this._unclassifiedClicks.add(clickEvent);\n }\n this._pushEvent(clickEvent);\n };\n\n private _setupClickCapture() {\n document.addEventListener(\"click\", this._onClickCapture, { capture: true });\n }\n\n private readonly _onDeadClickScroll = () => {\n this._lastScrollAtMs = Date.now();\n };\n\n private readonly _onDeadClickSelectionChange = () => {\n this._lastSelectionChangedAtMs = Date.now();\n };\n\n private readonly _onDeadClickVisibilityChange = () => {\n this._lastVisibilityChangeAtMs = Date.now();\n };\n\n private _setupDeadClickDetection() {\n if (typeof MutationObserver !== \"function\") return;\n\n this._deadClickMutationObserver = new MutationObserver((mutations) => {\n // The dev tool and the clickmap overlay rewrite their own DOM constantly\n // while open; their mutations must not mark host-page clicks as alive.\n if (mutations.every((mutation) => isInsideHexclaveUiNode(mutation.target))) {\n return;\n }\n this._lastMutationAtMs = Date.now();\n });\n this._deadClickMutationObserver.observe(document.documentElement, {\n childList: true,\n attributes: true,\n characterData: true,\n subtree: true,\n });\n\n // Capture phase so scrolls inside nested scroll containers count, not just\n // the document itself (scroll events don't bubble).\n document.addEventListener(\"scroll\", this._onDeadClickScroll, { capture: true, passive: true });\n document.addEventListener(\"selectionchange\", this._onDeadClickSelectionChange);\n document.addEventListener(\"visibilitychange\", this._onDeadClickVisibilityChange);\n\n this._deadClickTimer = setInterval(() => this._checkDeadClicks(), DEAD_CLICK_CHECK_INTERVAL_MS);\n }\n\n private _checkDeadClicks() {\n const nowMs = Date.now();\n for (const click of this._unclassifiedClicks) {\n const signalWithin = (signalAtMs: number | null, thresholdMs: number) =>\n signalAtMs != null && signalAtMs >= click.event_at_ms && signalAtMs - click.event_at_ms < thresholdMs;\n\n const isAlive = signalWithin(this._lastScrollAtMs, DEAD_CLICK_SCROLL_THRESHOLD_MS)\n || signalWithin(this._lastSelectionChangedAtMs, DEAD_CLICK_SELECTION_CHANGED_THRESHOLD_MS)\n || signalWithin(this._lastVisibilityChangeAtMs, DEAD_CLICK_VISIBILITY_CHANGE_THRESHOLD_MS)\n || signalWithin(this._lastMutationAtMs, DEAD_CLICK_MUTATION_THRESHOLD_MS);\n if (isAlive) {\n this._unclassifiedClicks.delete(click);\n } else if (nowMs - click.event_at_ms >= DEAD_CLICK_ABSOLUTE_TIMEOUT_MS) {\n // The already-buffered event is marked in place — no second event.\n click.data.dead = 1;\n this._unclassifiedClicks.delete(click);\n }\n }\n }\n\n private _teardownDeadClickDetection() {\n if (this._deadClickTimer !== null) {\n clearInterval(this._deadClickTimer);\n this._deadClickTimer = null;\n }\n if (this._deadClickMutationObserver !== null) {\n this._deadClickMutationObserver.disconnect();\n this._deadClickMutationObserver = null;\n }\n document.removeEventListener(\"scroll\", this._onDeadClickScroll, { capture: true });\n document.removeEventListener(\"selectionchange\", this._onDeadClickSelectionChange);\n document.removeEventListener(\"visibilitychange\", this._onDeadClickVisibilityChange);\n this._unclassifiedClicks.clear();\n }\n\n private readonly _onPageHide = () => {\n runAsynchronously(() => this._flush({ keepalive: true }));\n };\n\n private _setupPageHideListeners() {\n window.addEventListener(\"pagehide\", this._onPageHide);\n document.addEventListener(\"visibilitychange\", this._onPageHide);\n this._detachListeners = () => {\n window.removeEventListener(\"pagehide\", this._onPageHide);\n document.removeEventListener(\"visibilitychange\", this._onPageHide);\n };\n }\n\n private _teardown() {\n if (this._detachListeners) {\n this._detachListeners();\n this._detachListeners = null;\n }\n\n // Restore history methods\n const historyObject = window.history;\n if (hasHistoryMethods(historyObject)) {\n if (this._originalPushState) {\n historyObject.pushState = this._originalPushState;\n }\n if (this._originalReplaceState) {\n historyObject.replaceState = this._originalReplaceState;\n }\n }\n this._originalPushState = null;\n this._originalReplaceState = null;\n\n window.removeEventListener(\"popstate\", this._onPopState);\n document.removeEventListener(\"click\", this._onClickCapture, { capture: true });\n this._teardownDeadClickDetection();\n\n this._events = [];\n this._approxBytes = 0;\n }\n\n private async _flush(options: { keepalive: boolean }) {\n if (this._disabled) return;\n\n // A keepalive flush means the page is unloading — a click still awaiting\n // dead-click classification led to that unload, so it is alive by\n // definition and ships unmarked.\n if (options.keepalive) {\n this._unclassifiedClicks.clear();\n }\n\n // Clicks still awaiting classification stay buffered so the sweep can\n // mark them dead in place; classification finishes well within one flush\n // interval, so they ride the next flush at the latest.\n const events = this._events.filter((event) => !this._unclassifiedClicks.has(event));\n if (events.length === 0) return;\n this._events = this._events.filter((event) => this._unclassifiedClicks.has(event));\n this._approxBytes = this._events.reduce((total, event) => total + JSON.stringify(event).length, 0);\n\n const nowMs = Date.now();\n\n const batchId = generateUuid();\n const payload = {\n session_replay_segment_id: this._sessionReplaySegmentId,\n batch_id: batchId,\n sent_at_ms: nowMs,\n events,\n };\n\n const res = await this._deps.sendBatch(\n JSON.stringify(payload),\n { keepalive: options.keepalive },\n );\n\n if (res.status === \"error\") {\n if (isAnalyticsNotEnabledError(res.error)) {\n this._disable();\n return;\n }\n console.warn(\"EventTracker flush failed:\", res.error);\n return;\n }\n\n if (!res.data.ok) {\n console.warn(\"EventTracker flush failed:\", res.data.status, await res.data.text());\n }\n }\n\n private _disable() {\n this._disabled = true;\n if (this._flushTimer !== null) {\n clearInterval(this._flushTimer);\n this._flushTimer = null;\n }\n this._teardown();\n }\n\n private _tick() {\n if (this._cancelled) return;\n if (this._events.length > 0) {\n runAsynchronously(() => this._flush({ keepalive: false }));\n }\n }\n}\n"],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  const require_chunk = require('../../../../chunk-BE-pF4vm.js');
2
2
  let vitest = require("vitest");
3
3
  let _hexclave_shared_dist_utils_results = require("@hexclave/shared/dist/utils/results");
4
+ let _hexclave_shared_dist_known_errors = require("@hexclave/shared/dist/known-errors");
4
5
  let __event_tracker_js = require("./event-tracker.js");
5
6
 
6
7
  //#region src/lib/hexclave-app/apps/implementations/event-tracker.test.ts
@@ -302,7 +303,7 @@ function getSentEventTypes(sentBodies) {
302
303
  tracker.stop();
303
304
  }
304
305
  });
305
- (0, vitest.it)("silently disables when server responds with ANALYTICS_NOT_ENABLED", async () => {
306
+ (0, vitest.it)("silently disables when client interface returns ANALYTICS_NOT_ENABLED as an error", async () => {
306
307
  vitest.vi.useFakeTimers();
307
308
  document.body.innerHTML = "<button>Click me</button>";
308
309
  const warnSpy = vitest.vi.spyOn(console, "warn").mockImplementation(() => {});
@@ -311,13 +312,7 @@ function getSentEventTypes(sentBodies) {
311
312
  projectId: "internal",
312
313
  sendBatch: async (body) => {
313
314
  sentBodies.push(body);
314
- return _hexclave_shared_dist_utils_results.Result.ok(new Response(JSON.stringify({
315
- code: "ANALYTICS_NOT_ENABLED",
316
- error: "Analytics is not enabled for this project."
317
- }), {
318
- status: 400,
319
- headers: { "x-stack-known-error": "ANALYTICS_NOT_ENABLED" }
320
- }));
315
+ return _hexclave_shared_dist_utils_results.Result.error(new _hexclave_shared_dist_known_errors.KnownErrors.AnalyticsNotEnabled());
321
316
  }
322
317
  });
323
318
  try {
@@ -325,6 +320,7 @@ function getSentEventTypes(sentBodies) {
325
320
  await advancePastFlush();
326
321
  (0, vitest.expect)(sentBodies).toHaveLength(1);
327
322
  (0, vitest.expect)(warnSpy).not.toHaveBeenCalled();
323
+ (0, vitest.expect)(tracker._flushTimer).toBeNull();
328
324
  document.querySelector("button")?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
329
325
  await advancePastFlush();
330
326
  (0, vitest.expect)(sentBodies).toHaveLength(1);
@@ -1 +1 @@
1
- {"version":3,"file":"event-tracker.test.js","names":["vi","EventTracker","Result"],"sources":["../../../../../src/lib/hexclave-app/apps/implementations/event-tracker.test.ts"],"sourcesContent":["\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template\n//===========================================\n// @vitest-environment jsdom\n\nimport { Result } from \"@hexclave/shared/dist/utils/results\";\nimport { afterEach, describe, expect, it, vi } from \"vitest\";\nimport { EventTracker } from \"./event-tracker\";\n\nasync function advancePastFlush() {\n await vi.advanceTimersByTimeAsync(10_000);\n await Promise.resolve();\n}\n\nfunction getSentEventTypes(sentBodies: string[]) {\n const [body] = sentBodies;\n\n const payload = JSON.parse(body);\n if (typeof payload !== \"object\" || payload === null || !(\"events\" in payload) || !Array.isArray(payload.events)) {\n throw new Error(\"Expected analytics batch payload to include an events array.\");\n }\n\n return (payload.events as { event_type: string }[]).map((event) => event.event_type);\n}\n\ndescribe(\"EventTracker\", () => {\n afterEach(() => {\n vi.useRealTimers();\n });\n\n it(\"captures events when browser globals are exposed as accessor descriptors\", async () => {\n vi.useFakeTimers();\n document.body.innerHTML = \"<button>Open project</button>\";\n\n const screenDescriptor = Object.getOwnPropertyDescriptor(window, \"screen\");\n const historyDescriptor = Object.getOwnPropertyDescriptor(window, \"history\");\n expect(screenDescriptor?.value).toBeUndefined();\n expect(historyDescriptor?.value).toBeUndefined();\n expect(screenDescriptor?.get).toBeTypeOf(\"function\");\n expect(historyDescriptor?.get).toBeTypeOf(\"function\");\n\n const sentBodies: string[] = [];\n const tracker = new EventTracker({\n projectId: \"internal\",\n sendBatch: async (body) => {\n sentBodies.push(body);\n return Result.ok(new Response());\n },\n });\n\n try {\n tracker.start();\n document.querySelector(\"button\")?.dispatchEvent(new MouseEvent(\"click\", {\n bubbles: true,\n clientX: 12,\n clientY: 34,\n }));\n\n await advancePastFlush();\n\n // Dead-click classification marks the buffered $click in place —\n // exactly one click event either way.\n expect(getSentEventTypes(sentBodies)).toMatchInlineSnapshot(`\n [\n \"$page-view\",\n \"$click\",\n ]\n `);\n } finally {\n tracker.stop();\n }\n });\n\n it(\"emits a PostHog-style elements_chain plus scaled pointer coords for $click\", async () => {\n vi.useFakeTimers();\n document.body.innerHTML = `\n <main>\n <section class=\"card panel\">\n <button id=\"save-btn\" data-testid=\"save\" aria-label=\"Save project\">Save changes</button>\n </section>\n </main>\n `;\n\n const sentBodies: string[] = [];\n const tracker = new EventTracker({\n projectId: \"internal\",\n sendBatch: async (body) => {\n sentBodies.push(body);\n return Result.ok(new Response());\n },\n });\n\n try {\n tracker.start();\n const button = document.querySelector(\"#save-btn\");\n if (button == null) throw new Error(\"button missing\");\n button.dispatchEvent(new MouseEvent(\"click\", {\n bubbles: true,\n clientX: 100,\n clientY: 200,\n }));\n\n await advancePastFlush();\n\n const payload = JSON.parse(sentBodies[0] ?? \"{}\") as { events: { event_type: string, data: Record<string, unknown> }[] };\n const click = payload.events.find((event) => event.event_type === \"$click\");\n if (click == null) throw new Error(\"no $click event captured\");\n\n // elements_chain encodes the target leaf plus a few ancestors. Leaf is\n // first; segments are `;`-delimited. Assert against substrings rather\n // than the full string so jsdom layout quirks don't make this flaky.\n const chain = click.data.elements_chain;\n expect(typeof chain).toBe(\"string\");\n expect(chain).toContain('button');\n expect(chain).toContain('attr__id=\"save-btn\"');\n expect(chain).toContain('attr__data-testid=\"save\"');\n expect(chain).toContain('attr__aria-label=\"Save project\"');\n expect(chain).toContain('text=\"Save changes\"');\n // Ancestor section is in the chain too.\n expect(chain).toContain(\"section\");\n\n // Pre-scaled coords land in clickmap_events.pointer_*. SCALE_FACTOR=16.\n expect(click.data.x_scaled).toBe(Math.round(100 / 16));\n expect(click.data.y_scaled).toBe(Math.round(200 / 16));\n expect(click.data.client_y_scaled).toBe(Math.round(200 / 16));\n expect(click.data.scale_factor).toBe(16);\n expect(click.data.pointer_relative_x).toBeCloseTo(100 / window.innerWidth, 4);\n expect(click.data.pointer_target_fixed).toBe(0);\n\n // Legacy CSS selector still emitted for back-compat. The builder prefers\n // data-testid over id, so we assert against that anchor rather than #id.\n expect(click.data.selector).toContain('data-testid=\"save\"');\n expect(click.data.tag_name).toBe(\"button\");\n } finally {\n tracker.stop();\n }\n });\n\n it(\"ignores clicks inside the Hexclave dev tool\", async () => {\n vi.useFakeTimers();\n document.body.innerHTML = `\n <div id=\"__hexclave-dev-tool-root\">\n <button>Clickmap toolbar control</button>\n </div>\n `;\n\n const sentBodies: string[] = [];\n const tracker = new EventTracker({\n projectId: \"internal\",\n sendBatch: async (body) => {\n sentBodies.push(body);\n return Result.ok(new Response());\n },\n });\n\n try {\n tracker.start();\n document.querySelector(\"button\")?.dispatchEvent(new MouseEvent(\"click\", {\n bubbles: true,\n clientX: 100,\n clientY: 200,\n }));\n\n await advancePastFlush();\n\n expect(getSentEventTypes(sentBodies)).toMatchInlineSnapshot(`\n [\n \"$page-view\",\n ]\n `);\n } finally {\n tracker.stop();\n }\n });\n\n it(\"flags pointer_target_fixed when the target sits under a fixed-position ancestor\", async () => {\n vi.useFakeTimers();\n document.body.innerHTML = `\n <header style=\"position: fixed; top: 0\">\n <button id=\"cta\">Sign up</button>\n </header>\n `;\n\n const sentBodies: string[] = [];\n const tracker = new EventTracker({\n projectId: \"internal\",\n sendBatch: async (body) => {\n sentBodies.push(body);\n return Result.ok(new Response());\n },\n });\n\n try {\n tracker.start();\n document.querySelector(\"#cta\")?.dispatchEvent(new MouseEvent(\"click\", { bubbles: true }));\n await advancePastFlush();\n\n const payload = JSON.parse(sentBodies[0] ?? \"{}\") as { events: { event_type: string, data: Record<string, unknown> }[] };\n const click = payload.events.find((event) => event.event_type === \"$click\");\n expect(click?.data.pointer_target_fixed).toBe(1);\n } finally {\n tracker.stop();\n }\n });\n\n it(\"flags a click with no observable effect as dead on its single $click event\", async () => {\n vi.useFakeTimers();\n document.body.innerHTML = \"<button id=\\\"dead\\\">Does nothing</button>\";\n\n const sentBodies: string[] = [];\n const tracker = new EventTracker({\n projectId: \"internal\",\n sendBatch: async (body) => {\n sentBodies.push(body);\n return Result.ok(new Response());\n },\n });\n\n try {\n tracker.start();\n const clickAtMs = Date.now();\n document.querySelector(\"#dead\")?.dispatchEvent(new MouseEvent(\"click\", {\n bubbles: true,\n clientX: 10,\n clientY: 20,\n }));\n\n await advancePastFlush();\n\n const payload = JSON.parse(sentBodies[0] ?? \"{}\") as { events: { event_type: string, event_at_ms: number, data: Record<string, unknown> }[] };\n const clicks = payload.events.filter((event) => event.event_type === \"$click\");\n expect(clicks).toHaveLength(1);\n const click = clicks[0];\n\n // One event per physical click: the buffered $click is marked dead in\n // place, still timestamped at the original click rather than at\n // classification time (~3s later).\n expect(click.data.dead).toBe(1);\n expect(click.event_at_ms).toBe(clickAtMs);\n } finally {\n tracker.stop();\n }\n });\n\n it(\"does not flag a click as dead when it mutates the DOM\", async () => {\n vi.useFakeTimers();\n document.body.innerHTML = \"<button id=\\\"live\\\">Adds content</button><div id=\\\"out\\\"></div>\";\n\n const sentBodies: string[] = [];\n const tracker = new EventTracker({\n projectId: \"internal\",\n sendBatch: async (body) => {\n sentBodies.push(body);\n return Result.ok(new Response());\n },\n });\n\n try {\n tracker.start();\n const button = document.querySelector(\"#live\");\n if (button == null) throw new Error(\"button missing\");\n button.addEventListener(\"click\", () => {\n document.querySelector(\"#out\")?.appendChild(document.createElement(\"p\"));\n });\n button.dispatchEvent(new MouseEvent(\"click\", { bubbles: true }));\n // Let the MutationObserver microtask run so the mutation is recorded\n // before the dead-click sweeps start.\n await Promise.resolve();\n\n await advancePastFlush();\n\n const payload = JSON.parse(sentBodies[0] ?? \"{}\") as { events: { event_type: string, data: Record<string, unknown> }[] };\n const clicks = payload.events.filter((event) => event.event_type === \"$click\");\n expect(clicks).toHaveLength(1);\n expect(clicks[0].data.dead).toBeUndefined();\n } finally {\n tracker.stop();\n }\n });\n\n it(\"drains held clicks as alive on pagehide so navigation clicks are never lost\", async () => {\n vi.useFakeTimers();\n document.body.innerHTML = \"<a id=\\\"nav\\\" href=\\\"/pricing\\\">Pricing</a>\";\n\n const sentBodies: string[] = [];\n const tracker = new EventTracker({\n projectId: \"internal\",\n sendBatch: async (body) => {\n sentBodies.push(body);\n return Result.ok(new Response());\n },\n });\n\n try {\n tracker.start();\n const clickAtMs = Date.now();\n document.querySelector(\"#nav\")?.dispatchEvent(new MouseEvent(\"click\", { bubbles: true }));\n\n // Navigation fires pagehide well before any classification sweep — the\n // keepalive flush ships the still-unclassified click as a plain (alive)\n // $click.\n window.dispatchEvent(new Event(\"pagehide\"));\n await Promise.resolve();\n await Promise.resolve();\n\n const payload = JSON.parse(sentBodies[0] ?? \"{}\") as { events: { event_type: string, event_at_ms: number, data: Record<string, unknown> }[] };\n const clicks = payload.events.filter((event) => event.event_type === \"$click\");\n expect(clicks).toHaveLength(1);\n expect(clicks[0].data.dead).toBeUndefined();\n expect(clicks[0].event_at_ms).toBe(clickAtMs);\n } finally {\n tracker.stop();\n }\n });\n\n it(\"holds an unclassified click out of a flush and ships it on the next one\", async () => {\n vi.useFakeTimers();\n document.body.innerHTML = \"<button id=\\\"late\\\">Late click</button>\";\n\n const sentBodies: string[] = [];\n const tracker = new EventTracker({\n projectId: \"internal\",\n sendBatch: async (body) => {\n sentBodies.push(body);\n return Result.ok(new Response());\n },\n });\n\n try {\n tracker.start();\n // Click 500ms before the 10s flush tick: classification cannot finish\n // in time, so the flush must hold the click back rather than send it\n // unclassified.\n await vi.advanceTimersByTimeAsync(9_500);\n document.querySelector(\"#late\")?.dispatchEvent(new MouseEvent(\"click\", { bubbles: true }));\n await vi.advanceTimersByTimeAsync(500);\n\n expect(getSentEventTypes(sentBodies)).toMatchInlineSnapshot(`\n [\n \"$page-view\",\n ]\n `);\n\n // By the next flush the sweep has classified it (dead — nothing\n // observable happened) and it ships marked.\n await vi.advanceTimersByTimeAsync(10_000);\n const second = JSON.parse(sentBodies[1] ?? \"{}\") as { events: { event_type: string, data: Record<string, unknown> }[] };\n expect(second.events.map((event) => event.event_type)).toMatchInlineSnapshot(`\n [\n \"$click\",\n ]\n `);\n expect(second.events[0].data.dead).toBe(1);\n } finally {\n tracker.stop();\n }\n });\n\n it(\"captures client-side navigations when history is exposed as an accessor descriptor\", async () => {\n vi.useFakeTimers();\n\n const historyDescriptor = Object.getOwnPropertyDescriptor(window, \"history\");\n expect(historyDescriptor?.value).toBeUndefined();\n expect(historyDescriptor?.get).toBeTypeOf(\"function\");\n\n const sentBodies: string[] = [];\n const tracker = new EventTracker({\n projectId: \"internal\",\n sendBatch: async (body) => {\n sentBodies.push(body);\n return Result.ok(new Response());\n },\n });\n\n try {\n tracker.start();\n window.history.pushState({}, \"\", \"/projects/test-project\");\n\n await advancePastFlush();\n\n expect(getSentEventTypes(sentBodies)).toMatchInlineSnapshot(`\n [\n \"$page-view\",\n \"$page-view\",\n ]\n `);\n } finally {\n tracker.stop();\n }\n });\n\n it(\"silently disables when server responds with ANALYTICS_NOT_ENABLED\", async () => {\n vi.useFakeTimers();\n document.body.innerHTML = \"<button>Click me</button>\";\n\n const warnSpy = vi.spyOn(console, \"warn\").mockImplementation(() => {});\n const sentBodies: string[] = [];\n const tracker = new EventTracker({\n projectId: \"internal\",\n sendBatch: async (body) => {\n sentBodies.push(body);\n return Result.ok(new Response(\n JSON.stringify({ code: \"ANALYTICS_NOT_ENABLED\", error: \"Analytics is not enabled for this project.\" }),\n {\n status: 400,\n headers: { \"x-stack-known-error\": \"ANALYTICS_NOT_ENABLED\" },\n },\n ));\n },\n });\n\n try {\n tracker.start();\n\n // First flush sends the initial page-view event; server rejects it.\n await advancePastFlush();\n expect(sentBodies).toHaveLength(1);\n\n // No console.warn should have been emitted.\n expect(warnSpy).not.toHaveBeenCalled();\n\n // After disabling, new events should not accumulate or trigger further\n // flushes.\n document.querySelector(\"button\")?.dispatchEvent(new MouseEvent(\"click\", { bubbles: true }));\n await advancePastFlush();\n expect(sentBodies).toHaveLength(1);\n } finally {\n tracker.stop();\n warnSpy.mockRestore();\n }\n });\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"event-tracker.test.js","names":["vi","EventTracker","Result","KnownErrors"],"sources":["../../../../../src/lib/hexclave-app/apps/implementations/event-tracker.test.ts"],"sourcesContent":["\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template\n//===========================================\n// @vitest-environment jsdom\n\nimport { KnownErrors } from \"@hexclave/shared/dist/known-errors\";\nimport { Result } from \"@hexclave/shared/dist/utils/results\";\nimport { afterEach, describe, expect, it, vi } from \"vitest\";\nimport { EventTracker } from \"./event-tracker\";\n\nasync function advancePastFlush() {\n await vi.advanceTimersByTimeAsync(10_000);\n await Promise.resolve();\n}\n\nfunction getSentEventTypes(sentBodies: string[]) {\n const [body] = sentBodies;\n\n const payload = JSON.parse(body);\n if (typeof payload !== \"object\" || payload === null || !(\"events\" in payload) || !Array.isArray(payload.events)) {\n throw new Error(\"Expected analytics batch payload to include an events array.\");\n }\n\n return (payload.events as { event_type: string }[]).map((event) => event.event_type);\n}\n\ndescribe(\"EventTracker\", () => {\n afterEach(() => {\n vi.useRealTimers();\n });\n\n it(\"captures events when browser globals are exposed as accessor descriptors\", async () => {\n vi.useFakeTimers();\n document.body.innerHTML = \"<button>Open project</button>\";\n\n const screenDescriptor = Object.getOwnPropertyDescriptor(window, \"screen\");\n const historyDescriptor = Object.getOwnPropertyDescriptor(window, \"history\");\n expect(screenDescriptor?.value).toBeUndefined();\n expect(historyDescriptor?.value).toBeUndefined();\n expect(screenDescriptor?.get).toBeTypeOf(\"function\");\n expect(historyDescriptor?.get).toBeTypeOf(\"function\");\n\n const sentBodies: string[] = [];\n const tracker = new EventTracker({\n projectId: \"internal\",\n sendBatch: async (body) => {\n sentBodies.push(body);\n return Result.ok(new Response());\n },\n });\n\n try {\n tracker.start();\n document.querySelector(\"button\")?.dispatchEvent(new MouseEvent(\"click\", {\n bubbles: true,\n clientX: 12,\n clientY: 34,\n }));\n\n await advancePastFlush();\n\n // Dead-click classification marks the buffered $click in place —\n // exactly one click event either way.\n expect(getSentEventTypes(sentBodies)).toMatchInlineSnapshot(`\n [\n \"$page-view\",\n \"$click\",\n ]\n `);\n } finally {\n tracker.stop();\n }\n });\n\n it(\"emits a PostHog-style elements_chain plus scaled pointer coords for $click\", async () => {\n vi.useFakeTimers();\n document.body.innerHTML = `\n <main>\n <section class=\"card panel\">\n <button id=\"save-btn\" data-testid=\"save\" aria-label=\"Save project\">Save changes</button>\n </section>\n </main>\n `;\n\n const sentBodies: string[] = [];\n const tracker = new EventTracker({\n projectId: \"internal\",\n sendBatch: async (body) => {\n sentBodies.push(body);\n return Result.ok(new Response());\n },\n });\n\n try {\n tracker.start();\n const button = document.querySelector(\"#save-btn\");\n if (button == null) throw new Error(\"button missing\");\n button.dispatchEvent(new MouseEvent(\"click\", {\n bubbles: true,\n clientX: 100,\n clientY: 200,\n }));\n\n await advancePastFlush();\n\n const payload = JSON.parse(sentBodies[0] ?? \"{}\") as { events: { event_type: string, data: Record<string, unknown> }[] };\n const click = payload.events.find((event) => event.event_type === \"$click\");\n if (click == null) throw new Error(\"no $click event captured\");\n\n // elements_chain encodes the target leaf plus a few ancestors. Leaf is\n // first; segments are `;`-delimited. Assert against substrings rather\n // than the full string so jsdom layout quirks don't make this flaky.\n const chain = click.data.elements_chain;\n expect(typeof chain).toBe(\"string\");\n expect(chain).toContain('button');\n expect(chain).toContain('attr__id=\"save-btn\"');\n expect(chain).toContain('attr__data-testid=\"save\"');\n expect(chain).toContain('attr__aria-label=\"Save project\"');\n expect(chain).toContain('text=\"Save changes\"');\n // Ancestor section is in the chain too.\n expect(chain).toContain(\"section\");\n\n // Pre-scaled coords land in clickmap_events.pointer_*. SCALE_FACTOR=16.\n expect(click.data.x_scaled).toBe(Math.round(100 / 16));\n expect(click.data.y_scaled).toBe(Math.round(200 / 16));\n expect(click.data.client_y_scaled).toBe(Math.round(200 / 16));\n expect(click.data.scale_factor).toBe(16);\n expect(click.data.pointer_relative_x).toBeCloseTo(100 / window.innerWidth, 4);\n expect(click.data.pointer_target_fixed).toBe(0);\n\n // Legacy CSS selector still emitted for back-compat. The builder prefers\n // data-testid over id, so we assert against that anchor rather than #id.\n expect(click.data.selector).toContain('data-testid=\"save\"');\n expect(click.data.tag_name).toBe(\"button\");\n } finally {\n tracker.stop();\n }\n });\n\n it(\"ignores clicks inside the Hexclave dev tool\", async () => {\n vi.useFakeTimers();\n document.body.innerHTML = `\n <div id=\"__hexclave-dev-tool-root\">\n <button>Clickmap toolbar control</button>\n </div>\n `;\n\n const sentBodies: string[] = [];\n const tracker = new EventTracker({\n projectId: \"internal\",\n sendBatch: async (body) => {\n sentBodies.push(body);\n return Result.ok(new Response());\n },\n });\n\n try {\n tracker.start();\n document.querySelector(\"button\")?.dispatchEvent(new MouseEvent(\"click\", {\n bubbles: true,\n clientX: 100,\n clientY: 200,\n }));\n\n await advancePastFlush();\n\n expect(getSentEventTypes(sentBodies)).toMatchInlineSnapshot(`\n [\n \"$page-view\",\n ]\n `);\n } finally {\n tracker.stop();\n }\n });\n\n it(\"flags pointer_target_fixed when the target sits under a fixed-position ancestor\", async () => {\n vi.useFakeTimers();\n document.body.innerHTML = `\n <header style=\"position: fixed; top: 0\">\n <button id=\"cta\">Sign up</button>\n </header>\n `;\n\n const sentBodies: string[] = [];\n const tracker = new EventTracker({\n projectId: \"internal\",\n sendBatch: async (body) => {\n sentBodies.push(body);\n return Result.ok(new Response());\n },\n });\n\n try {\n tracker.start();\n document.querySelector(\"#cta\")?.dispatchEvent(new MouseEvent(\"click\", { bubbles: true }));\n await advancePastFlush();\n\n const payload = JSON.parse(sentBodies[0] ?? \"{}\") as { events: { event_type: string, data: Record<string, unknown> }[] };\n const click = payload.events.find((event) => event.event_type === \"$click\");\n expect(click?.data.pointer_target_fixed).toBe(1);\n } finally {\n tracker.stop();\n }\n });\n\n it(\"flags a click with no observable effect as dead on its single $click event\", async () => {\n vi.useFakeTimers();\n document.body.innerHTML = \"<button id=\\\"dead\\\">Does nothing</button>\";\n\n const sentBodies: string[] = [];\n const tracker = new EventTracker({\n projectId: \"internal\",\n sendBatch: async (body) => {\n sentBodies.push(body);\n return Result.ok(new Response());\n },\n });\n\n try {\n tracker.start();\n const clickAtMs = Date.now();\n document.querySelector(\"#dead\")?.dispatchEvent(new MouseEvent(\"click\", {\n bubbles: true,\n clientX: 10,\n clientY: 20,\n }));\n\n await advancePastFlush();\n\n const payload = JSON.parse(sentBodies[0] ?? \"{}\") as { events: { event_type: string, event_at_ms: number, data: Record<string, unknown> }[] };\n const clicks = payload.events.filter((event) => event.event_type === \"$click\");\n expect(clicks).toHaveLength(1);\n const click = clicks[0];\n\n // One event per physical click: the buffered $click is marked dead in\n // place, still timestamped at the original click rather than at\n // classification time (~3s later).\n expect(click.data.dead).toBe(1);\n expect(click.event_at_ms).toBe(clickAtMs);\n } finally {\n tracker.stop();\n }\n });\n\n it(\"does not flag a click as dead when it mutates the DOM\", async () => {\n vi.useFakeTimers();\n document.body.innerHTML = \"<button id=\\\"live\\\">Adds content</button><div id=\\\"out\\\"></div>\";\n\n const sentBodies: string[] = [];\n const tracker = new EventTracker({\n projectId: \"internal\",\n sendBatch: async (body) => {\n sentBodies.push(body);\n return Result.ok(new Response());\n },\n });\n\n try {\n tracker.start();\n const button = document.querySelector(\"#live\");\n if (button == null) throw new Error(\"button missing\");\n button.addEventListener(\"click\", () => {\n document.querySelector(\"#out\")?.appendChild(document.createElement(\"p\"));\n });\n button.dispatchEvent(new MouseEvent(\"click\", { bubbles: true }));\n // Let the MutationObserver microtask run so the mutation is recorded\n // before the dead-click sweeps start.\n await Promise.resolve();\n\n await advancePastFlush();\n\n const payload = JSON.parse(sentBodies[0] ?? \"{}\") as { events: { event_type: string, data: Record<string, unknown> }[] };\n const clicks = payload.events.filter((event) => event.event_type === \"$click\");\n expect(clicks).toHaveLength(1);\n expect(clicks[0].data.dead).toBeUndefined();\n } finally {\n tracker.stop();\n }\n });\n\n it(\"drains held clicks as alive on pagehide so navigation clicks are never lost\", async () => {\n vi.useFakeTimers();\n document.body.innerHTML = \"<a id=\\\"nav\\\" href=\\\"/pricing\\\">Pricing</a>\";\n\n const sentBodies: string[] = [];\n const tracker = new EventTracker({\n projectId: \"internal\",\n sendBatch: async (body) => {\n sentBodies.push(body);\n return Result.ok(new Response());\n },\n });\n\n try {\n tracker.start();\n const clickAtMs = Date.now();\n document.querySelector(\"#nav\")?.dispatchEvent(new MouseEvent(\"click\", { bubbles: true }));\n\n // Navigation fires pagehide well before any classification sweep — the\n // keepalive flush ships the still-unclassified click as a plain (alive)\n // $click.\n window.dispatchEvent(new Event(\"pagehide\"));\n await Promise.resolve();\n await Promise.resolve();\n\n const payload = JSON.parse(sentBodies[0] ?? \"{}\") as { events: { event_type: string, event_at_ms: number, data: Record<string, unknown> }[] };\n const clicks = payload.events.filter((event) => event.event_type === \"$click\");\n expect(clicks).toHaveLength(1);\n expect(clicks[0].data.dead).toBeUndefined();\n expect(clicks[0].event_at_ms).toBe(clickAtMs);\n } finally {\n tracker.stop();\n }\n });\n\n it(\"holds an unclassified click out of a flush and ships it on the next one\", async () => {\n vi.useFakeTimers();\n document.body.innerHTML = \"<button id=\\\"late\\\">Late click</button>\";\n\n const sentBodies: string[] = [];\n const tracker = new EventTracker({\n projectId: \"internal\",\n sendBatch: async (body) => {\n sentBodies.push(body);\n return Result.ok(new Response());\n },\n });\n\n try {\n tracker.start();\n // Click 500ms before the 10s flush tick: classification cannot finish\n // in time, so the flush must hold the click back rather than send it\n // unclassified.\n await vi.advanceTimersByTimeAsync(9_500);\n document.querySelector(\"#late\")?.dispatchEvent(new MouseEvent(\"click\", { bubbles: true }));\n await vi.advanceTimersByTimeAsync(500);\n\n expect(getSentEventTypes(sentBodies)).toMatchInlineSnapshot(`\n [\n \"$page-view\",\n ]\n `);\n\n // By the next flush the sweep has classified it (dead — nothing\n // observable happened) and it ships marked.\n await vi.advanceTimersByTimeAsync(10_000);\n const second = JSON.parse(sentBodies[1] ?? \"{}\") as { events: { event_type: string, data: Record<string, unknown> }[] };\n expect(second.events.map((event) => event.event_type)).toMatchInlineSnapshot(`\n [\n \"$click\",\n ]\n `);\n expect(second.events[0].data.dead).toBe(1);\n } finally {\n tracker.stop();\n }\n });\n\n it(\"captures client-side navigations when history is exposed as an accessor descriptor\", async () => {\n vi.useFakeTimers();\n\n const historyDescriptor = Object.getOwnPropertyDescriptor(window, \"history\");\n expect(historyDescriptor?.value).toBeUndefined();\n expect(historyDescriptor?.get).toBeTypeOf(\"function\");\n\n const sentBodies: string[] = [];\n const tracker = new EventTracker({\n projectId: \"internal\",\n sendBatch: async (body) => {\n sentBodies.push(body);\n return Result.ok(new Response());\n },\n });\n\n try {\n tracker.start();\n window.history.pushState({}, \"\", \"/projects/test-project\");\n\n await advancePastFlush();\n\n expect(getSentEventTypes(sentBodies)).toMatchInlineSnapshot(`\n [\n \"$page-view\",\n \"$page-view\",\n ]\n `);\n } finally {\n tracker.stop();\n }\n });\n\n it(\"silently disables when client interface returns ANALYTICS_NOT_ENABLED as an error\", async () => {\n vi.useFakeTimers();\n document.body.innerHTML = \"<button>Click me</button>\";\n\n const warnSpy = vi.spyOn(console, \"warn\").mockImplementation(() => {});\n const sentBodies: string[] = [];\n const tracker = new EventTracker({\n projectId: \"internal\",\n sendBatch: async (body) => {\n sentBodies.push(body);\n return Result.error(new KnownErrors.AnalyticsNotEnabled());\n },\n });\n\n try {\n tracker.start();\n\n await advancePastFlush();\n expect(sentBodies).toHaveLength(1);\n expect(warnSpy).not.toHaveBeenCalled();\n // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access\n expect((tracker as any)._flushTimer).toBeNull();\n\n document.querySelector(\"button\")?.dispatchEvent(new MouseEvent(\"click\", { bubbles: true }));\n await advancePastFlush();\n expect(sentBodies).toHaveLength(1);\n } finally {\n tracker.stop();\n warnSpy.mockRestore();\n }\n });\n});\n"],"mappings":"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"}
@@ -70,7 +70,7 @@ declare class _HexclaveServerAppImplIncomplete<HasTokenStore extends boolean, Pr
70
70
  protected _serverNotificationCategoryFromCrud(userId: string, crud: NotificationPreferenceCrud['Server']['Read']): NotificationCategory;
71
71
  protected _serverOAuthProviderFromCrud(crud: OAuthProviderCrud['Server']['Read']): {
72
72
  id: string;
73
- type: "google" | "github" | "microsoft" | "spotify" | "facebook" | "discord" | "gitlab" | "bitbucket" | "linkedin" | "apple" | "x" | "twitch";
73
+ type: "x" | "google" | "github" | "microsoft" | "spotify" | "facebook" | "discord" | "gitlab" | "bitbucket" | "linkedin" | "apple" | "twitch";
74
74
  userId: string;
75
75
  accountId: string;
76
76
  email: string | undefined;
@@ -75,6 +75,7 @@ type SessionRecorderDeps = {
75
75
  keepalive: boolean;
76
76
  }) => Promise<Result<Response, Error>>;
77
77
  };
78
+ declare function isAnalyticsNotEnabledError(error: unknown): boolean;
78
79
  declare class SessionRecorder {
79
80
  private _started;
80
81
  private _cancelled;
@@ -104,10 +105,11 @@ declare class SessionRecorder {
104
105
  clearBuffer(): void;
105
106
  private _persistActivity;
106
107
  private _flush;
108
+ private _disable;
107
109
  private _startRecording;
108
110
  private _stopCurrentRecording;
109
111
  private _tick;
110
112
  }
111
113
  //#endregion
112
- export { AnalyticsOptions, AnalyticsReplayOptions, SessionRecorder, SessionRecorderDeps, StoredSession, analyticsOptionsFromJson, analyticsOptionsToJson, generateUuid, getOrRotateSession, getSessionReplayOptions, makeLegacyStorageKey, makeStorageKey, safeParseStoredSession };
114
+ export { AnalyticsOptions, AnalyticsReplayOptions, SessionRecorder, SessionRecorderDeps, StoredSession, analyticsOptionsFromJson, analyticsOptionsToJson, generateUuid, getOrRotateSession, getSessionReplayOptions, isAnalyticsNotEnabledError, makeLegacyStorageKey, makeStorageKey, safeParseStoredSession };
113
115
  //# sourceMappingURL=session-replay.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"session-replay.d.ts","names":[],"sources":["../../../../../src/lib/hexclave-app/apps/implementations/session-replay.ts"],"mappings":";;;KASY,sBAAA;;AAAZ;;;;EAME,OAAA;EAMA;;;;;EAAA,aAAA;EAiBU;;;;;;EAVV,UAAA,YAAsB,MAAA;EAqBU;;AAGlC;;;;EAjBE,aAAA;AAAA;AAAA,KAGU,gBAAA;EAcmG;;AAa/G;;;EArBE,OAAA;EAqB8C;;;;EAhB9C,OAAA,GAAU,sBAAA;AAAA;AAAA,iBAGI,uBAAA,CAAwB,gBAAA,EAAkB,gBAAA,eAA+B,sBAAA;;;;;;;iBAazE,sBAAA,CAAuB,OAAA,EAAS,gBAAA,eAA+B,gBAAA;AA+C/E;;;;AAAA,iBA7BgB,wBAAA,CAAyB,IAAA,EAAM,gBAAA,eAA+B,gBAAA;AAAA,KA6BlE,aAAA;EACV,UAAA;EACA,aAAA;EACA,gBAAA;AAAA;AAAA,iBAGc,sBAAA,CAAuB,GAAA,kBAAqB,aAAA;AAAA,iBAc5C,cAAA,CAAe,SAAA;AAAA,iBAKf,oBAAA,CAAqB,SAAA;AAAA,iBAIrB,YAAA,CAAA;AAAA,iBAIA,kBAAA,CAAmB,OAAA;EAAW,GAAA;EAAa,SAAA;EAAoB,KAAA;AAAA,IAAkB,aAAA;AAAA,KAiBrF,mBAAA;EACV,SAAA;EACA,SAAA,GAAY,IAAA,UAAc,OAAA;IAAW,SAAA;EAAA,MAAyB,OAAA,CAAQ,MAAA,CAAO,QAAA,EAAU,KAAA;AAAA;AAAA,cAG5E,eAAA;EAAA,QACH,QAAA;EAAA,QACA,UAAA;EAAA,QACA,SAAA;EAAA,QACA,cAAA;EAAA,QACA,gBAAA;EAAA,QACA,WAAA;EAAA,QACA,OAAA;EAAA,QACA,YAAA;EAAA,QACA,oBAAA;EAAA,QACA,UAAA;EAAA,QACA,YAAA;EAAA,QACA,qBAAA;EAAA,QACA,eAAA;EAAA,QACA,gBAAA;EAAA,iBACS,uBAAA;EAAA,iBACA,WAAA;EAAA,iBAEA,iBAAA;EAAA,iBACA,KAAA;EAAA,iBACA,cAAA;cAEL,IAAA,EAAM,mBAAA,EAAqB,aAAA,EAAe,sBAAA;EAzBQ;;;EAoC9D,KAAA,CAAA;EAYA,IAAA,CAAA;EAWA,WAAA,CAAA;EAAA,QAKQ,gBAAA;EAAA,QASM,MAAA;EAAA,QA0DA,eAAA;EAAA,QA6DN,qBAAA;EAAA,QAcA,KAAA;AAAA"}
1
+ {"version":3,"file":"session-replay.d.ts","names":[],"sources":["../../../../../src/lib/hexclave-app/apps/implementations/session-replay.ts"],"mappings":";;;KAUY,sBAAA;;AAAZ;;;;EAME,OAAA;EAMA;;;;;EAAA,aAAA;EAiBU;;;;;;EAVV,UAAA,YAAsB,MAAA;EAqBU;;AAGlC;;;;EAjBE,aAAA;AAAA;AAAA,KAGU,gBAAA;EAcmG;;AAa/G;;;EArBE,OAAA;EAqB8C;;;;EAhB9C,OAAA,GAAU,sBAAA;AAAA;AAAA,iBAGI,uBAAA,CAAwB,gBAAA,EAAkB,gBAAA,eAA+B,sBAAA;;;;;;;iBAazE,sBAAA,CAAuB,OAAA,EAAS,gBAAA,eAA+B,gBAAA;AA+C/E;;;;AAAA,iBA7BgB,wBAAA,CAAyB,IAAA,EAAM,gBAAA,eAA+B,gBAAA;AAAA,KA6BlE,aAAA;EACV,UAAA;EACA,aAAA;EACA,gBAAA;AAAA;AAAA,iBAGc,sBAAA,CAAuB,GAAA,kBAAqB,aAAA;AAAA,iBAc5C,cAAA,CAAe,SAAA;AAAA,iBAKf,oBAAA,CAAqB,SAAA;AAAA,iBAIrB,YAAA,CAAA;AAAA,iBAIA,kBAAA,CAAmB,OAAA;EAAW,GAAA;EAAa,SAAA;EAAoB,KAAA;AAAA,IAAkB,aAAA;AAAA,KAiBrF,mBAAA;EACV,SAAA;EACA,SAAA,GAAY,IAAA,UAAc,OAAA;IAAW,SAAA;EAAA,MAAyB,OAAA,CAAQ,MAAA,CAAO,QAAA,EAAU,KAAA;AAAA;AAAA,iBAGzE,0BAAA,CAA2B,KAAA;AAAA,cAI9B,eAAA;EAAA,QACH,QAAA;EAAA,QACA,UAAA;EAAA,QACA,SAAA;EAAA,QACA,cAAA;EAAA,QACA,gBAAA;EAAA,QACA,WAAA;EAAA,QACA,OAAA;EAAA,QACA,YAAA;EAAA,QACA,oBAAA;EAAA,QACA,UAAA;EAAA,QACA,YAAA;EAAA,QACA,qBAAA;EAAA,QACA,eAAA;EAAA,QACA,gBAAA;EAAA,iBACS,uBAAA;EAAA,iBACA,WAAA;EAAA,iBAEA,iBAAA;EAAA,iBACA,KAAA;EAAA,iBACA,cAAA;cAEL,IAAA,EAAM,mBAAA,EAAqB,aAAA,EAAe,sBAAA;EA7Be;;;EAwCrE,KAAA,CAAA;EAYA,IAAA,CAAA;EAWA,WAAA,CAAA;EAAA,QAKQ,gBAAA;EAAA,QASM,MAAA;EAAA,QAkDN,QAAA;EAAA,QAUM,eAAA;EAAA,QA6DN,qBAAA;EAAA,QAcA,KAAA;AAAA"}
@@ -4,6 +4,7 @@ let _hexclave_shared_dist_utils_promises = require("@hexclave/shared/dist/utils/
4
4
  let _hexclave_shared_dist_utils_errors = require("@hexclave/shared/dist/utils/errors");
5
5
  let _hexclave_shared_dist_utils_results = require("@hexclave/shared/dist/utils/results");
6
6
  let _hexclave_shared_dist_utils_env = require("@hexclave/shared/dist/utils/env");
7
+ let _hexclave_shared_dist_known_errors = require("@hexclave/shared/dist/known-errors");
7
8
 
8
9
  //#region src/lib/hexclave-app/apps/implementations/session-replay.ts
9
10
  function getSessionReplayOptions(analyticsOptions) {
@@ -91,6 +92,9 @@ function getOrRotateSession(options) {
91
92
  localStorage.setItem(options.key, JSON.stringify(next));
92
93
  return next;
93
94
  }
95
+ function isAnalyticsNotEnabledError(error) {
96
+ return _hexclave_shared_dist_known_errors.KnownErrors.AnalyticsNotEnabled.isInstance(error);
97
+ }
94
98
  var SessionRecorder = class {
95
99
  constructor(deps, replayOptions) {
96
100
  this._started = false;
@@ -176,25 +180,27 @@ var SessionRecorder = class {
176
180
  try {
177
181
  const res = await this._deps.sendBatch(JSON.stringify(payload), { keepalive: options.keepalive });
178
182
  if (res.status === "error") {
179
- (0, _hexclave_shared_dist_utils_errors.captureWarning)("SessionRecorder.flush", res.error);
180
- return;
181
- }
182
- if (!res.data.ok) {
183
- if ((res.data.headers.get("x-hexclave-known-error") ?? res.data.headers.get("x-stack-known-error")) === "ANALYTICS_NOT_ENABLED") {
184
- this._disabled = true;
185
- if (this._flushTimer !== null) {
186
- clearInterval(this._flushTimer);
187
- this._flushTimer = null;
188
- }
189
- this._stopCurrentRecording();
183
+ if (isAnalyticsNotEnabledError(res.error)) {
184
+ this._disable();
190
185
  return;
191
186
  }
192
- (0, _hexclave_shared_dist_utils_errors.captureWarning)("SessionRecorder.flush", /* @__PURE__ */ new Error(`SessionRecorder flush failed: ${res.data.status} ${await res.data.text()}`));
187
+ (0, _hexclave_shared_dist_utils_errors.captureWarning)("SessionRecorder.flush", res.error);
188
+ return;
193
189
  }
190
+ if (!res.data.ok) (0, _hexclave_shared_dist_utils_errors.captureWarning)("SessionRecorder.flush", /* @__PURE__ */ new Error(`SessionRecorder flush failed: ${res.data.status} ${await res.data.text()}`));
194
191
  } finally {
195
192
  this._flushInProgress = false;
196
193
  }
197
194
  }
195
+ _disable() {
196
+ this._disabled = true;
197
+ this.clearBuffer();
198
+ if (this._flushTimer !== null) {
199
+ clearInterval(this._flushTimer);
200
+ this._flushTimer = null;
201
+ }
202
+ this._stopCurrentRecording();
203
+ }
198
204
  async _startRecording() {
199
205
  if (this._recording || this._cancelled) return;
200
206
  if (!this._rrwebModule) {
@@ -265,6 +271,7 @@ exports.analyticsOptionsToJson = analyticsOptionsToJson;
265
271
  exports.generateUuid = generateUuid;
266
272
  exports.getOrRotateSession = getOrRotateSession;
267
273
  exports.getSessionReplayOptions = getSessionReplayOptions;
274
+ exports.isAnalyticsNotEnabledError = isAnalyticsNotEnabledError;
268
275
  exports.makeLegacyStorageKey = makeLegacyStorageKey;
269
276
  exports.makeStorageKey = makeStorageKey;
270
277
  exports.safeParseStoredSession = safeParseStoredSession;
@@ -1 +1 @@
1
- {"version":3,"file":"session-replay.js","names":["Result"],"sources":["../../../../../src/lib/hexclave-app/apps/implementations/session-replay.ts"],"sourcesContent":["\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template\n//===========================================\nimport { isBrowserLike } from \"@hexclave/shared/dist/utils/env\";\nimport { captureWarning } from \"@hexclave/shared/dist/utils/errors\";\nimport { runAsynchronously } from \"@hexclave/shared/dist/utils/promises\";\nimport { Result } from \"@hexclave/shared/dist/utils/results\";\n\nexport type AnalyticsReplayOptions = {\n /**\n * Whether session replays are enabled.\n *\n * @default true\n */\n enabled?: boolean,\n /**\n * Whether to mask the content of all `<input>` elements.\n *\n * @default true\n */\n maskAllInputs?: boolean,\n /**\n * A CSS class name or RegExp. Elements with a matching class will be blocked\n * (replaced with a placeholder in the recording).\n *\n * @default undefined\n */\n blockClass?: string | RegExp,\n /**\n * A CSS selector string. Elements matching this selector will be blocked\n * (replaced with a placeholder in the recording).\n *\n * @default undefined\n */\n blockSelector?: string,\n};\n\nexport type AnalyticsOptions = {\n /**\n * Whether SDK-managed analytics capture is enabled.\n *\n * @default true\n */\n enabled?: boolean,\n /**\n * Options for session replay recording. Replays are enabled by default;\n * set `enabled: false` to opt out.\n */\n replays?: AnalyticsReplayOptions,\n};\n\nexport function getSessionReplayOptions(analyticsOptions: AnalyticsOptions | undefined): AnalyticsReplayOptions {\n return {\n ...analyticsOptions?.replays,\n enabled: analyticsOptions?.replays?.enabled ?? true,\n };\n}\n\n/**\n * Converts AnalyticsOptions to a JSON-safe representation.\n * RegExp blockClass values are serialized as `{ __regexp, __flags }` objects.\n * The return type is AnalyticsOptions to keep StackClientAppJson simple;\n * the actual runtime value is JSON-safe.\n */\nexport function analyticsOptionsToJson(options: AnalyticsOptions | undefined): AnalyticsOptions | undefined {\n if (!options?.replays?.blockClass) return options;\n const { blockClass, ...rest } = options.replays;\n if (!(blockClass instanceof RegExp)) return options;\n return {\n ...options,\n replays: {\n ...rest,\n // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n blockClass: { __regexp: blockClass.source, __flags: blockClass.flags } as any,\n },\n };\n}\n\n/**\n * Reconstructs AnalyticsOptions from a JSON-deserialized value.\n * Converts `{ __regexp, __flags }` objects back to RegExp instances.\n */\nexport function analyticsOptionsFromJson(json: AnalyticsOptions | undefined): AnalyticsOptions | undefined {\n if (!json?.replays?.blockClass) return json;\n const { blockClass, ...rest } = json.replays;\n if (typeof blockClass === 'object' && '__regexp' in blockClass) {\n const bc = blockClass as unknown as { __regexp: string, __flags: string };\n return {\n ...json,\n replays: {\n ...rest,\n blockClass: new RegExp(bc.__regexp, bc.__flags),\n },\n };\n }\n return json;\n}\n\n// ---------- Recording internals ----------\n\n// Hexclave rebrand: canonical localStorage prefix (colon delimiters preserved).\nconst LOCAL_STORAGE_PREFIX = \"hexclave:session-replay:v1\";\n// Hexclave rebrand: legacy prefix — dual-read only, so a recording session active\n// across an SDK upgrade is not orphaned. Never written.\nconst LEGACY_LOCAL_STORAGE_PREFIX = \"stack:session-replay:v1\";\nconst IDLE_TTL_MS = 3 * 60 * 1000;\n\nconst FLUSH_INTERVAL_MS = 5_000;\nconst MAX_EVENTS_PER_BATCH = 200;\nconst MAX_APPROX_BYTES_PER_BATCH = 512_000;\n\nexport type StoredSession = {\n session_id: string,\n created_at_ms: number,\n last_activity_ms: number,\n};\n\nexport function safeParseStoredSession(raw: string | null): StoredSession | null {\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw);\n if (typeof parsed !== \"object\" || parsed === null) return null;\n if (typeof parsed.session_id !== \"string\") return null;\n if (typeof parsed.created_at_ms !== \"number\") return null;\n if (typeof parsed.last_activity_ms !== \"number\") return null;\n return parsed as StoredSession;\n } catch {\n return null;\n }\n}\n\nexport function makeStorageKey(projectId: string) {\n return `${LOCAL_STORAGE_PREFIX}:${projectId}`;\n}\n\n// Hexclave rebrand: legacy key, dual-read only (never written).\nexport function makeLegacyStorageKey(projectId: string) {\n return `${LEGACY_LOCAL_STORAGE_PREFIX}:${projectId}`;\n}\n\nexport function generateUuid() {\n return crypto.randomUUID();\n}\n\nexport function getOrRotateSession(options: { key: string, legacyKey?: string, nowMs: number }): StoredSession {\n // Hexclave rebrand: prefer the new key; fall back to the legacy key so a\n // recording session active across an SDK upgrade is not orphaned.\n const existing = safeParseStoredSession(localStorage.getItem(options.key))\n ?? (options.legacyKey ? safeParseStoredSession(localStorage.getItem(options.legacyKey)) : null);\n if (existing && options.nowMs - existing.last_activity_ms <= IDLE_TTL_MS) {\n return existing;\n }\n const next: StoredSession = {\n session_id: generateUuid(),\n created_at_ms: options.nowMs,\n last_activity_ms: options.nowMs,\n };\n localStorage.setItem(options.key, JSON.stringify(next));\n return next;\n}\n\nexport type SessionRecorderDeps = {\n projectId: string,\n sendBatch: (body: string, options: { keepalive: boolean }) => Promise<Result<Response, Error>>,\n};\n\nexport class SessionRecorder {\n private _started = false;\n private _cancelled = false;\n private _disabled = false;\n private _stopRecording: (() => void) | null = null;\n private _detachListeners: (() => void) | null = null;\n private _flushTimer: ReturnType<typeof setInterval> | null = null;\n private _events: unknown[] = [];\n private _approxBytes = 0;\n private _lastPersistActivity = 0;\n private _recording = false;\n private _rrwebModule: typeof import(\"rrweb\") | null = null;\n private _lastBrowserSessionId: string | null = null;\n private _takingSnapshot = false;\n private _flushInProgress = false;\n private readonly _sessionReplaySegmentId: string;\n private readonly _storageKey: string;\n // Hexclave rebrand: legacy key used for dual-read fallback only.\n private readonly _legacyStorageKey: string;\n private readonly _deps: SessionRecorderDeps;\n private readonly _replayOptions: AnalyticsReplayOptions;\n\n constructor(deps: SessionRecorderDeps, replayOptions: AnalyticsReplayOptions) {\n this._deps = deps;\n this._replayOptions = replayOptions;\n this._sessionReplaySegmentId = generateUuid();\n this._storageKey = makeStorageKey(deps.projectId);\n this._legacyStorageKey = makeLegacyStorageKey(deps.projectId);\n }\n\n /**\n * Starts recording. Idempotent — calling multiple times is safe.\n */\n start() {\n if (this._started) return;\n if (!isBrowserLike()) return;\n this._started = true;\n\n // Kick off rrweb recording\n runAsynchronously(() => this._startRecording(), { noErrorLogging: true });\n\n // Periodic flush\n this._flushTimer = setInterval(() => this._tick(), FLUSH_INTERVAL_MS);\n }\n\n stop() {\n this._cancelled = true;\n if (this._flushTimer !== null) {\n clearInterval(this._flushTimer);\n this._flushTimer = null;\n }\n // Flush remaining events before cleanup\n runAsynchronously(() => this._flush({ keepalive: true }));\n this._stopCurrentRecording();\n }\n\n clearBuffer() {\n this._events = [];\n this._approxBytes = 0;\n }\n\n private _persistActivity(nowMs: number): StoredSession {\n const stored = getOrRotateSession({ key: this._storageKey, legacyKey: this._legacyStorageKey, nowMs });\n if (nowMs - this._lastPersistActivity < 5_000) return stored;\n this._lastPersistActivity = nowMs;\n const updated: StoredSession = { ...stored, last_activity_ms: nowMs };\n localStorage.setItem(this._storageKey, JSON.stringify(updated));\n return stored;\n }\n\n private async _flush(options: { keepalive: boolean }) {\n if (this._disabled) return;\n if (this._events.length === 0) return;\n // Prevent concurrent in-flight HTTP requests. When a flush is already\n // in-flight, a second batch could race on the server (both call\n // findRecentSessionReplay before either upsert commits) and create\n // duplicate SessionReplay records. Events stay in _events and will be\n // picked up by the next tick or batch-size check.\n if (this._flushInProgress) return;\n\n const nowMs = Date.now();\n const stored = getOrRotateSession({ key: this._storageKey, legacyKey: this._legacyStorageKey, nowMs });\n\n const batchId = generateUuid();\n const payload = {\n browser_session_id: stored.session_id,\n session_replay_segment_id: this._sessionReplaySegmentId,\n batch_id: batchId,\n started_at_ms: stored.created_at_ms,\n sent_at_ms: nowMs,\n events: this._events,\n };\n\n this._events = [];\n this._approxBytes = 0;\n\n this._flushInProgress = true;\n try {\n const res = await this._deps.sendBatch(\n JSON.stringify(payload),\n { keepalive: options.keepalive },\n );\n\n if (res.status === \"error\") {\n captureWarning(\"SessionRecorder.flush\", res.error);\n return;\n }\n\n if (!res.data.ok) {\n // If the server tells us analytics is not enabled for this project,\n // silently disable the recorder — no point retrying or warning the user.\n const knownError = res.data.headers.get(\"x-hexclave-known-error\") ?? res.data.headers.get(\"x-stack-known-error\");\n if (knownError === \"ANALYTICS_NOT_ENABLED\") {\n this._disabled = true;\n if (this._flushTimer !== null) {\n clearInterval(this._flushTimer);\n this._flushTimer = null;\n }\n this._stopCurrentRecording();\n return;\n }\n captureWarning(\"SessionRecorder.flush\", new Error(`SessionRecorder flush failed: ${res.data.status} ${await res.data.text()}`));\n }\n } finally {\n this._flushInProgress = false;\n }\n }\n\n private async _startRecording() {\n if (this._recording || this._cancelled) return;\n\n if (!this._rrwebModule) {\n const rrwebImport = await Result.fromPromise(import(\"rrweb\"));\n if (rrwebImport.status === \"error\") {\n console.warn(\"SessionRecorder: rrweb import failed. Is rrweb installed?\", rrwebImport.error);\n return;\n }\n this._rrwebModule = rrwebImport.data;\n }\n\n // cancelled may change during the await above\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (this._cancelled) return;\n\n this._stopRecording = this._rrwebModule.record({\n emit: (event) => {\n const nowMs = Date.now();\n const stored = this._persistActivity(nowMs);\n\n // Detect session rotation: after 3+ minutes idle, getOrRotateSession\n // creates a new session ID. We need to inject a FullSnapshot so the\n // new server-side SessionReplay record is playable.\n if (this._lastBrowserSessionId === null) {\n this._lastBrowserSessionId = stored.session_id;\n } else if (stored.session_id !== this._lastBrowserSessionId && !this._takingSnapshot) {\n this._lastBrowserSessionId = stored.session_id;\n // Inject a FullSnapshot for the new session (calls emit synchronously)\n this._takingSnapshot = true;\n try {\n this._rrwebModule!.record.takeFullSnapshot();\n } finally {\n this._takingSnapshot = false;\n }\n }\n\n this._events.push(event);\n this._approxBytes += JSON.stringify(event).length;\n if (this._events.length >= MAX_EVENTS_PER_BATCH || this._approxBytes >= MAX_APPROX_BYTES_PER_BATCH) {\n runAsynchronously(() => this._flush({ keepalive: false }));\n }\n },\n maskAllInputs: this._replayOptions.maskAllInputs ?? true,\n ...(this._replayOptions.blockClass !== undefined ? { blockClass: this._replayOptions.blockClass } : {}),\n ...(this._replayOptions.blockSelector !== undefined ? { blockSelector: this._replayOptions.blockSelector } : {}),\n }) ?? null;\n\n this._recording = true;\n\n const onPageHide = () => {\n runAsynchronously(() => this._flush({ keepalive: true }));\n };\n window.addEventListener(\"pagehide\", onPageHide);\n document.addEventListener(\"visibilitychange\", onPageHide);\n this._detachListeners = () => {\n window.removeEventListener(\"pagehide\", onPageHide);\n document.removeEventListener(\"visibilitychange\", onPageHide);\n };\n }\n\n private _stopCurrentRecording() {\n if (this._detachListeners) {\n this._detachListeners();\n this._detachListeners = null;\n }\n if (this._stopRecording) {\n this._stopRecording();\n this._stopRecording = null;\n }\n this._events = [];\n this._approxBytes = 0;\n this._recording = false;\n }\n\n private _tick() {\n if (this._cancelled) return;\n if (this._events.length > 0) {\n runAsynchronously(() => this._flush({ keepalive: false }));\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"session-replay.js","names":["KnownErrors","Result"],"sources":["../../../../../src/lib/hexclave-app/apps/implementations/session-replay.ts"],"sourcesContent":["\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template\n//===========================================\nimport { KnownErrors } from \"@hexclave/shared/dist/known-errors\";\nimport { isBrowserLike } from \"@hexclave/shared/dist/utils/env\";\nimport { captureWarning } from \"@hexclave/shared/dist/utils/errors\";\nimport { runAsynchronously } from \"@hexclave/shared/dist/utils/promises\";\nimport { Result } from \"@hexclave/shared/dist/utils/results\";\n\nexport type AnalyticsReplayOptions = {\n /**\n * Whether session replays are enabled.\n *\n * @default true\n */\n enabled?: boolean,\n /**\n * Whether to mask the content of all `<input>` elements.\n *\n * @default true\n */\n maskAllInputs?: boolean,\n /**\n * A CSS class name or RegExp. Elements with a matching class will be blocked\n * (replaced with a placeholder in the recording).\n *\n * @default undefined\n */\n blockClass?: string | RegExp,\n /**\n * A CSS selector string. Elements matching this selector will be blocked\n * (replaced with a placeholder in the recording).\n *\n * @default undefined\n */\n blockSelector?: string,\n};\n\nexport type AnalyticsOptions = {\n /**\n * Whether SDK-managed analytics capture is enabled.\n *\n * @default true\n */\n enabled?: boolean,\n /**\n * Options for session replay recording. Replays are enabled by default;\n * set `enabled: false` to opt out.\n */\n replays?: AnalyticsReplayOptions,\n};\n\nexport function getSessionReplayOptions(analyticsOptions: AnalyticsOptions | undefined): AnalyticsReplayOptions {\n return {\n ...analyticsOptions?.replays,\n enabled: analyticsOptions?.replays?.enabled ?? true,\n };\n}\n\n/**\n * Converts AnalyticsOptions to a JSON-safe representation.\n * RegExp blockClass values are serialized as `{ __regexp, __flags }` objects.\n * The return type is AnalyticsOptions to keep StackClientAppJson simple;\n * the actual runtime value is JSON-safe.\n */\nexport function analyticsOptionsToJson(options: AnalyticsOptions | undefined): AnalyticsOptions | undefined {\n if (!options?.replays?.blockClass) return options;\n const { blockClass, ...rest } = options.replays;\n if (!(blockClass instanceof RegExp)) return options;\n return {\n ...options,\n replays: {\n ...rest,\n // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n blockClass: { __regexp: blockClass.source, __flags: blockClass.flags } as any,\n },\n };\n}\n\n/**\n * Reconstructs AnalyticsOptions from a JSON-deserialized value.\n * Converts `{ __regexp, __flags }` objects back to RegExp instances.\n */\nexport function analyticsOptionsFromJson(json: AnalyticsOptions | undefined): AnalyticsOptions | undefined {\n if (!json?.replays?.blockClass) return json;\n const { blockClass, ...rest } = json.replays;\n if (typeof blockClass === 'object' && '__regexp' in blockClass) {\n const bc = blockClass as unknown as { __regexp: string, __flags: string };\n return {\n ...json,\n replays: {\n ...rest,\n blockClass: new RegExp(bc.__regexp, bc.__flags),\n },\n };\n }\n return json;\n}\n\n// ---------- Recording internals ----------\n\n// Hexclave rebrand: canonical localStorage prefix (colon delimiters preserved).\nconst LOCAL_STORAGE_PREFIX = \"hexclave:session-replay:v1\";\n// Hexclave rebrand: legacy prefix — dual-read only, so a recording session active\n// across an SDK upgrade is not orphaned. Never written.\nconst LEGACY_LOCAL_STORAGE_PREFIX = \"stack:session-replay:v1\";\nconst IDLE_TTL_MS = 3 * 60 * 1000;\n\nconst FLUSH_INTERVAL_MS = 5_000;\nconst MAX_EVENTS_PER_BATCH = 200;\nconst MAX_APPROX_BYTES_PER_BATCH = 512_000;\n\nexport type StoredSession = {\n session_id: string,\n created_at_ms: number,\n last_activity_ms: number,\n};\n\nexport function safeParseStoredSession(raw: string | null): StoredSession | null {\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw);\n if (typeof parsed !== \"object\" || parsed === null) return null;\n if (typeof parsed.session_id !== \"string\") return null;\n if (typeof parsed.created_at_ms !== \"number\") return null;\n if (typeof parsed.last_activity_ms !== \"number\") return null;\n return parsed as StoredSession;\n } catch {\n return null;\n }\n}\n\nexport function makeStorageKey(projectId: string) {\n return `${LOCAL_STORAGE_PREFIX}:${projectId}`;\n}\n\n// Hexclave rebrand: legacy key, dual-read only (never written).\nexport function makeLegacyStorageKey(projectId: string) {\n return `${LEGACY_LOCAL_STORAGE_PREFIX}:${projectId}`;\n}\n\nexport function generateUuid() {\n return crypto.randomUUID();\n}\n\nexport function getOrRotateSession(options: { key: string, legacyKey?: string, nowMs: number }): StoredSession {\n // Hexclave rebrand: prefer the new key; fall back to the legacy key so a\n // recording session active across an SDK upgrade is not orphaned.\n const existing = safeParseStoredSession(localStorage.getItem(options.key))\n ?? (options.legacyKey ? safeParseStoredSession(localStorage.getItem(options.legacyKey)) : null);\n if (existing && options.nowMs - existing.last_activity_ms <= IDLE_TTL_MS) {\n return existing;\n }\n const next: StoredSession = {\n session_id: generateUuid(),\n created_at_ms: options.nowMs,\n last_activity_ms: options.nowMs,\n };\n localStorage.setItem(options.key, JSON.stringify(next));\n return next;\n}\n\nexport type SessionRecorderDeps = {\n projectId: string,\n sendBatch: (body: string, options: { keepalive: boolean }) => Promise<Result<Response, Error>>,\n};\n\nexport function isAnalyticsNotEnabledError(error: unknown): boolean {\n return KnownErrors.AnalyticsNotEnabled.isInstance(error);\n}\n\nexport class SessionRecorder {\n private _started = false;\n private _cancelled = false;\n private _disabled = false;\n private _stopRecording: (() => void) | null = null;\n private _detachListeners: (() => void) | null = null;\n private _flushTimer: ReturnType<typeof setInterval> | null = null;\n private _events: unknown[] = [];\n private _approxBytes = 0;\n private _lastPersistActivity = 0;\n private _recording = false;\n private _rrwebModule: typeof import(\"rrweb\") | null = null;\n private _lastBrowserSessionId: string | null = null;\n private _takingSnapshot = false;\n private _flushInProgress = false;\n private readonly _sessionReplaySegmentId: string;\n private readonly _storageKey: string;\n // Hexclave rebrand: legacy key used for dual-read fallback only.\n private readonly _legacyStorageKey: string;\n private readonly _deps: SessionRecorderDeps;\n private readonly _replayOptions: AnalyticsReplayOptions;\n\n constructor(deps: SessionRecorderDeps, replayOptions: AnalyticsReplayOptions) {\n this._deps = deps;\n this._replayOptions = replayOptions;\n this._sessionReplaySegmentId = generateUuid();\n this._storageKey = makeStorageKey(deps.projectId);\n this._legacyStorageKey = makeLegacyStorageKey(deps.projectId);\n }\n\n /**\n * Starts recording. Idempotent — calling multiple times is safe.\n */\n start() {\n if (this._started) return;\n if (!isBrowserLike()) return;\n this._started = true;\n\n // Kick off rrweb recording\n runAsynchronously(() => this._startRecording(), { noErrorLogging: true });\n\n // Periodic flush\n this._flushTimer = setInterval(() => this._tick(), FLUSH_INTERVAL_MS);\n }\n\n stop() {\n this._cancelled = true;\n if (this._flushTimer !== null) {\n clearInterval(this._flushTimer);\n this._flushTimer = null;\n }\n // Flush remaining events before cleanup\n runAsynchronously(() => this._flush({ keepalive: true }));\n this._stopCurrentRecording();\n }\n\n clearBuffer() {\n this._events = [];\n this._approxBytes = 0;\n }\n\n private _persistActivity(nowMs: number): StoredSession {\n const stored = getOrRotateSession({ key: this._storageKey, legacyKey: this._legacyStorageKey, nowMs });\n if (nowMs - this._lastPersistActivity < 5_000) return stored;\n this._lastPersistActivity = nowMs;\n const updated: StoredSession = { ...stored, last_activity_ms: nowMs };\n localStorage.setItem(this._storageKey, JSON.stringify(updated));\n return stored;\n }\n\n private async _flush(options: { keepalive: boolean }) {\n if (this._disabled) return;\n if (this._events.length === 0) return;\n // Prevent concurrent in-flight HTTP requests. When a flush is already\n // in-flight, a second batch could race on the server (both call\n // findRecentSessionReplay before either upsert commits) and create\n // duplicate SessionReplay records. Events stay in _events and will be\n // picked up by the next tick or batch-size check.\n if (this._flushInProgress) return;\n\n const nowMs = Date.now();\n const stored = getOrRotateSession({ key: this._storageKey, legacyKey: this._legacyStorageKey, nowMs });\n\n const batchId = generateUuid();\n const payload = {\n browser_session_id: stored.session_id,\n session_replay_segment_id: this._sessionReplaySegmentId,\n batch_id: batchId,\n started_at_ms: stored.created_at_ms,\n sent_at_ms: nowMs,\n events: this._events,\n };\n\n this._events = [];\n this._approxBytes = 0;\n\n this._flushInProgress = true;\n try {\n const res = await this._deps.sendBatch(\n JSON.stringify(payload),\n { keepalive: options.keepalive },\n );\n\n if (res.status === \"error\") {\n if (isAnalyticsNotEnabledError(res.error)) {\n this._disable();\n return;\n }\n captureWarning(\"SessionRecorder.flush\", res.error);\n return;\n }\n\n if (!res.data.ok) {\n captureWarning(\"SessionRecorder.flush\", new Error(`SessionRecorder flush failed: ${res.data.status} ${await res.data.text()}`));\n }\n } finally {\n this._flushInProgress = false;\n }\n }\n\n private _disable() {\n this._disabled = true;\n this.clearBuffer();\n if (this._flushTimer !== null) {\n clearInterval(this._flushTimer);\n this._flushTimer = null;\n }\n this._stopCurrentRecording();\n }\n\n private async _startRecording() {\n if (this._recording || this._cancelled) return;\n\n if (!this._rrwebModule) {\n const rrwebImport = await Result.fromPromise(import(\"rrweb\"));\n if (rrwebImport.status === \"error\") {\n console.warn(\"SessionRecorder: rrweb import failed. Is rrweb installed?\", rrwebImport.error);\n return;\n }\n this._rrwebModule = rrwebImport.data;\n }\n\n // cancelled may change during the await above\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (this._cancelled) return;\n\n this._stopRecording = this._rrwebModule.record({\n emit: (event) => {\n const nowMs = Date.now();\n const stored = this._persistActivity(nowMs);\n\n // Detect session rotation: after 3+ minutes idle, getOrRotateSession\n // creates a new session ID. We need to inject a FullSnapshot so the\n // new server-side SessionReplay record is playable.\n if (this._lastBrowserSessionId === null) {\n this._lastBrowserSessionId = stored.session_id;\n } else if (stored.session_id !== this._lastBrowserSessionId && !this._takingSnapshot) {\n this._lastBrowserSessionId = stored.session_id;\n // Inject a FullSnapshot for the new session (calls emit synchronously)\n this._takingSnapshot = true;\n try {\n this._rrwebModule!.record.takeFullSnapshot();\n } finally {\n this._takingSnapshot = false;\n }\n }\n\n this._events.push(event);\n this._approxBytes += JSON.stringify(event).length;\n if (this._events.length >= MAX_EVENTS_PER_BATCH || this._approxBytes >= MAX_APPROX_BYTES_PER_BATCH) {\n runAsynchronously(() => this._flush({ keepalive: false }));\n }\n },\n maskAllInputs: this._replayOptions.maskAllInputs ?? true,\n ...(this._replayOptions.blockClass !== undefined ? { blockClass: this._replayOptions.blockClass } : {}),\n ...(this._replayOptions.blockSelector !== undefined ? { blockSelector: this._replayOptions.blockSelector } : {}),\n }) ?? null;\n\n this._recording = true;\n\n const onPageHide = () => {\n runAsynchronously(() => this._flush({ keepalive: true }));\n };\n window.addEventListener(\"pagehide\", onPageHide);\n document.addEventListener(\"visibilitychange\", onPageHide);\n this._detachListeners = () => {\n window.removeEventListener(\"pagehide\", onPageHide);\n document.removeEventListener(\"visibilitychange\", onPageHide);\n };\n }\n\n private _stopCurrentRecording() {\n if (this._detachListeners) {\n this._detachListeners();\n this._detachListeners = null;\n }\n if (this._stopRecording) {\n this._stopRecording();\n this._stopRecording = null;\n }\n this._events = [];\n this._approxBytes = 0;\n this._recording = false;\n }\n\n private _tick() {\n if (this._cancelled) return;\n if (this._events.length > 0) {\n runAsynchronously(() => this._flush({ keepalive: false }));\n }\n }\n}\n"],"mappings":"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"}