@hexclave/next 1.0.14 → 1.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/clickmap/clickmap-core.d.ts +15 -0
- package/dist/clickmap/clickmap-core.d.ts.map +1 -0
- package/dist/clickmap/clickmap-core.js +1527 -0
- package/dist/clickmap/clickmap-core.js.map +1 -0
- package/dist/clickmap/clickmap-styles.d.ts +5 -0
- package/dist/clickmap/clickmap-styles.d.ts.map +1 -0
- package/dist/clickmap/clickmap-styles.js +1095 -0
- package/dist/clickmap/clickmap-styles.js.map +1 -0
- package/dist/clickmap/index.d.ts +16 -0
- package/dist/clickmap/index.d.ts.map +1 -0
- package/dist/clickmap/index.js +74 -0
- package/dist/clickmap/index.js.map +1 -0
- package/dist/components/api-key-dialogs.d.ts +1 -1
- package/dist/components/api-key-dialogs.js +5 -5
- package/dist/components/credential-sign-in.js +3 -3
- package/dist/components/credential-sign-up.js +5 -5
- package/dist/components/elements/sidebar-layout.js +1 -1
- package/dist/components/elements/user-avatar.js +1 -1
- package/dist/components/magic-link-sign-in.js +5 -5
- package/dist/components/message-cards/known-error-message-card.d.ts +1 -1
- package/dist/components/message-cards/predefined-message-card.js +1 -1
- package/dist/components/passkey-button.js +1 -1
- package/dist/components/profile-image-editor.js +1 -1
- package/dist/components/team-icon.js +1 -1
- package/dist/components/team-switcher.js +2 -2
- package/dist/components/user-button.js +1 -1
- package/dist/components-page/account-settings/active-sessions/active-sessions-page.js +1 -1
- package/dist/components-page/account-settings/editable-text.js +1 -1
- package/dist/components-page/account-settings/email-and-auth/emails-section.js +3 -3
- package/dist/components-page/account-settings/email-and-auth/mfa-section.js +1 -1
- package/dist/components-page/account-settings/email-and-auth/password-section.js +3 -3
- package/dist/components-page/account-settings/teams/team-api-keys-section.js +1 -1
- package/dist/components-page/account-settings/teams/team-creation-page.js +3 -3
- package/dist/components-page/account-settings/teams/team-member-invitation-section.js +4 -4
- package/dist/components-page/account-settings.js +3 -3
- package/dist/components-page/auth-page.js +2 -2
- package/dist/components-page/cli-auth-confirm.js +2 -2
- package/dist/components-page/cli-auth-confirm.test.js +1 -1
- package/dist/components-page/email-verification.js +1 -1
- package/dist/components-page/forgot-password.d.ts.map +1 -1
- package/dist/components-page/forgot-password.js +6 -7
- package/dist/components-page/forgot-password.js.map +1 -1
- package/dist/components-page/hexclave-handler-client.d.ts +1 -1
- package/dist/components-page/hexclave-handler-client.js +2 -2
- package/dist/components-page/magic-link-callback.js +1 -1
- package/dist/components-page/mfa.js +7 -22
- package/dist/components-page/mfa.js.map +1 -1
- package/dist/components-page/oauth-callback.js +2 -2
- package/dist/components-page/onboarding.js +4 -4
- package/dist/components-page/password-reset.d.ts.map +1 -1
- package/dist/components-page/password-reset.js +12 -14
- package/dist/components-page/password-reset.js.map +1 -1
- package/dist/components-page/team-creation.js +5 -5
- package/dist/dev-tool/dev-tool-core.d.ts.map +1 -1
- package/dist/dev-tool/dev-tool-core.js +258 -262
- package/dist/dev-tool/dev-tool-core.js.map +1 -1
- package/dist/dev-tool/dev-tool-styles.d.ts +1 -1
- package/dist/dev-tool/dev-tool-styles.d.ts.map +1 -1
- package/dist/dev-tool/dev-tool-styles.js +13 -143
- package/dist/dev-tool/dev-tool-styles.js.map +1 -1
- package/dist/dev-tool/index.d.ts.map +1 -1
- package/dist/dev-tool/index.js +4 -11
- package/dist/dev-tool/index.js.map +1 -1
- package/dist/esm/clickmap/clickmap-core.d.ts +15 -0
- package/dist/esm/clickmap/clickmap-core.d.ts.map +1 -0
- package/dist/esm/clickmap/clickmap-core.js +1525 -0
- package/dist/esm/clickmap/clickmap-core.js.map +1 -0
- package/dist/esm/clickmap/clickmap-styles.d.ts +5 -0
- package/dist/esm/clickmap/clickmap-styles.d.ts.map +1 -0
- package/dist/esm/clickmap/clickmap-styles.js +1093 -0
- package/dist/esm/clickmap/clickmap-styles.js.map +1 -0
- package/dist/esm/clickmap/index.d.ts +16 -0
- package/dist/esm/clickmap/index.d.ts.map +1 -0
- package/dist/esm/clickmap/index.js +72 -0
- package/dist/esm/clickmap/index.js.map +1 -0
- package/dist/esm/components/api-key-dialogs.d.ts +1 -1
- package/dist/esm/components/api-key-dialogs.js +5 -5
- package/dist/esm/components/credential-sign-in.js +3 -3
- package/dist/esm/components/credential-sign-up.js +5 -5
- package/dist/esm/components/elements/sidebar-layout.js +1 -1
- package/dist/esm/components/elements/user-avatar.js +1 -1
- package/dist/esm/components/magic-link-sign-in.js +5 -5
- package/dist/esm/components/message-cards/predefined-message-card.js +1 -1
- package/dist/esm/components/passkey-button.js +1 -1
- package/dist/esm/components/profile-image-editor.js +1 -1
- package/dist/esm/components/team-icon.js +1 -1
- package/dist/esm/components/team-switcher.js +2 -2
- package/dist/esm/components/user-button.js +1 -1
- package/dist/esm/components-page/account-settings/active-sessions/active-sessions-page.js +1 -1
- package/dist/esm/components-page/account-settings/editable-text.js +1 -1
- package/dist/esm/components-page/account-settings/email-and-auth/emails-section.js +3 -3
- package/dist/esm/components-page/account-settings/email-and-auth/mfa-section.js +1 -1
- package/dist/esm/components-page/account-settings/email-and-auth/password-section.js +3 -3
- package/dist/esm/components-page/account-settings/teams/team-api-keys-section.js +1 -1
- package/dist/esm/components-page/account-settings/teams/team-creation-page.js +3 -3
- package/dist/esm/components-page/account-settings/teams/team-member-invitation-section.js +4 -4
- package/dist/esm/components-page/account-settings.d.ts +1 -1
- package/dist/esm/components-page/account-settings.js +3 -3
- package/dist/esm/components-page/auth-page.js +2 -2
- package/dist/esm/components-page/cli-auth-confirm.js +2 -2
- package/dist/esm/components-page/cli-auth-confirm.test.js +1 -1
- package/dist/esm/components-page/email-verification.js +1 -1
- package/dist/esm/components-page/forgot-password.d.ts.map +1 -1
- package/dist/esm/components-page/forgot-password.js +6 -7
- package/dist/esm/components-page/forgot-password.js.map +1 -1
- package/dist/esm/components-page/hexclave-handler-client.d.ts +1 -1
- package/dist/esm/components-page/hexclave-handler-client.js +2 -2
- package/dist/esm/components-page/magic-link-callback.js +1 -1
- package/dist/esm/components-page/mfa.js +7 -22
- package/dist/esm/components-page/mfa.js.map +1 -1
- package/dist/esm/components-page/oauth-callback.js +2 -2
- package/dist/esm/components-page/onboarding.js +4 -4
- package/dist/esm/components-page/password-reset.d.ts.map +1 -1
- package/dist/esm/components-page/password-reset.js +11 -13
- package/dist/esm/components-page/password-reset.js.map +1 -1
- package/dist/esm/components-page/team-creation.js +5 -5
- package/dist/esm/dev-tool/dev-tool-core.d.ts.map +1 -1
- package/dist/esm/dev-tool/dev-tool-core.js +35 -39
- package/dist/esm/dev-tool/dev-tool-core.js.map +1 -1
- package/dist/esm/dev-tool/dev-tool-styles.d.ts +1 -1
- package/dist/esm/dev-tool/dev-tool-styles.d.ts.map +1 -1
- package/dist/esm/dev-tool/dev-tool-styles.js +13 -143
- package/dist/esm/dev-tool/dev-tool-styles.js.map +1 -1
- package/dist/esm/dev-tool/index.d.ts.map +1 -1
- package/dist/esm/dev-tool/index.js +1 -8
- package/dist/esm/dev-tool/index.js.map +1 -1
- package/dist/esm/generated/global-css.d.ts +1 -1
- package/dist/esm/generated/global-css.js +1 -1
- package/dist/esm/generated/global-css.js.map +1 -1
- package/dist/esm/generated/quetzal-translations.d.ts +2 -2
- package/dist/esm/in-page-ui/base-styles.d.ts +5 -0
- package/dist/esm/in-page-ui/base-styles.d.ts.map +1 -0
- package/dist/esm/in-page-ui/base-styles.js +166 -0
- package/dist/esm/in-page-ui/base-styles.js.map +1 -0
- package/dist/esm/in-page-ui/dom.d.ts +15 -0
- package/dist/esm/in-page-ui/dom.d.ts.map +1 -0
- package/dist/esm/in-page-ui/dom.js +44 -0
- package/dist/esm/in-page-ui/dom.js.map +1 -0
- package/dist/esm/lib/auth.js +2 -2
- package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +6 -2
- package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.js +21 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js +4 -2
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/common.js +2 -2
- package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.d.ts +13 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.js +146 -14
- package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.test.js +221 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.test.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/server-app-impl.d.ts +2 -2
- package/dist/esm/lib/hexclave-app/apps/implementations/server-app-impl.js +1 -1
- package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.d.ts +5 -0
- package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.js.map +1 -1
- package/dist/esm/lib/hexclave-app/users/index.d.ts +1 -1
- package/dist/esm/providers/theme-provider.js +1 -1
- package/dist/esm/providers/translation-provider.js +1 -1
- package/dist/generated/global-css.d.ts +1 -1
- package/dist/generated/global-css.js +1 -1
- package/dist/generated/global-css.js.map +1 -1
- package/dist/generated/quetzal-translations.d.ts +2 -2
- package/dist/in-page-ui/base-styles.d.ts +5 -0
- package/dist/in-page-ui/base-styles.d.ts.map +1 -0
- package/dist/in-page-ui/base-styles.js +168 -0
- package/dist/in-page-ui/base-styles.js.map +1 -0
- package/dist/in-page-ui/dom.d.ts +15 -0
- package/dist/in-page-ui/dom.d.ts.map +1 -0
- package/dist/in-page-ui/dom.js +51 -0
- package/dist/in-page-ui/dom.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/integrations/convex/component/convex.config.d.ts +1 -1
- package/dist/lib/auth.js +2 -2
- package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +5 -1
- package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.js +21 -1
- package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js +4 -2
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/common.js +2 -2
- package/dist/lib/hexclave-app/apps/implementations/event-tracker.d.ts +13 -0
- package/dist/lib/hexclave-app/apps/implementations/event-tracker.d.ts.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/event-tracker.js +146 -14
- package/dist/lib/hexclave-app/apps/implementations/event-tracker.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/event-tracker.test.js +221 -0
- package/dist/lib/hexclave-app/apps/implementations/event-tracker.test.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/server-app-impl.d.ts +1 -1
- package/dist/lib/hexclave-app/apps/implementations/server-app-impl.js +1 -1
- package/dist/lib/hexclave-app/apps/interfaces/admin-app.d.ts +5 -0
- package/dist/lib/hexclave-app/apps/interfaces/admin-app.d.ts.map +1 -1
- package/dist/lib/hexclave-app/apps/interfaces/admin-app.js.map +1 -1
- package/dist/lib/hexclave-app/apps/interfaces/server-app.d.ts +1 -1
- package/dist/lib/hexclave-app/common.d.ts +1 -1
- package/dist/providers/hexclave-provider-client.d.ts +1 -1
- package/dist/providers/theme-provider.js +1 -1
- package/dist/providers/translation-provider.js +1 -1
- package/dist/{storage-CKzvsBxG.d.ts → storage-ksajV_p6.d.ts} +1 -1
- package/dist/{storage-CKzvsBxG.d.ts.map → storage-ksajV_p6.d.ts.map} +1 -1
- package/package.json +4 -4
- package/src/clickmap/clickmap-core.ts +1997 -0
- package/src/clickmap/clickmap-styles.ts +1102 -0
- package/src/clickmap/index.ts +95 -0
- package/src/components-page/forgot-password.tsx +1 -2
- package/src/components-page/mfa.tsx +12 -21
- package/src/components-page/password-reset.tsx +4 -6
- package/src/dev-tool/dev-tool-core.ts +38 -65
- package/src/dev-tool/dev-tool-styles.ts +13 -142
- package/src/dev-tool/index.ts +1 -14
- package/src/in-page-ui/base-styles.ts +171 -0
- package/src/in-page-ui/dom.ts +80 -0
- package/src/lib/hexclave-app/apps/implementations/admin-app-impl.ts +23 -1
- package/src/lib/hexclave-app/apps/implementations/client-app-impl.ts +7 -0
- package/src/lib/hexclave-app/apps/implementations/event-tracker.test.ts +287 -0
- package/src/lib/hexclave-app/apps/implementations/event-tracker.ts +226 -16
- 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 <
|
|
252
|
+
while (current && depth < 8 && current !== document.documentElement) {
|
|
174
253
|
let part = current.tagName.toLowerCase();
|
|
175
|
-
|
|
176
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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
|
|
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>,
|