@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.
- 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.js +2 -2
- package/dist/components/credential-sign-in.js +1 -1
- package/dist/components/credential-sign-up.js +1 -1
- package/dist/components/magic-link-sign-in.js +1 -1
- package/dist/components/message-cards/known-error-message-card.d.ts +1 -1
- package/dist/components/team-switcher.js +1 -1
- package/dist/components-page/account-settings/active-sessions/active-sessions-page.js +1 -1
- package/dist/components-page/account-settings/email-and-auth/emails-section.js +1 -1
- 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 +1 -1
- package/dist/components-page/account-settings/teams/team-creation-page.js +1 -1
- package/dist/components-page/account-settings/teams/team-member-invitation-section.js +1 -1
- package/dist/components-page/auth-page.js +1 -1
- package/dist/components-page/cli-auth-confirm.js +1 -1
- package/dist/components-page/cli-auth-confirm.test.js +1 -1
- package/dist/components-page/forgot-password.d.ts.map +1 -1
- package/dist/components-page/forgot-password.js +2 -3
- 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/mfa.js +4 -19
- package/dist/components-page/mfa.js.map +1 -1
- package/dist/components-page/oauth-callback.js +1 -1
- package/dist/components-page/onboarding.js +1 -1
- package/dist/components-page/password-reset.d.ts.map +1 -1
- package/dist/components-page/password-reset.js +5 -7
- package/dist/components-page/password-reset.js.map +1 -1
- package/dist/components-page/team-creation.js +1 -1
- 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 +5 -12
- 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.js +2 -2
- package/dist/esm/components/credential-sign-in.js +1 -1
- package/dist/esm/components/credential-sign-up.js +1 -1
- package/dist/esm/components/magic-link-sign-in.js +1 -1
- package/dist/esm/components/team-switcher.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/email-and-auth/emails-section.js +1 -1
- 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 +1 -1
- package/dist/esm/components-page/account-settings/teams/team-creation-page.js +1 -1
- package/dist/esm/components-page/account-settings/teams/team-member-invitation-section.js +1 -1
- package/dist/esm/components-page/auth-page.js +1 -1
- package/dist/esm/components-page/cli-auth-confirm.js +1 -1
- package/dist/esm/components-page/cli-auth-confirm.test.js +1 -1
- package/dist/esm/components-page/forgot-password.d.ts.map +1 -1
- package/dist/esm/components-page/forgot-password.js +2 -3
- 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/mfa.js +4 -19
- package/dist/esm/components-page/mfa.js.map +1 -1
- package/dist/esm/components-page/oauth-callback.js +1 -1
- package/dist/esm/components-page/onboarding.js +1 -1
- package/dist/esm/components-page/password-reset.d.ts.map +1 -1
- package/dist/esm/components-page/password-reset.js +5 -7
- package/dist/esm/components-page/password-reset.js.map +1 -1
- package/dist/esm/components-page/team-creation.js +1 -1
- 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 +2 -9
- 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 +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +5 -1
- 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 +20 -0
- 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.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 +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/server-app-impl.js +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.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/providers/theme-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 +1 -1
- 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 +20 -0
- 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/implementations/session-replay.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/{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
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
|
|
2
|
+
//===========================================
|
|
3
|
+
// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template
|
|
4
|
+
//===========================================
|
|
5
|
+
// Shared design tokens + base reset for Hexclave's in-page UIs (the dev tool
|
|
6
|
+
// and the standalone clickmap overlay). Each feature passes its own scope
|
|
7
|
+
// selector so the two stylesheets never collide, while the tokens stay defined
|
|
8
|
+
// in exactly one place. This module deliberately lives outside both feature
|
|
9
|
+
// folders so either feature can be removed without affecting the other.
|
|
10
|
+
|
|
11
|
+
export function getInPageUiBaseCSS(scopeSelector: string): string {
|
|
12
|
+
return `
|
|
13
|
+
${scopeSelector} {
|
|
14
|
+
--sdt-bg: #0a0a0b;
|
|
15
|
+
--sdt-bg-elevated: #141416;
|
|
16
|
+
--sdt-bg-hover: #1c1c1f;
|
|
17
|
+
--sdt-bg-active: #232326;
|
|
18
|
+
--sdt-bg-subtle: #111113;
|
|
19
|
+
--sdt-border: #2a2a2e;
|
|
20
|
+
--sdt-border-subtle: #1e1e22;
|
|
21
|
+
--sdt-text: #ececef;
|
|
22
|
+
--sdt-text-secondary: #8b8b93;
|
|
23
|
+
--sdt-text-tertiary: #5c5c66;
|
|
24
|
+
--sdt-accent: #6366f1;
|
|
25
|
+
--sdt-accent-hover: #818cf8;
|
|
26
|
+
--sdt-accent-muted: rgba(99, 102, 241, 0.15);
|
|
27
|
+
--sdt-success: #22c55e;
|
|
28
|
+
--sdt-success-muted: rgba(34, 197, 94, 0.15);
|
|
29
|
+
--sdt-warning: #eab308;
|
|
30
|
+
--sdt-warning-muted: rgba(234, 179, 8, 0.15);
|
|
31
|
+
--sdt-error: #ef4444;
|
|
32
|
+
--sdt-error-muted: rgba(239, 68, 68, 0.15);
|
|
33
|
+
--sdt-info: #3b82f6;
|
|
34
|
+
--sdt-info-muted: rgba(59, 130, 246, 0.15);
|
|
35
|
+
--sdt-overlay-bg: rgba(17, 17, 19, 0.92);
|
|
36
|
+
--sdt-radius: 8px;
|
|
37
|
+
--sdt-radius-sm: 4px;
|
|
38
|
+
--sdt-radius-lg: 12px;
|
|
39
|
+
--sdt-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
40
|
+
--sdt-font-mono: 'SF Mono', SFMono-Regular, ui-monospace, 'DejaVu Sans Mono', Menlo, Consolas, monospace;
|
|
41
|
+
--sdt-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.05);
|
|
42
|
+
--sdt-trigger-shadow: 0 4px 12px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.08);
|
|
43
|
+
|
|
44
|
+
all: initial;
|
|
45
|
+
font-family: var(--sdt-font);
|
|
46
|
+
color: var(--sdt-text);
|
|
47
|
+
font-size: 13px;
|
|
48
|
+
line-height: 1.5;
|
|
49
|
+
-webkit-font-smoothing: antialiased;
|
|
50
|
+
-moz-osx-font-smoothing: grayscale;
|
|
51
|
+
box-sizing: border-box;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
${scopeSelector} *, ${scopeSelector} *::before, ${scopeSelector} *::after {
|
|
55
|
+
box-sizing: border-box;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* Thin, unobtrusive scrollbars for every scroll container */
|
|
59
|
+
${scopeSelector} * {
|
|
60
|
+
scrollbar-width: thin;
|
|
61
|
+
scrollbar-color: var(--sdt-border) transparent;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
${scopeSelector} *::-webkit-scrollbar {
|
|
65
|
+
width: 6px;
|
|
66
|
+
height: 6px;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
${scopeSelector} *::-webkit-scrollbar-track {
|
|
70
|
+
background: transparent;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
${scopeSelector} *::-webkit-scrollbar-thumb {
|
|
74
|
+
background: var(--sdt-border);
|
|
75
|
+
border-radius: 3px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
${scopeSelector} *::-webkit-scrollbar-thumb:hover {
|
|
79
|
+
background: var(--sdt-text-tertiary);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
${scopeSelector} *::-webkit-scrollbar-corner {
|
|
83
|
+
background: transparent;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* --- Light theme: system preference fallback --- */
|
|
87
|
+
@media (prefers-color-scheme: light) {
|
|
88
|
+
${scopeSelector} {
|
|
89
|
+
--sdt-bg: #ffffff;
|
|
90
|
+
--sdt-bg-elevated: #f8f8fa;
|
|
91
|
+
--sdt-bg-hover: #f0f0f3;
|
|
92
|
+
--sdt-bg-active: #e8e8ec;
|
|
93
|
+
--sdt-bg-subtle: #fafafa;
|
|
94
|
+
--sdt-border: #e0e0e5;
|
|
95
|
+
--sdt-border-subtle: #eaeaef;
|
|
96
|
+
--sdt-text: #111113;
|
|
97
|
+
--sdt-text-secondary: #6b6b73;
|
|
98
|
+
--sdt-text-tertiary: #9b9ba3;
|
|
99
|
+
--sdt-accent: #6366f1;
|
|
100
|
+
--sdt-accent-hover: #4f46e5;
|
|
101
|
+
--sdt-accent-muted: rgba(99, 102, 241, 0.1);
|
|
102
|
+
--sdt-success: #16a34a;
|
|
103
|
+
--sdt-success-muted: rgba(22, 163, 74, 0.1);
|
|
104
|
+
--sdt-warning: #ca8a04;
|
|
105
|
+
--sdt-warning-muted: rgba(202, 138, 4, 0.1);
|
|
106
|
+
--sdt-error: #dc2626;
|
|
107
|
+
--sdt-error-muted: rgba(220, 38, 38, 0.1);
|
|
108
|
+
--sdt-info: #2563eb;
|
|
109
|
+
--sdt-info-muted: rgba(37, 99, 235, 0.1);
|
|
110
|
+
--sdt-overlay-bg: rgba(255, 255, 255, 0.92);
|
|
111
|
+
--sdt-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0, 0, 0, 0.06);
|
|
112
|
+
--sdt-trigger-shadow: 0 4px 12px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(0, 0, 0, 0.06);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* --- Stack theme explicit overrides (take priority over system preference) --- */
|
|
117
|
+
html:has(head > [data-stack-theme="light"]) ${scopeSelector} {
|
|
118
|
+
--sdt-bg: #ffffff;
|
|
119
|
+
--sdt-bg-elevated: #f8f8fa;
|
|
120
|
+
--sdt-bg-hover: #f0f0f3;
|
|
121
|
+
--sdt-bg-active: #e8e8ec;
|
|
122
|
+
--sdt-bg-subtle: #fafafa;
|
|
123
|
+
--sdt-border: #e0e0e5;
|
|
124
|
+
--sdt-border-subtle: #eaeaef;
|
|
125
|
+
--sdt-text: #111113;
|
|
126
|
+
--sdt-text-secondary: #6b6b73;
|
|
127
|
+
--sdt-text-tertiary: #9b9ba3;
|
|
128
|
+
--sdt-accent: #6366f1;
|
|
129
|
+
--sdt-accent-hover: #4f46e5;
|
|
130
|
+
--sdt-accent-muted: rgba(99, 102, 241, 0.1);
|
|
131
|
+
--sdt-success: #16a34a;
|
|
132
|
+
--sdt-success-muted: rgba(22, 163, 74, 0.1);
|
|
133
|
+
--sdt-warning: #ca8a04;
|
|
134
|
+
--sdt-warning-muted: rgba(202, 138, 4, 0.1);
|
|
135
|
+
--sdt-error: #dc2626;
|
|
136
|
+
--sdt-error-muted: rgba(220, 38, 38, 0.1);
|
|
137
|
+
--sdt-info: #2563eb;
|
|
138
|
+
--sdt-info-muted: rgba(37, 99, 235, 0.1);
|
|
139
|
+
--sdt-overlay-bg: rgba(255, 255, 255, 0.92);
|
|
140
|
+
--sdt-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0, 0, 0, 0.06);
|
|
141
|
+
--sdt-trigger-shadow: 0 4px 12px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(0, 0, 0, 0.06);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
html:has(head > [data-stack-theme="dark"]) ${scopeSelector} {
|
|
145
|
+
--sdt-bg: #0a0a0b;
|
|
146
|
+
--sdt-bg-elevated: #141416;
|
|
147
|
+
--sdt-bg-hover: #1c1c1f;
|
|
148
|
+
--sdt-bg-active: #232326;
|
|
149
|
+
--sdt-bg-subtle: #111113;
|
|
150
|
+
--sdt-border: #2a2a2e;
|
|
151
|
+
--sdt-border-subtle: #1e1e22;
|
|
152
|
+
--sdt-text: #ececef;
|
|
153
|
+
--sdt-text-secondary: #8b8b93;
|
|
154
|
+
--sdt-text-tertiary: #5c5c66;
|
|
155
|
+
--sdt-accent: #6366f1;
|
|
156
|
+
--sdt-accent-hover: #818cf8;
|
|
157
|
+
--sdt-accent-muted: rgba(99, 102, 241, 0.15);
|
|
158
|
+
--sdt-success: #22c55e;
|
|
159
|
+
--sdt-success-muted: rgba(34, 197, 94, 0.15);
|
|
160
|
+
--sdt-warning: #eab308;
|
|
161
|
+
--sdt-warning-muted: rgba(234, 179, 8, 0.15);
|
|
162
|
+
--sdt-error: #ef4444;
|
|
163
|
+
--sdt-error-muted: rgba(239, 68, 68, 0.15);
|
|
164
|
+
--sdt-info: #3b82f6;
|
|
165
|
+
--sdt-info-muted: rgba(59, 130, 246, 0.15);
|
|
166
|
+
--sdt-overlay-bg: rgba(17, 17, 19, 0.92);
|
|
167
|
+
--sdt-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.05);
|
|
168
|
+
--sdt-trigger-shadow: 0 4px 12px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.08);
|
|
169
|
+
}
|
|
170
|
+
`;
|
|
171
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
|
|
2
|
+
//===========================================
|
|
3
|
+
// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template
|
|
4
|
+
//===========================================
|
|
5
|
+
// Tiny DOM helpers shared by Hexclave's in-page UIs (the dev tool and the
|
|
6
|
+
// standalone clickmap overlay). This module deliberately lives outside both
|
|
7
|
+
// feature folders so either feature can be removed without affecting the other.
|
|
8
|
+
|
|
9
|
+
export function h<K extends keyof HTMLElementTagNameMap>(
|
|
10
|
+
tag: K,
|
|
11
|
+
attrs?: Record<string, any> | null,
|
|
12
|
+
...children: (string | Node | null | undefined)[]
|
|
13
|
+
): HTMLElementTagNameMap[K] {
|
|
14
|
+
const el = document.createElement(tag);
|
|
15
|
+
if (attrs) {
|
|
16
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
17
|
+
if (v == null) continue;
|
|
18
|
+
if (k === 'className') {
|
|
19
|
+
el.className = v;
|
|
20
|
+
} else if (k === 'style' && typeof v === 'object') {
|
|
21
|
+
Object.assign(el.style, v);
|
|
22
|
+
} else if (k.startsWith('on') && typeof v === 'function') {
|
|
23
|
+
el.addEventListener(k.slice(2).toLowerCase(), v);
|
|
24
|
+
} else {
|
|
25
|
+
el.setAttribute(k, String(v));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
for (const child of children) {
|
|
30
|
+
if (child == null) continue;
|
|
31
|
+
el.appendChild(typeof child === 'string' ? document.createTextNode(child) : child);
|
|
32
|
+
}
|
|
33
|
+
return el;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function setHtml(el: HTMLElement, html: string) {
|
|
37
|
+
el.innerHTML = html;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function hasAppendChild(value: unknown): value is { appendChild(node: Node): void } {
|
|
41
|
+
return typeof value === 'object' && value !== null && typeof Reflect.get(value, 'appendChild') === 'function';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function canMountIntoDom(): boolean {
|
|
45
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
if (typeof document.createElement !== 'function') {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return hasAppendChild(Reflect.get(document, 'body'));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Window-global singleton slot, so remounts (e.g. across HMR or multiple app
|
|
56
|
+
// instances) can tear down the previous instance before mounting a new one.
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
export type UiGlobalInstance = {
|
|
60
|
+
cleanup: () => void;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
function isUiGlobalInstance(value: unknown): value is UiGlobalInstance {
|
|
64
|
+
return typeof value === 'object' && value !== null && typeof Reflect.get(value, 'cleanup') === 'function';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getGlobalUiInstance(key: string): UiGlobalInstance | null {
|
|
68
|
+
if (typeof window === 'undefined') return null;
|
|
69
|
+
const value: unknown = Reflect.get(window, key);
|
|
70
|
+
return isUiGlobalInstance(value) ? value : null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function setGlobalUiInstance(key: string, instance: UiGlobalInstance | null) {
|
|
74
|
+
if (typeof window === 'undefined') return;
|
|
75
|
+
if (instance === null) {
|
|
76
|
+
Reflect.deleteProperty(window, key);
|
|
77
|
+
} else {
|
|
78
|
+
Reflect.set(window, key, instance);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { KnownErrors, HexclaveAdminInterface } from "@hexclave/shared";
|
|
6
6
|
import { getProductionModeErrors } from "@hexclave/shared/dist/helpers/production-mode";
|
|
7
7
|
import { InternalApiKeyCreateCrudResponse } from "@hexclave/shared/dist/interface/admin-interface";
|
|
8
|
-
import type { MetricsResponse, MetricsUserCounts, UserActivityResponse } from "@hexclave/shared/dist/interface/admin-metrics";
|
|
8
|
+
import type { AnalyticsClickmapOptions, AnalyticsClickmapResponse, AnalyticsClickmapTokenResponse, MetricsResponse, MetricsUserCounts, UserActivityResponse } from "@hexclave/shared/dist/interface/admin-metrics";
|
|
9
9
|
import { AnalyticsQueryOptions, AnalyticsQueryResponse } from "@hexclave/shared/dist/interface/crud/analytics";
|
|
10
10
|
import { EmailTemplateCrud } from "@hexclave/shared/dist/interface/crud/email-templates";
|
|
11
11
|
import { InternalApiKeysCrud } from "@hexclave/shared/dist/interface/crud/internal-api-keys";
|
|
@@ -1151,6 +1151,28 @@ export class _HexclaveAdminAppImplIncomplete<HasTokenStore extends boolean, Proj
|
|
|
1151
1151
|
return await this._interface.queryAnalytics(options);
|
|
1152
1152
|
}
|
|
1153
1153
|
|
|
1154
|
+
async getAnalyticsClickmap(options: AnalyticsClickmapOptions): Promise<AnalyticsClickmapResponse> {
|
|
1155
|
+
return await this._interface.getAnalyticsClickmap({
|
|
1156
|
+
kind: options.kind,
|
|
1157
|
+
member_user_ids: options.memberUserIds,
|
|
1158
|
+
route_path: options.routePath,
|
|
1159
|
+
route_regex: options.routeRegex,
|
|
1160
|
+
url_pattern: options.urlPattern,
|
|
1161
|
+
user_id: options.userId,
|
|
1162
|
+
replay_id: options.replayId,
|
|
1163
|
+
device: options.device,
|
|
1164
|
+
viewport_width_min: options.viewportWidthMin,
|
|
1165
|
+
viewport_width_max: options.viewportWidthMax,
|
|
1166
|
+
sampling: options.sampling,
|
|
1167
|
+
since: options.since,
|
|
1168
|
+
until: options.until,
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
async createAnalyticsClickmapToken(options: { origin: string }): Promise<AnalyticsClickmapTokenResponse> {
|
|
1173
|
+
return await this._interface.createAnalyticsClickmapToken(options);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1154
1176
|
async listSessionReplays(options?: ListSessionReplaysOptions): Promise<ListSessionReplaysResult> {
|
|
1155
1177
|
const response = await this._interface.listSessionReplays({
|
|
1156
1178
|
cursor: options?.cursor,
|
|
@@ -70,6 +70,7 @@ import { subscribeSessionRefresh } from "./session-refresh-subscription";
|
|
|
70
70
|
import { AnalyticsOptions, SessionRecorder, analyticsOptionsFromJson, analyticsOptionsToJson } from "./session-replay";
|
|
71
71
|
|
|
72
72
|
import { useAsyncCache } from "./common";
|
|
73
|
+
import { mountClickmapOverlay } from "../../../../clickmap";
|
|
73
74
|
import { mountDevTool } from "../../../../dev-tool";
|
|
74
75
|
|
|
75
76
|
let isReactServer = false;
|
|
@@ -702,6 +703,12 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
702
703
|
if (isBrowserLike() && resolvedOptions.devTool !== false) {
|
|
703
704
|
mountDevTool(this as any);
|
|
704
705
|
}
|
|
706
|
+
if (isBrowserLike()) {
|
|
707
|
+
// Independent of the dev tool: the clickmap overlay only ever renders
|
|
708
|
+
// when a dashboard-minted token is handed over, so the listener is
|
|
709
|
+
// mounted unconditionally (the heavy UI is lazy-loaded on demand).
|
|
710
|
+
mountClickmapOverlay(this as any);
|
|
711
|
+
}
|
|
705
712
|
}
|
|
706
713
|
|
|
707
714
|
protected _initUniqueIdentifier() {
|
|
@@ -59,6 +59,8 @@ describe("EventTracker", () => {
|
|
|
59
59
|
|
|
60
60
|
await advancePastFlush();
|
|
61
61
|
|
|
62
|
+
// Dead-click classification marks the buffered $click in place —
|
|
63
|
+
// exactly one click event either way.
|
|
62
64
|
expect(getSentEventTypes(sentBodies)).toMatchInlineSnapshot(`
|
|
63
65
|
[
|
|
64
66
|
"$page-view",
|
|
@@ -70,6 +72,291 @@ describe("EventTracker", () => {
|
|
|
70
72
|
}
|
|
71
73
|
});
|
|
72
74
|
|
|
75
|
+
it("emits a PostHog-style elements_chain plus scaled pointer coords for $click", async () => {
|
|
76
|
+
vi.useFakeTimers();
|
|
77
|
+
document.body.innerHTML = `
|
|
78
|
+
<main>
|
|
79
|
+
<section class="card panel">
|
|
80
|
+
<button id="save-btn" data-testid="save" aria-label="Save project">Save changes</button>
|
|
81
|
+
</section>
|
|
82
|
+
</main>
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
const sentBodies: string[] = [];
|
|
86
|
+
const tracker = new EventTracker({
|
|
87
|
+
projectId: "internal",
|
|
88
|
+
sendBatch: async (body) => {
|
|
89
|
+
sentBodies.push(body);
|
|
90
|
+
return Result.ok(new Response());
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
tracker.start();
|
|
96
|
+
const button = document.querySelector("#save-btn");
|
|
97
|
+
if (button == null) throw new Error("button missing");
|
|
98
|
+
button.dispatchEvent(new MouseEvent("click", {
|
|
99
|
+
bubbles: true,
|
|
100
|
+
clientX: 100,
|
|
101
|
+
clientY: 200,
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
await advancePastFlush();
|
|
105
|
+
|
|
106
|
+
const payload = JSON.parse(sentBodies[0] ?? "{}") as { events: { event_type: string, data: Record<string, unknown> }[] };
|
|
107
|
+
const click = payload.events.find((event) => event.event_type === "$click");
|
|
108
|
+
if (click == null) throw new Error("no $click event captured");
|
|
109
|
+
|
|
110
|
+
// elements_chain encodes the target leaf plus a few ancestors. Leaf is
|
|
111
|
+
// first; segments are `;`-delimited. Assert against substrings rather
|
|
112
|
+
// than the full string so jsdom layout quirks don't make this flaky.
|
|
113
|
+
const chain = click.data.elements_chain;
|
|
114
|
+
expect(typeof chain).toBe("string");
|
|
115
|
+
expect(chain).toContain('button');
|
|
116
|
+
expect(chain).toContain('attr__id="save-btn"');
|
|
117
|
+
expect(chain).toContain('attr__data-testid="save"');
|
|
118
|
+
expect(chain).toContain('attr__aria-label="Save project"');
|
|
119
|
+
expect(chain).toContain('text="Save changes"');
|
|
120
|
+
// Ancestor section is in the chain too.
|
|
121
|
+
expect(chain).toContain("section");
|
|
122
|
+
|
|
123
|
+
// Pre-scaled coords land in clickmap_events.pointer_*. SCALE_FACTOR=16.
|
|
124
|
+
expect(click.data.x_scaled).toBe(Math.round(100 / 16));
|
|
125
|
+
expect(click.data.y_scaled).toBe(Math.round(200 / 16));
|
|
126
|
+
expect(click.data.client_y_scaled).toBe(Math.round(200 / 16));
|
|
127
|
+
expect(click.data.scale_factor).toBe(16);
|
|
128
|
+
expect(click.data.pointer_relative_x).toBeCloseTo(100 / window.innerWidth, 4);
|
|
129
|
+
expect(click.data.pointer_target_fixed).toBe(0);
|
|
130
|
+
|
|
131
|
+
// Legacy CSS selector still emitted for back-compat. The builder prefers
|
|
132
|
+
// data-testid over id, so we assert against that anchor rather than #id.
|
|
133
|
+
expect(click.data.selector).toContain('data-testid="save"');
|
|
134
|
+
expect(click.data.tag_name).toBe("button");
|
|
135
|
+
} finally {
|
|
136
|
+
tracker.stop();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("ignores clicks inside the Hexclave dev tool", async () => {
|
|
141
|
+
vi.useFakeTimers();
|
|
142
|
+
document.body.innerHTML = `
|
|
143
|
+
<div id="__hexclave-dev-tool-root">
|
|
144
|
+
<button>Clickmap toolbar control</button>
|
|
145
|
+
</div>
|
|
146
|
+
`;
|
|
147
|
+
|
|
148
|
+
const sentBodies: string[] = [];
|
|
149
|
+
const tracker = new EventTracker({
|
|
150
|
+
projectId: "internal",
|
|
151
|
+
sendBatch: async (body) => {
|
|
152
|
+
sentBodies.push(body);
|
|
153
|
+
return Result.ok(new Response());
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
tracker.start();
|
|
159
|
+
document.querySelector("button")?.dispatchEvent(new MouseEvent("click", {
|
|
160
|
+
bubbles: true,
|
|
161
|
+
clientX: 100,
|
|
162
|
+
clientY: 200,
|
|
163
|
+
}));
|
|
164
|
+
|
|
165
|
+
await advancePastFlush();
|
|
166
|
+
|
|
167
|
+
expect(getSentEventTypes(sentBodies)).toMatchInlineSnapshot(`
|
|
168
|
+
[
|
|
169
|
+
"$page-view",
|
|
170
|
+
]
|
|
171
|
+
`);
|
|
172
|
+
} finally {
|
|
173
|
+
tracker.stop();
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("flags pointer_target_fixed when the target sits under a fixed-position ancestor", async () => {
|
|
178
|
+
vi.useFakeTimers();
|
|
179
|
+
document.body.innerHTML = `
|
|
180
|
+
<header style="position: fixed; top: 0">
|
|
181
|
+
<button id="cta">Sign up</button>
|
|
182
|
+
</header>
|
|
183
|
+
`;
|
|
184
|
+
|
|
185
|
+
const sentBodies: string[] = [];
|
|
186
|
+
const tracker = new EventTracker({
|
|
187
|
+
projectId: "internal",
|
|
188
|
+
sendBatch: async (body) => {
|
|
189
|
+
sentBodies.push(body);
|
|
190
|
+
return Result.ok(new Response());
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
tracker.start();
|
|
196
|
+
document.querySelector("#cta")?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
197
|
+
await advancePastFlush();
|
|
198
|
+
|
|
199
|
+
const payload = JSON.parse(sentBodies[0] ?? "{}") as { events: { event_type: string, data: Record<string, unknown> }[] };
|
|
200
|
+
const click = payload.events.find((event) => event.event_type === "$click");
|
|
201
|
+
expect(click?.data.pointer_target_fixed).toBe(1);
|
|
202
|
+
} finally {
|
|
203
|
+
tracker.stop();
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("flags a click with no observable effect as dead on its single $click event", async () => {
|
|
208
|
+
vi.useFakeTimers();
|
|
209
|
+
document.body.innerHTML = "<button id=\"dead\">Does nothing</button>";
|
|
210
|
+
|
|
211
|
+
const sentBodies: string[] = [];
|
|
212
|
+
const tracker = new EventTracker({
|
|
213
|
+
projectId: "internal",
|
|
214
|
+
sendBatch: async (body) => {
|
|
215
|
+
sentBodies.push(body);
|
|
216
|
+
return Result.ok(new Response());
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
tracker.start();
|
|
222
|
+
const clickAtMs = Date.now();
|
|
223
|
+
document.querySelector("#dead")?.dispatchEvent(new MouseEvent("click", {
|
|
224
|
+
bubbles: true,
|
|
225
|
+
clientX: 10,
|
|
226
|
+
clientY: 20,
|
|
227
|
+
}));
|
|
228
|
+
|
|
229
|
+
await advancePastFlush();
|
|
230
|
+
|
|
231
|
+
const payload = JSON.parse(sentBodies[0] ?? "{}") as { events: { event_type: string, event_at_ms: number, data: Record<string, unknown> }[] };
|
|
232
|
+
const clicks = payload.events.filter((event) => event.event_type === "$click");
|
|
233
|
+
expect(clicks).toHaveLength(1);
|
|
234
|
+
const click = clicks[0];
|
|
235
|
+
|
|
236
|
+
// One event per physical click: the buffered $click is marked dead in
|
|
237
|
+
// place, still timestamped at the original click rather than at
|
|
238
|
+
// classification time (~3s later).
|
|
239
|
+
expect(click.data.dead).toBe(1);
|
|
240
|
+
expect(click.event_at_ms).toBe(clickAtMs);
|
|
241
|
+
} finally {
|
|
242
|
+
tracker.stop();
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("does not flag a click as dead when it mutates the DOM", async () => {
|
|
247
|
+
vi.useFakeTimers();
|
|
248
|
+
document.body.innerHTML = "<button id=\"live\">Adds content</button><div id=\"out\"></div>";
|
|
249
|
+
|
|
250
|
+
const sentBodies: string[] = [];
|
|
251
|
+
const tracker = new EventTracker({
|
|
252
|
+
projectId: "internal",
|
|
253
|
+
sendBatch: async (body) => {
|
|
254
|
+
sentBodies.push(body);
|
|
255
|
+
return Result.ok(new Response());
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
tracker.start();
|
|
261
|
+
const button = document.querySelector("#live");
|
|
262
|
+
if (button == null) throw new Error("button missing");
|
|
263
|
+
button.addEventListener("click", () => {
|
|
264
|
+
document.querySelector("#out")?.appendChild(document.createElement("p"));
|
|
265
|
+
});
|
|
266
|
+
button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
267
|
+
// Let the MutationObserver microtask run so the mutation is recorded
|
|
268
|
+
// before the dead-click sweeps start.
|
|
269
|
+
await Promise.resolve();
|
|
270
|
+
|
|
271
|
+
await advancePastFlush();
|
|
272
|
+
|
|
273
|
+
const payload = JSON.parse(sentBodies[0] ?? "{}") as { events: { event_type: string, data: Record<string, unknown> }[] };
|
|
274
|
+
const clicks = payload.events.filter((event) => event.event_type === "$click");
|
|
275
|
+
expect(clicks).toHaveLength(1);
|
|
276
|
+
expect(clicks[0].data.dead).toBeUndefined();
|
|
277
|
+
} finally {
|
|
278
|
+
tracker.stop();
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("drains held clicks as alive on pagehide so navigation clicks are never lost", async () => {
|
|
283
|
+
vi.useFakeTimers();
|
|
284
|
+
document.body.innerHTML = "<a id=\"nav\" href=\"/pricing\">Pricing</a>";
|
|
285
|
+
|
|
286
|
+
const sentBodies: string[] = [];
|
|
287
|
+
const tracker = new EventTracker({
|
|
288
|
+
projectId: "internal",
|
|
289
|
+
sendBatch: async (body) => {
|
|
290
|
+
sentBodies.push(body);
|
|
291
|
+
return Result.ok(new Response());
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
tracker.start();
|
|
297
|
+
const clickAtMs = Date.now();
|
|
298
|
+
document.querySelector("#nav")?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
299
|
+
|
|
300
|
+
// Navigation fires pagehide well before any classification sweep — the
|
|
301
|
+
// keepalive flush ships the still-unclassified click as a plain (alive)
|
|
302
|
+
// $click.
|
|
303
|
+
window.dispatchEvent(new Event("pagehide"));
|
|
304
|
+
await Promise.resolve();
|
|
305
|
+
await Promise.resolve();
|
|
306
|
+
|
|
307
|
+
const payload = JSON.parse(sentBodies[0] ?? "{}") as { events: { event_type: string, event_at_ms: number, data: Record<string, unknown> }[] };
|
|
308
|
+
const clicks = payload.events.filter((event) => event.event_type === "$click");
|
|
309
|
+
expect(clicks).toHaveLength(1);
|
|
310
|
+
expect(clicks[0].data.dead).toBeUndefined();
|
|
311
|
+
expect(clicks[0].event_at_ms).toBe(clickAtMs);
|
|
312
|
+
} finally {
|
|
313
|
+
tracker.stop();
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("holds an unclassified click out of a flush and ships it on the next one", async () => {
|
|
318
|
+
vi.useFakeTimers();
|
|
319
|
+
document.body.innerHTML = "<button id=\"late\">Late click</button>";
|
|
320
|
+
|
|
321
|
+
const sentBodies: string[] = [];
|
|
322
|
+
const tracker = new EventTracker({
|
|
323
|
+
projectId: "internal",
|
|
324
|
+
sendBatch: async (body) => {
|
|
325
|
+
sentBodies.push(body);
|
|
326
|
+
return Result.ok(new Response());
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
tracker.start();
|
|
332
|
+
// Click 500ms before the 10s flush tick: classification cannot finish
|
|
333
|
+
// in time, so the flush must hold the click back rather than send it
|
|
334
|
+
// unclassified.
|
|
335
|
+
await vi.advanceTimersByTimeAsync(9_500);
|
|
336
|
+
document.querySelector("#late")?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
337
|
+
await vi.advanceTimersByTimeAsync(500);
|
|
338
|
+
|
|
339
|
+
expect(getSentEventTypes(sentBodies)).toMatchInlineSnapshot(`
|
|
340
|
+
[
|
|
341
|
+
"$page-view",
|
|
342
|
+
]
|
|
343
|
+
`);
|
|
344
|
+
|
|
345
|
+
// By the next flush the sweep has classified it (dead — nothing
|
|
346
|
+
// observable happened) and it ships marked.
|
|
347
|
+
await vi.advanceTimersByTimeAsync(10_000);
|
|
348
|
+
const second = JSON.parse(sentBodies[1] ?? "{}") as { events: { event_type: string, data: Record<string, unknown> }[] };
|
|
349
|
+
expect(second.events.map((event) => event.event_type)).toMatchInlineSnapshot(`
|
|
350
|
+
[
|
|
351
|
+
"$click",
|
|
352
|
+
]
|
|
353
|
+
`);
|
|
354
|
+
expect(second.events[0].data.dead).toBe(1);
|
|
355
|
+
} finally {
|
|
356
|
+
tracker.stop();
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
73
360
|
it("captures client-side navigations when history is exposed as an accessor descriptor", async () => {
|
|
74
361
|
vi.useFakeTimers();
|
|
75
362
|
|