@hexclave/next 1.0.13 → 1.0.15

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 (189) hide show
  1. package/dist/clickmap/clickmap-core.d.ts +15 -0
  2. package/dist/clickmap/clickmap-core.d.ts.map +1 -0
  3. package/dist/clickmap/clickmap-core.js +1527 -0
  4. package/dist/clickmap/clickmap-core.js.map +1 -0
  5. package/dist/clickmap/clickmap-styles.d.ts +5 -0
  6. package/dist/clickmap/clickmap-styles.d.ts.map +1 -0
  7. package/dist/clickmap/clickmap-styles.js +1095 -0
  8. package/dist/clickmap/clickmap-styles.js.map +1 -0
  9. package/dist/clickmap/index.d.ts +16 -0
  10. package/dist/clickmap/index.d.ts.map +1 -0
  11. package/dist/clickmap/index.js +74 -0
  12. package/dist/clickmap/index.js.map +1 -0
  13. package/dist/components/api-key-dialogs.js +2 -2
  14. package/dist/components/credential-sign-in.js +1 -1
  15. package/dist/components/credential-sign-up.js +1 -1
  16. package/dist/components/magic-link-sign-in.js +1 -1
  17. package/dist/components/message-cards/known-error-message-card.d.ts +1 -1
  18. package/dist/components/team-switcher.js +1 -1
  19. package/dist/components-page/account-settings/active-sessions/active-sessions-page.js +1 -1
  20. package/dist/components-page/account-settings/email-and-auth/emails-section.js +1 -1
  21. package/dist/components-page/account-settings/email-and-auth/mfa-section.js +1 -1
  22. package/dist/components-page/account-settings/email-and-auth/password-section.js +1 -1
  23. package/dist/components-page/account-settings/teams/team-creation-page.js +1 -1
  24. package/dist/components-page/account-settings/teams/team-member-invitation-section.js +1 -1
  25. package/dist/components-page/auth-page.js +1 -1
  26. package/dist/components-page/cli-auth-confirm.js +1 -1
  27. package/dist/components-page/cli-auth-confirm.test.js +1 -1
  28. package/dist/components-page/forgot-password.d.ts.map +1 -1
  29. package/dist/components-page/forgot-password.js +2 -3
  30. package/dist/components-page/forgot-password.js.map +1 -1
  31. package/dist/components-page/hexclave-handler-client.d.ts +1 -1
  32. package/dist/components-page/mfa.js +4 -19
  33. package/dist/components-page/mfa.js.map +1 -1
  34. package/dist/components-page/oauth-callback.js +1 -1
  35. package/dist/components-page/onboarding.js +1 -1
  36. package/dist/components-page/password-reset.d.ts.map +1 -1
  37. package/dist/components-page/password-reset.js +5 -7
  38. package/dist/components-page/password-reset.js.map +1 -1
  39. package/dist/components-page/team-creation.js +1 -1
  40. package/dist/dev-tool/dev-tool-core.d.ts.map +1 -1
  41. package/dist/dev-tool/dev-tool-core.js +258 -262
  42. package/dist/dev-tool/dev-tool-core.js.map +1 -1
  43. package/dist/dev-tool/dev-tool-styles.d.ts +1 -1
  44. package/dist/dev-tool/dev-tool-styles.d.ts.map +1 -1
  45. package/dist/dev-tool/dev-tool-styles.js +13 -143
  46. package/dist/dev-tool/dev-tool-styles.js.map +1 -1
  47. package/dist/dev-tool/index.d.ts.map +1 -1
  48. package/dist/dev-tool/index.js +5 -12
  49. package/dist/dev-tool/index.js.map +1 -1
  50. package/dist/esm/clickmap/clickmap-core.d.ts +15 -0
  51. package/dist/esm/clickmap/clickmap-core.d.ts.map +1 -0
  52. package/dist/esm/clickmap/clickmap-core.js +1525 -0
  53. package/dist/esm/clickmap/clickmap-core.js.map +1 -0
  54. package/dist/esm/clickmap/clickmap-styles.d.ts +5 -0
  55. package/dist/esm/clickmap/clickmap-styles.d.ts.map +1 -0
  56. package/dist/esm/clickmap/clickmap-styles.js +1093 -0
  57. package/dist/esm/clickmap/clickmap-styles.js.map +1 -0
  58. package/dist/esm/clickmap/index.d.ts +16 -0
  59. package/dist/esm/clickmap/index.d.ts.map +1 -0
  60. package/dist/esm/clickmap/index.js +72 -0
  61. package/dist/esm/clickmap/index.js.map +1 -0
  62. package/dist/esm/components/api-key-dialogs.js +2 -2
  63. package/dist/esm/components/credential-sign-in.js +1 -1
  64. package/dist/esm/components/credential-sign-up.js +1 -1
  65. package/dist/esm/components/magic-link-sign-in.js +1 -1
  66. package/dist/esm/components/team-switcher.js +1 -1
  67. package/dist/esm/components-page/account-settings/active-sessions/active-sessions-page.js +1 -1
  68. package/dist/esm/components-page/account-settings/email-and-auth/emails-section.js +1 -1
  69. package/dist/esm/components-page/account-settings/email-and-auth/mfa-section.js +1 -1
  70. package/dist/esm/components-page/account-settings/email-and-auth/password-section.js +1 -1
  71. package/dist/esm/components-page/account-settings/teams/team-creation-page.js +1 -1
  72. package/dist/esm/components-page/account-settings/teams/team-member-invitation-section.js +1 -1
  73. package/dist/esm/components-page/auth-page.js +1 -1
  74. package/dist/esm/components-page/cli-auth-confirm.js +1 -1
  75. package/dist/esm/components-page/cli-auth-confirm.test.js +1 -1
  76. package/dist/esm/components-page/forgot-password.d.ts.map +1 -1
  77. package/dist/esm/components-page/forgot-password.js +2 -3
  78. package/dist/esm/components-page/forgot-password.js.map +1 -1
  79. package/dist/esm/components-page/hexclave-handler-client.d.ts +1 -1
  80. package/dist/esm/components-page/mfa.js +4 -19
  81. package/dist/esm/components-page/mfa.js.map +1 -1
  82. package/dist/esm/components-page/oauth-callback.js +1 -1
  83. package/dist/esm/components-page/onboarding.js +1 -1
  84. package/dist/esm/components-page/password-reset.d.ts.map +1 -1
  85. package/dist/esm/components-page/password-reset.js +5 -7
  86. package/dist/esm/components-page/password-reset.js.map +1 -1
  87. package/dist/esm/components-page/team-creation.js +1 -1
  88. package/dist/esm/dev-tool/dev-tool-core.d.ts.map +1 -1
  89. package/dist/esm/dev-tool/dev-tool-core.js +35 -39
  90. package/dist/esm/dev-tool/dev-tool-core.js.map +1 -1
  91. package/dist/esm/dev-tool/dev-tool-styles.d.ts +1 -1
  92. package/dist/esm/dev-tool/dev-tool-styles.d.ts.map +1 -1
  93. package/dist/esm/dev-tool/dev-tool-styles.js +13 -143
  94. package/dist/esm/dev-tool/dev-tool-styles.js.map +1 -1
  95. package/dist/esm/dev-tool/index.d.ts.map +1 -1
  96. package/dist/esm/dev-tool/index.js +2 -9
  97. package/dist/esm/dev-tool/index.js.map +1 -1
  98. package/dist/esm/generated/global-css.d.ts +1 -1
  99. package/dist/esm/generated/global-css.js +1 -1
  100. package/dist/esm/generated/global-css.js.map +1 -1
  101. package/dist/esm/generated/quetzal-translations.d.ts +2 -2
  102. package/dist/esm/in-page-ui/base-styles.d.ts +5 -0
  103. package/dist/esm/in-page-ui/base-styles.d.ts.map +1 -0
  104. package/dist/esm/in-page-ui/base-styles.js +166 -0
  105. package/dist/esm/in-page-ui/base-styles.js.map +1 -0
  106. package/dist/esm/in-page-ui/dom.d.ts +15 -0
  107. package/dist/esm/in-page-ui/dom.d.ts.map +1 -0
  108. package/dist/esm/in-page-ui/dom.js +44 -0
  109. package/dist/esm/in-page-ui/dom.js.map +1 -0
  110. package/dist/esm/lib/auth.js +1 -1
  111. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +5 -1
  112. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
  113. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.js +20 -0
  114. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.js.map +1 -1
  115. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
  116. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js +4 -2
  117. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
  118. package/dist/esm/lib/hexclave-app/apps/implementations/common.js +2 -2
  119. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.d.ts +13 -0
  120. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.d.ts.map +1 -1
  121. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.js +146 -14
  122. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.js.map +1 -1
  123. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.test.js +221 -0
  124. package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.test.js.map +1 -1
  125. package/dist/esm/lib/hexclave-app/apps/implementations/server-app-impl.d.ts +1 -1
  126. package/dist/esm/lib/hexclave-app/apps/implementations/server-app-impl.js +1 -1
  127. package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.js +1 -1
  128. package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.d.ts +5 -0
  129. package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.d.ts.map +1 -1
  130. package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.js.map +1 -1
  131. package/dist/esm/providers/theme-provider.js +1 -1
  132. package/dist/generated/global-css.d.ts +1 -1
  133. package/dist/generated/global-css.js +1 -1
  134. package/dist/generated/global-css.js.map +1 -1
  135. package/dist/generated/quetzal-translations.d.ts +2 -2
  136. package/dist/in-page-ui/base-styles.d.ts +5 -0
  137. package/dist/in-page-ui/base-styles.d.ts.map +1 -0
  138. package/dist/in-page-ui/base-styles.js +168 -0
  139. package/dist/in-page-ui/base-styles.js.map +1 -0
  140. package/dist/in-page-ui/dom.d.ts +15 -0
  141. package/dist/in-page-ui/dom.d.ts.map +1 -0
  142. package/dist/in-page-ui/dom.js +51 -0
  143. package/dist/in-page-ui/dom.js.map +1 -0
  144. package/dist/index.d.ts +1 -1
  145. package/dist/integrations/convex/component/convex.config.d.ts +1 -1
  146. package/dist/lib/auth.js +1 -1
  147. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +5 -1
  148. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
  149. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.js +20 -0
  150. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.js.map +1 -1
  151. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
  152. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js +4 -2
  153. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
  154. package/dist/lib/hexclave-app/apps/implementations/common.js +2 -2
  155. package/dist/lib/hexclave-app/apps/implementations/event-tracker.d.ts +13 -0
  156. package/dist/lib/hexclave-app/apps/implementations/event-tracker.d.ts.map +1 -1
  157. package/dist/lib/hexclave-app/apps/implementations/event-tracker.js +146 -14
  158. package/dist/lib/hexclave-app/apps/implementations/event-tracker.js.map +1 -1
  159. package/dist/lib/hexclave-app/apps/implementations/event-tracker.test.js +221 -0
  160. package/dist/lib/hexclave-app/apps/implementations/event-tracker.test.js.map +1 -1
  161. package/dist/lib/hexclave-app/apps/implementations/server-app-impl.d.ts +1 -1
  162. package/dist/lib/hexclave-app/apps/implementations/server-app-impl.js +1 -1
  163. package/dist/lib/hexclave-app/apps/implementations/session-replay.js +1 -1
  164. package/dist/lib/hexclave-app/apps/interfaces/admin-app.d.ts +5 -0
  165. package/dist/lib/hexclave-app/apps/interfaces/admin-app.d.ts.map +1 -1
  166. package/dist/lib/hexclave-app/apps/interfaces/admin-app.js.map +1 -1
  167. package/dist/lib/hexclave-app/apps/interfaces/server-app.d.ts +1 -1
  168. package/dist/lib/hexclave-app/common.d.ts +1 -1
  169. package/dist/providers/hexclave-provider-client.d.ts +1 -1
  170. package/dist/providers/theme-provider.js +1 -1
  171. package/dist/{storage-CKzvsBxG.d.ts → storage-ksajV_p6.d.ts} +1 -1
  172. package/dist/{storage-CKzvsBxG.d.ts.map → storage-ksajV_p6.d.ts.map} +1 -1
  173. package/package.json +4 -4
  174. package/src/clickmap/clickmap-core.ts +1997 -0
  175. package/src/clickmap/clickmap-styles.ts +1102 -0
  176. package/src/clickmap/index.ts +95 -0
  177. package/src/components-page/forgot-password.tsx +1 -2
  178. package/src/components-page/mfa.tsx +12 -21
  179. package/src/components-page/password-reset.tsx +4 -6
  180. package/src/dev-tool/dev-tool-core.ts +38 -65
  181. package/src/dev-tool/dev-tool-styles.ts +13 -142
  182. package/src/dev-tool/index.ts +1 -14
  183. package/src/in-page-ui/base-styles.ts +171 -0
  184. package/src/in-page-ui/dom.ts +80 -0
  185. package/src/lib/hexclave-app/apps/implementations/admin-app-impl.ts +23 -1
  186. package/src/lib/hexclave-app/apps/implementations/client-app-impl.ts +7 -0
  187. package/src/lib/hexclave-app/apps/implementations/event-tracker.test.ts +287 -0
  188. package/src/lib/hexclave-app/apps/implementations/event-tracker.ts +226 -16
  189. package/src/lib/hexclave-app/apps/interfaces/admin-app.ts +3 -0
@@ -3,6 +3,9 @@
3
3
  // THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template
4
4
  //===========================================
5
5
  import { isBrowserLike } from "@hexclave/shared/dist/utils/env";
6
+ import { CLICKMAP_ROOT_ID, DEV_TOOL_ROOT_ID } from "@hexclave/shared/dist/utils/dev-tool";
7
+ import { cssEscapeIdent } from "@hexclave/shared/dist/utils/dom";
8
+ import { buildElementsChain, ELEMENTS_CHAIN_MAX_DEPTH } from "@hexclave/shared/dist/utils/elements-chain";
6
9
  import { runAsynchronously } from "@hexclave/shared/dist/utils/promises";
7
10
  import { Result } from "@hexclave/shared/dist/utils/results";
8
11
  import { generateUuid } from "./session-replay";
@@ -31,6 +34,70 @@ function hasHistoryMethods(value: unknown): value is { pushState: History["pushS
31
34
  return typeof value.pushState === "function" && typeof value.replaceState === "function";
32
35
  }
33
36
 
37
+ // Pixel quantization factor for x/y/viewport in stored click events. Matches the
38
+ // SCALE_FACTOR used by the ClickHouse clickmap_events MV — keep them in sync.
39
+ const CLICKMAP_SCALE_FACTOR = 16;
40
+
41
+ // Dead-click detection (PostHog-style). Whether an element has a click handler
42
+ // is unknowable from page script, so a click is classified by its observable
43
+ // consequences instead: it is "alive" if the page scrolled, the text selection
44
+ // changed, or the tab visibility changed (a new tab opened) almost
45
+ // immediately, or if the DOM mutated within a couple of seconds — and "dead"
46
+ // if none of that happened by the absolute timeout.
47
+ //
48
+ // The $click event is buffered immediately like any other event (so
49
+ // event_at_ms, ordering, and every query are untouched) and the sweep sets
50
+ // data.dead=1 on it in place if nothing observable happened. _flush holds
51
+ // back clicks that are still unclassified — classification always finishes
52
+ // well within one FLUSH_INTERVAL_MS, so a held click rides the next flush at
53
+ // the latest. A keepalive flush (pagehide/stop) sends them unmarked: a click
54
+ // still pending when the page unloads led to that navigation, alive by
55
+ // definition.
56
+ //
57
+ // NOTE — blocker for any future real-time / "live clicks" view: a click that
58
+ // is still unclassified when its natural flush fires arrives up to one extra
59
+ // FLUSH_INTERVAL_MS late. A surface showing clicks as they happen must either
60
+ // accept that lag or emit a provisional $click plus a later dead-click
61
+ // reconciliation event.
62
+ const DEAD_CLICK_SCROLL_THRESHOLD_MS = 100;
63
+ const DEAD_CLICK_SELECTION_CHANGED_THRESHOLD_MS = 100;
64
+ const DEAD_CLICK_VISIBILITY_CHANGE_THRESHOLD_MS = 100;
65
+ const DEAD_CLICK_MUTATION_THRESHOLD_MS = 2_500;
66
+ // 1.1x the mutation threshold, mirroring posthog-js: every signal window has
67
+ // closed before a click is declared dead.
68
+ const DEAD_CLICK_ABSOLUTE_TIMEOUT_MS = 2_750;
69
+ const DEAD_CLICK_CHECK_INTERVAL_MS = 1_000;
70
+ // Backstop against click storms (e.g. rage clicks on a dead element): past the
71
+ // cap, clicks are simply not classified rather than not recorded.
72
+ const DEAD_CLICK_MAX_PENDING = 50;
73
+
74
+ function isPointerTargetFixed(element: Element): boolean {
75
+ let current: Element | null = element;
76
+ let depth = 0;
77
+ while (current != null && depth < ELEMENTS_CHAIN_MAX_DEPTH * 2) {
78
+ const style = window.getComputedStyle(current);
79
+ if (style.position === "fixed" || style.position === "sticky") {
80
+ return true;
81
+ }
82
+ current = current.parentElement;
83
+ depth += 1;
84
+ }
85
+ return false;
86
+ }
87
+
88
+ // Clicks on Hexclave's own in-page UI (the dev tool and the standalone
89
+ // clickmap overlay) must never be ingested as analytics events.
90
+ function isInsideHexclaveUi(element: Element): boolean {
91
+ return element.closest(`#${cssEscapeIdent(DEV_TOOL_ROOT_ID)}, #${cssEscapeIdent(CLICKMAP_ROOT_ID)}`) != null;
92
+ }
93
+
94
+ // Mutation-record targets can be text/comment nodes; resolve to the nearest
95
+ // element before asking whether the mutation came from Hexclave's own UI.
96
+ function isInsideHexclaveUiNode(node: Node | null): boolean {
97
+ const element = node instanceof Element ? node : node?.parentElement ?? null;
98
+ return element != null && isInsideHexclaveUi(element);
99
+ }
100
+
34
101
  export type EventTrackerDeps = {
35
102
  projectId: string,
36
103
  sendBatch: (body: string, options: { keepalive: boolean }) => Promise<Result<Response, Error>>,
@@ -56,6 +123,16 @@ export class EventTracker {
56
123
  private _originalPushState: History["pushState"] | null = null;
57
124
  private _originalReplaceState: History["replaceState"] | null = null;
58
125
 
126
+ private _deadClickTimer: ReturnType<typeof setInterval> | null = null;
127
+ private _deadClickMutationObserver: MutationObserver | null = null;
128
+ // Buffered $click events still awaiting dead-click classification. Always a
129
+ // subset of _events — _flush holds these back until the sweep resolves them.
130
+ private _unclassifiedClicks = new Set<TrackedEvent>();
131
+ private _lastMutationAtMs: number | null = null;
132
+ private _lastScrollAtMs: number | null = null;
133
+ private _lastSelectionChangedAtMs: number | null = null;
134
+ private _lastVisibilityChangeAtMs: number | null = null;
135
+
59
136
  constructor(deps: EventTrackerDeps) {
60
137
  this._deps = deps;
61
138
  this._sessionReplaySegmentId = generateUuid();
@@ -77,6 +154,7 @@ export class EventTracker {
77
154
 
78
155
  this._setupPageViewCapture();
79
156
  this._setupClickCapture();
157
+ this._setupDeadClickDetection();
80
158
  this._setupPageHideListeners();
81
159
 
82
160
  this._flushTimer = setInterval(() => this._tick(), FLUSH_INTERVAL_MS);
@@ -95,6 +173,7 @@ export class EventTracker {
95
173
  clearBuffer() {
96
174
  this._events = [];
97
175
  this._approxBytes = 0;
176
+ this._unclassifiedClicks.clear();
98
177
  }
99
178
 
100
179
  private _pushEvent(event: TrackedEvent) {
@@ -170,21 +249,40 @@ export class EventTracker {
170
249
  let current: Element | null = element;
171
250
  let depth = 0;
172
251
 
173
- while (current && depth < 5) {
252
+ while (current && depth < 8 && current !== document.documentElement) {
174
253
  let part = current.tagName.toLowerCase();
175
- if (current.id) {
176
- part += `#${current.id}`;
254
+ let testIdAttr = "data-testid";
255
+ let testId = current.getAttribute("data-testid");
256
+ if (testId == null) {
257
+ testIdAttr = "data-test-id";
258
+ testId = current.getAttribute("data-test-id");
259
+ }
260
+ if (testId != null && testId.trim() !== "") {
261
+ part += `[${testIdAttr}="${testId.replace(/"/g, '\\"')}"]`;
262
+ parts.unshift(part);
263
+ break;
264
+ }
265
+ if (current.id !== "") {
266
+ part += `#${cssEscapeIdent(current.id)}`;
177
267
  parts.unshift(part);
178
268
  break;
179
269
  }
180
270
  if (current.className && typeof current.className === "string") {
181
- const classes = current.className.trim().split(/\s+/).filter(Boolean);
271
+ const classes = current.className.trim().split(/\s+/).filter(Boolean).slice(0, 4);
182
272
  if (classes.length > 0) {
183
- part += `.${classes.join(".")}`;
273
+ part += `.${classes.map(cssEscapeIdent).join(".")}`;
274
+ }
275
+ }
276
+ const parent: Element | null = current.parentElement;
277
+ if (parent != null) {
278
+ const tagName = current.tagName;
279
+ const siblings = Array.from(parent.children).filter((child) => child.tagName === tagName);
280
+ if (siblings.length > 1) {
281
+ part += `:nth-of-type(${siblings.indexOf(current) + 1})`;
184
282
  }
185
283
  }
186
284
  parts.unshift(part);
187
- current = current.parentElement;
285
+ current = parent;
188
286
  depth++;
189
287
  }
190
288
 
@@ -205,8 +303,18 @@ export class EventTracker {
205
303
  private readonly _onClickCapture = (event: MouseEvent) => {
206
304
  const target = event.target;
207
305
  if (!(target instanceof Element)) return;
208
-
209
- this._pushEvent({
306
+ if (isInsideHexclaveUi(target)) return;
307
+
308
+ const viewportWidth = window.innerWidth;
309
+ const viewportHeight = window.innerHeight;
310
+ const pointerTargetFixed = isPointerTargetFixed(target);
311
+ // Pre-scale at ingest so old + new rows land in identical buckets in CH.
312
+ const xScaled = Math.round(event.pageX / CLICKMAP_SCALE_FACTOR);
313
+ const yScaled = Math.round(event.pageY / CLICKMAP_SCALE_FACTOR);
314
+ const clientYScaled = Math.round(event.clientY / CLICKMAP_SCALE_FACTOR);
315
+ const relativeX = viewportWidth > 0 ? event.clientX / viewportWidth : 0;
316
+
317
+ const clickEvent: TrackedEvent = {
210
318
  event_type: "$click",
211
319
  event_at_ms: Date.now(),
212
320
  data: {
@@ -214,20 +322,111 @@ export class EventTracker {
214
322
  text: target.textContent.trim().substring(0, 200),
215
323
  href: this._findNearestAnchorHref(target),
216
324
  selector: this._buildSelector(target),
325
+ elements_chain: buildElementsChain(target),
326
+ pointer_target_fixed: pointerTargetFixed ? 1 : 0,
327
+ url: window.location.href,
328
+ path: window.location.pathname,
329
+ title: document.title,
217
330
  x: event.clientX,
218
331
  y: event.clientY,
219
332
  page_x: event.pageX,
220
333
  page_y: event.pageY,
221
- viewport_width: window.innerWidth,
222
- viewport_height: window.innerHeight,
334
+ x_scaled: xScaled,
335
+ y_scaled: yScaled,
336
+ client_y_scaled: clientYScaled,
337
+ pointer_relative_x: relativeX,
338
+ viewport_width: viewportWidth,
339
+ viewport_height: viewportHeight,
340
+ scale_factor: CLICKMAP_SCALE_FACTOR,
223
341
  },
224
- });
342
+ };
343
+
344
+ // Register for dead-click classification before buffering, so a
345
+ // size-triggered flush from this very push already holds the click back.
346
+ if (this._deadClickTimer !== null && this._unclassifiedClicks.size < DEAD_CLICK_MAX_PENDING) {
347
+ this._unclassifiedClicks.add(clickEvent);
348
+ }
349
+ this._pushEvent(clickEvent);
225
350
  };
226
351
 
227
352
  private _setupClickCapture() {
228
353
  document.addEventListener("click", this._onClickCapture, { capture: true });
229
354
  }
230
355
 
356
+ private readonly _onDeadClickScroll = () => {
357
+ this._lastScrollAtMs = Date.now();
358
+ };
359
+
360
+ private readonly _onDeadClickSelectionChange = () => {
361
+ this._lastSelectionChangedAtMs = Date.now();
362
+ };
363
+
364
+ private readonly _onDeadClickVisibilityChange = () => {
365
+ this._lastVisibilityChangeAtMs = Date.now();
366
+ };
367
+
368
+ private _setupDeadClickDetection() {
369
+ if (typeof MutationObserver !== "function") return;
370
+
371
+ this._deadClickMutationObserver = new MutationObserver((mutations) => {
372
+ // The dev tool and the clickmap overlay rewrite their own DOM constantly
373
+ // while open; their mutations must not mark host-page clicks as alive.
374
+ if (mutations.every((mutation) => isInsideHexclaveUiNode(mutation.target))) {
375
+ return;
376
+ }
377
+ this._lastMutationAtMs = Date.now();
378
+ });
379
+ this._deadClickMutationObserver.observe(document.documentElement, {
380
+ childList: true,
381
+ attributes: true,
382
+ characterData: true,
383
+ subtree: true,
384
+ });
385
+
386
+ // Capture phase so scrolls inside nested scroll containers count, not just
387
+ // the document itself (scroll events don't bubble).
388
+ document.addEventListener("scroll", this._onDeadClickScroll, { capture: true, passive: true });
389
+ document.addEventListener("selectionchange", this._onDeadClickSelectionChange);
390
+ document.addEventListener("visibilitychange", this._onDeadClickVisibilityChange);
391
+
392
+ this._deadClickTimer = setInterval(() => this._checkDeadClicks(), DEAD_CLICK_CHECK_INTERVAL_MS);
393
+ }
394
+
395
+ private _checkDeadClicks() {
396
+ const nowMs = Date.now();
397
+ for (const click of this._unclassifiedClicks) {
398
+ const signalWithin = (signalAtMs: number | null, thresholdMs: number) =>
399
+ signalAtMs != null && signalAtMs >= click.event_at_ms && signalAtMs - click.event_at_ms < thresholdMs;
400
+
401
+ const isAlive = signalWithin(this._lastScrollAtMs, DEAD_CLICK_SCROLL_THRESHOLD_MS)
402
+ || signalWithin(this._lastSelectionChangedAtMs, DEAD_CLICK_SELECTION_CHANGED_THRESHOLD_MS)
403
+ || signalWithin(this._lastVisibilityChangeAtMs, DEAD_CLICK_VISIBILITY_CHANGE_THRESHOLD_MS)
404
+ || signalWithin(this._lastMutationAtMs, DEAD_CLICK_MUTATION_THRESHOLD_MS);
405
+ if (isAlive) {
406
+ this._unclassifiedClicks.delete(click);
407
+ } else if (nowMs - click.event_at_ms >= DEAD_CLICK_ABSOLUTE_TIMEOUT_MS) {
408
+ // The already-buffered event is marked in place — no second event.
409
+ click.data.dead = 1;
410
+ this._unclassifiedClicks.delete(click);
411
+ }
412
+ }
413
+ }
414
+
415
+ private _teardownDeadClickDetection() {
416
+ if (this._deadClickTimer !== null) {
417
+ clearInterval(this._deadClickTimer);
418
+ this._deadClickTimer = null;
419
+ }
420
+ if (this._deadClickMutationObserver !== null) {
421
+ this._deadClickMutationObserver.disconnect();
422
+ this._deadClickMutationObserver = null;
423
+ }
424
+ document.removeEventListener("scroll", this._onDeadClickScroll, { capture: true });
425
+ document.removeEventListener("selectionchange", this._onDeadClickSelectionChange);
426
+ document.removeEventListener("visibilitychange", this._onDeadClickVisibilityChange);
427
+ this._unclassifiedClicks.clear();
428
+ }
429
+
231
430
  private readonly _onPageHide = () => {
232
431
  runAsynchronously(() => this._flush({ keepalive: true }));
233
432
  };
@@ -262,13 +461,27 @@ export class EventTracker {
262
461
 
263
462
  window.removeEventListener("popstate", this._onPopState);
264
463
  document.removeEventListener("click", this._onClickCapture, { capture: true });
464
+ this._teardownDeadClickDetection();
265
465
 
266
466
  this._events = [];
267
467
  this._approxBytes = 0;
268
468
  }
269
469
 
270
470
  private async _flush(options: { keepalive: boolean }) {
271
- if (this._events.length === 0) return;
471
+ // A keepalive flush means the page is unloading — a click still awaiting
472
+ // dead-click classification led to that unload, so it is alive by
473
+ // definition and ships unmarked.
474
+ if (options.keepalive) {
475
+ this._unclassifiedClicks.clear();
476
+ }
477
+
478
+ // Clicks still awaiting classification stay buffered so the sweep can
479
+ // mark them dead in place; classification finishes well within one flush
480
+ // interval, so they ride the next flush at the latest.
481
+ const events = this._events.filter((event) => !this._unclassifiedClicks.has(event));
482
+ if (events.length === 0) return;
483
+ this._events = this._events.filter((event) => this._unclassifiedClicks.has(event));
484
+ this._approxBytes = this._events.reduce((total, event) => total + JSON.stringify(event).length, 0);
272
485
 
273
486
  const nowMs = Date.now();
274
487
 
@@ -277,12 +490,9 @@ export class EventTracker {
277
490
  session_replay_segment_id: this._sessionReplaySegmentId,
278
491
  batch_id: batchId,
279
492
  sent_at_ms: nowMs,
280
- events: this._events,
493
+ events,
281
494
  };
282
495
 
283
- this._events = [];
284
- this._approxBytes = 0;
285
-
286
496
  const res = await this._deps.sendBatch(
287
497
  JSON.stringify(payload),
288
498
  { keepalive: options.keepalive },
@@ -2,6 +2,7 @@
2
2
  //===========================================
3
3
  // THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template
4
4
  //===========================================
5
+ import type { AnalyticsClickmapOptions, AnalyticsClickmapResponse, AnalyticsClickmapTokenResponse } from "@hexclave/shared/dist/interface/admin-metrics";
5
6
  import { AnalyticsQueryOptions, AnalyticsQueryResponse } from "@hexclave/shared/dist/interface/crud/analytics";
6
7
  import type { AdminGetSessionReplayChunkEventsResponse, AdminGetSessionReplayAllEventsResponse } from "@hexclave/shared/dist/interface/crud/session-replays";
7
8
  import type { Transaction, TransactionType } from "@hexclave/shared/dist/interface/crud/transactions";
@@ -159,6 +160,8 @@ export type StackAdminApp<HasTokenStore extends boolean = boolean, ProjectId ext
159
160
  endAction?: "now" | "at-period-end",
160
161
  }): Promise<{ refundTransactionId: string }>,
161
162
  queryAnalytics(options: AnalyticsQueryOptions): Promise<AnalyticsQueryResponse>,
163
+ getAnalyticsClickmap(options: AnalyticsClickmapOptions): Promise<AnalyticsClickmapResponse>,
164
+ createAnalyticsClickmapToken(options: { origin: string }): Promise<AnalyticsClickmapTokenResponse>,
162
165
 
163
166
  listSessionReplays(options?: ListSessionReplaysOptions): Promise<ListSessionReplaysResult>,
164
167
  getSessionReplay(sessionReplayId: string): Promise<AdminSessionReplay>,