@hexclave/next 1.0.14 → 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 +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.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 +1 -1
- 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.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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
2
|
const require_chunk = require('../../../../chunk-BE-pF4vm.js');
|
|
3
3
|
let _hexclave_shared_dist_utils_promises = require("@hexclave/shared/dist/utils/promises");
|
|
4
|
+
let _hexclave_shared_dist_utils_errors = require("@hexclave/shared/dist/utils/errors");
|
|
4
5
|
let react = require("react");
|
|
5
6
|
react = require_chunk.__toESM(react);
|
|
6
|
-
let _hexclave_shared_dist_utils_errors = require("@hexclave/shared/dist/utils/errors");
|
|
7
7
|
let _hexclave_shared_dist_utils_react = require("@hexclave/shared/dist/utils/react");
|
|
8
8
|
let _hexclave_shared_dist_utils_objects = require("@hexclave/shared/dist/utils/objects");
|
|
9
9
|
let _hexclave_shared_dist_utils_urls = require("@hexclave/shared/dist/utils/urls");
|
|
@@ -17,7 +17,7 @@ let ____________generated_env_js = require("../../../../generated/env.js");
|
|
|
17
17
|
let ______url_targets_js = require("../../url-targets.js");
|
|
18
18
|
|
|
19
19
|
//#region src/lib/hexclave-app/apps/implementations/common.ts
|
|
20
|
-
const clientVersion = "js @hexclave/next@1.0.
|
|
20
|
+
const clientVersion = "js @hexclave/next@1.0.15";
|
|
21
21
|
if (clientVersion.startsWith("STACK_COMPILE_TIME")) throw new _hexclave_shared_dist_utils_errors.HexclaveAssertionError("Client version was not replaced. Something went wrong during build!");
|
|
22
22
|
const replaceHexclavePortPrefix = (input) => {
|
|
23
23
|
if (!input) return input;
|
|
@@ -19,6 +19,13 @@ declare class EventTracker {
|
|
|
19
19
|
private readonly _deps;
|
|
20
20
|
private _originalPushState;
|
|
21
21
|
private _originalReplaceState;
|
|
22
|
+
private _deadClickTimer;
|
|
23
|
+
private _deadClickMutationObserver;
|
|
24
|
+
private _unclassifiedClicks;
|
|
25
|
+
private _lastMutationAtMs;
|
|
26
|
+
private _lastScrollAtMs;
|
|
27
|
+
private _lastSelectionChangedAtMs;
|
|
28
|
+
private _lastVisibilityChangeAtMs;
|
|
22
29
|
constructor(deps: EventTrackerDeps);
|
|
23
30
|
start(): void;
|
|
24
31
|
stop(): void;
|
|
@@ -31,6 +38,12 @@ declare class EventTracker {
|
|
|
31
38
|
private _findNearestAnchorHref;
|
|
32
39
|
private readonly _onClickCapture;
|
|
33
40
|
private _setupClickCapture;
|
|
41
|
+
private readonly _onDeadClickScroll;
|
|
42
|
+
private readonly _onDeadClickSelectionChange;
|
|
43
|
+
private readonly _onDeadClickVisibilityChange;
|
|
44
|
+
private _setupDeadClickDetection;
|
|
45
|
+
private _checkDeadClicks;
|
|
46
|
+
private _teardownDeadClickDetection;
|
|
34
47
|
private readonly _onPageHide;
|
|
35
48
|
private _setupPageHideListeners;
|
|
36
49
|
private _teardown;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event-tracker.d.ts","names":[],"sources":["../../../../../src/lib/hexclave-app/apps/implementations/event-tracker.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"event-tracker.d.ts","names":[],"sources":["../../../../../src/lib/hexclave-app/apps/implementations/event-tracker.ts"],"mappings":";;;KAoGY,gBAAA;EACV,SAAA;EACA,SAAA,GAAY,IAAA,UAAc,OAAA;IAAW,SAAA;EAAA,MAAyB,OAAA,CAAQ,MAAA,CAAO,QAAA,EAAU,KAAA;AAAA;AAAA,cAS5E,YAAA;EAAA,QACH,QAAA;EAAA,QACA,UAAA;EAAA,QACA,gBAAA;EAAA,QACA,WAAA;EAAA,QACA,OAAA;EAAA,QACA,YAAA;EAAA,QACA,QAAA;EAAA,iBACS,uBAAA;EAAA,iBACA,KAAA;EAAA,QAET,kBAAA;EAAA,QACA,qBAAA;EAAA,QAEA,eAAA;EAAA,QACA,0BAAA;EAAA,QAGA,mBAAA;EAAA,QACA,iBAAA;EAAA,QACA,eAAA;EAAA,QACA,yBAAA;EAAA,QACA,yBAAA;cAEI,IAAA,EAAM,gBAAA;EAKlB,KAAA,CAAA;EAsBA,IAAA,CAAA;EAUA,WAAA,CAAA;EAAA,QAMQ,UAAA;EAAA,QAQA,gBAAA;EAAA,QA4BA,qBAAA;EAAA,iBA4BS,WAAA;EAAA,QAIT,cAAA;EAAA,QA6CA,sBAAA;EAAA,iBAWS,eAAA;EAAA,QAiDT,kBAAA;EAAA,iBAIS,kBAAA;EAAA,iBAIA,2BAAA;EAAA,iBAIA,4BAAA;EAAA,QAIT,wBAAA;EAAA,QA2BA,gBAAA;EAAA,QAoBA,2BAAA;EAAA,iBAeS,WAAA;EAAA,QAIT,uBAAA;EAAA,QASA,SAAA;EAAA,QA2BM,MAAA;EAAA,QAyCN,KAAA;AAAA"}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
2
|
const require_chunk = require('../../../../chunk-BE-pF4vm.js');
|
|
3
|
+
let _hexclave_shared_dist_utils_dev_tool = require("@hexclave/shared/dist/utils/dev-tool");
|
|
4
|
+
let _hexclave_shared_dist_utils_dom = require("@hexclave/shared/dist/utils/dom");
|
|
5
|
+
let _hexclave_shared_dist_utils_elements_chain = require("@hexclave/shared/dist/utils/elements-chain");
|
|
3
6
|
let _hexclave_shared_dist_utils_promises = require("@hexclave/shared/dist/utils/promises");
|
|
4
7
|
let _hexclave_shared_dist_utils_env = require("@hexclave/shared/dist/utils/env");
|
|
5
8
|
let __session_replay_js = require("./session-replay.js");
|
|
@@ -18,6 +21,32 @@ function hasHistoryMethods(value) {
|
|
|
18
21
|
if (!("pushState" in value) || !("replaceState" in value)) return false;
|
|
19
22
|
return typeof value.pushState === "function" && typeof value.replaceState === "function";
|
|
20
23
|
}
|
|
24
|
+
const CLICKMAP_SCALE_FACTOR = 16;
|
|
25
|
+
const DEAD_CLICK_SCROLL_THRESHOLD_MS = 100;
|
|
26
|
+
const DEAD_CLICK_SELECTION_CHANGED_THRESHOLD_MS = 100;
|
|
27
|
+
const DEAD_CLICK_VISIBILITY_CHANGE_THRESHOLD_MS = 100;
|
|
28
|
+
const DEAD_CLICK_MUTATION_THRESHOLD_MS = 2500;
|
|
29
|
+
const DEAD_CLICK_ABSOLUTE_TIMEOUT_MS = 2750;
|
|
30
|
+
const DEAD_CLICK_CHECK_INTERVAL_MS = 1e3;
|
|
31
|
+
const DEAD_CLICK_MAX_PENDING = 50;
|
|
32
|
+
function isPointerTargetFixed(element) {
|
|
33
|
+
let current = element;
|
|
34
|
+
let depth = 0;
|
|
35
|
+
while (current != null && depth < _hexclave_shared_dist_utils_elements_chain.ELEMENTS_CHAIN_MAX_DEPTH * 2) {
|
|
36
|
+
const style = window.getComputedStyle(current);
|
|
37
|
+
if (style.position === "fixed" || style.position === "sticky") return true;
|
|
38
|
+
current = current.parentElement;
|
|
39
|
+
depth += 1;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
function isInsideHexclaveUi(element) {
|
|
44
|
+
return element.closest(`#${(0, _hexclave_shared_dist_utils_dom.cssEscapeIdent)(_hexclave_shared_dist_utils_dev_tool.DEV_TOOL_ROOT_ID)}, #${(0, _hexclave_shared_dist_utils_dom.cssEscapeIdent)(_hexclave_shared_dist_utils_dev_tool.CLICKMAP_ROOT_ID)}`) != null;
|
|
45
|
+
}
|
|
46
|
+
function isInsideHexclaveUiNode(node) {
|
|
47
|
+
const element = node instanceof Element ? node : node?.parentElement ?? null;
|
|
48
|
+
return element != null && isInsideHexclaveUi(element);
|
|
49
|
+
}
|
|
21
50
|
var EventTracker = class {
|
|
22
51
|
constructor(deps) {
|
|
23
52
|
this._started = false;
|
|
@@ -29,13 +58,28 @@ var EventTracker = class {
|
|
|
29
58
|
this._lastUrl = null;
|
|
30
59
|
this._originalPushState = null;
|
|
31
60
|
this._originalReplaceState = null;
|
|
61
|
+
this._deadClickTimer = null;
|
|
62
|
+
this._deadClickMutationObserver = null;
|
|
63
|
+
this._unclassifiedClicks = /* @__PURE__ */ new Set();
|
|
64
|
+
this._lastMutationAtMs = null;
|
|
65
|
+
this._lastScrollAtMs = null;
|
|
66
|
+
this._lastSelectionChangedAtMs = null;
|
|
67
|
+
this._lastVisibilityChangeAtMs = null;
|
|
32
68
|
this._onPopState = () => {
|
|
33
69
|
this._capturePageView("pop");
|
|
34
70
|
};
|
|
35
71
|
this._onClickCapture = (event) => {
|
|
36
72
|
const target = event.target;
|
|
37
73
|
if (!(target instanceof Element)) return;
|
|
38
|
-
|
|
74
|
+
if (isInsideHexclaveUi(target)) return;
|
|
75
|
+
const viewportWidth = window.innerWidth;
|
|
76
|
+
const viewportHeight = window.innerHeight;
|
|
77
|
+
const pointerTargetFixed = isPointerTargetFixed(target);
|
|
78
|
+
const xScaled = Math.round(event.pageX / CLICKMAP_SCALE_FACTOR);
|
|
79
|
+
const yScaled = Math.round(event.pageY / CLICKMAP_SCALE_FACTOR);
|
|
80
|
+
const clientYScaled = Math.round(event.clientY / CLICKMAP_SCALE_FACTOR);
|
|
81
|
+
const relativeX = viewportWidth > 0 ? event.clientX / viewportWidth : 0;
|
|
82
|
+
const clickEvent = {
|
|
39
83
|
event_type: "$click",
|
|
40
84
|
event_at_ms: Date.now(),
|
|
41
85
|
data: {
|
|
@@ -43,14 +87,35 @@ var EventTracker = class {
|
|
|
43
87
|
text: target.textContent.trim().substring(0, 200),
|
|
44
88
|
href: this._findNearestAnchorHref(target),
|
|
45
89
|
selector: this._buildSelector(target),
|
|
90
|
+
elements_chain: (0, _hexclave_shared_dist_utils_elements_chain.buildElementsChain)(target),
|
|
91
|
+
pointer_target_fixed: pointerTargetFixed ? 1 : 0,
|
|
92
|
+
url: window.location.href,
|
|
93
|
+
path: window.location.pathname,
|
|
94
|
+
title: document.title,
|
|
46
95
|
x: event.clientX,
|
|
47
96
|
y: event.clientY,
|
|
48
97
|
page_x: event.pageX,
|
|
49
98
|
page_y: event.pageY,
|
|
50
|
-
|
|
51
|
-
|
|
99
|
+
x_scaled: xScaled,
|
|
100
|
+
y_scaled: yScaled,
|
|
101
|
+
client_y_scaled: clientYScaled,
|
|
102
|
+
pointer_relative_x: relativeX,
|
|
103
|
+
viewport_width: viewportWidth,
|
|
104
|
+
viewport_height: viewportHeight,
|
|
105
|
+
scale_factor: CLICKMAP_SCALE_FACTOR
|
|
52
106
|
}
|
|
53
|
-
}
|
|
107
|
+
};
|
|
108
|
+
if (this._deadClickTimer !== null && this._unclassifiedClicks.size < DEAD_CLICK_MAX_PENDING) this._unclassifiedClicks.add(clickEvent);
|
|
109
|
+
this._pushEvent(clickEvent);
|
|
110
|
+
};
|
|
111
|
+
this._onDeadClickScroll = () => {
|
|
112
|
+
this._lastScrollAtMs = Date.now();
|
|
113
|
+
};
|
|
114
|
+
this._onDeadClickSelectionChange = () => {
|
|
115
|
+
this._lastSelectionChangedAtMs = Date.now();
|
|
116
|
+
};
|
|
117
|
+
this._onDeadClickVisibilityChange = () => {
|
|
118
|
+
this._lastVisibilityChangeAtMs = Date.now();
|
|
54
119
|
};
|
|
55
120
|
this._onPageHide = () => {
|
|
56
121
|
(0, _hexclave_shared_dist_utils_promises.runAsynchronously)(() => this._flush({ keepalive: true }));
|
|
@@ -65,6 +130,7 @@ var EventTracker = class {
|
|
|
65
130
|
this._started = true;
|
|
66
131
|
this._setupPageViewCapture();
|
|
67
132
|
this._setupClickCapture();
|
|
133
|
+
this._setupDeadClickDetection();
|
|
68
134
|
this._setupPageHideListeners();
|
|
69
135
|
this._flushTimer = setInterval(() => this._tick(), FLUSH_INTERVAL_MS);
|
|
70
136
|
}
|
|
@@ -80,6 +146,7 @@ var EventTracker = class {
|
|
|
80
146
|
clearBuffer() {
|
|
81
147
|
this._events = [];
|
|
82
148
|
this._approxBytes = 0;
|
|
149
|
+
this._unclassifiedClicks.clear();
|
|
83
150
|
}
|
|
84
151
|
_pushEvent(event) {
|
|
85
152
|
this._events.push(event);
|
|
@@ -131,19 +198,36 @@ var EventTracker = class {
|
|
|
131
198
|
const parts = [];
|
|
132
199
|
let current = element;
|
|
133
200
|
let depth = 0;
|
|
134
|
-
while (current && depth <
|
|
201
|
+
while (current && depth < 8 && current !== document.documentElement) {
|
|
135
202
|
let part = current.tagName.toLowerCase();
|
|
136
|
-
|
|
137
|
-
|
|
203
|
+
let testIdAttr = "data-testid";
|
|
204
|
+
let testId = current.getAttribute("data-testid");
|
|
205
|
+
if (testId == null) {
|
|
206
|
+
testIdAttr = "data-test-id";
|
|
207
|
+
testId = current.getAttribute("data-test-id");
|
|
208
|
+
}
|
|
209
|
+
if (testId != null && testId.trim() !== "") {
|
|
210
|
+
part += `[${testIdAttr}="${testId.replace(/"/g, "\\\"")}"]`;
|
|
211
|
+
parts.unshift(part);
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
if (current.id !== "") {
|
|
215
|
+
part += `#${(0, _hexclave_shared_dist_utils_dom.cssEscapeIdent)(current.id)}`;
|
|
138
216
|
parts.unshift(part);
|
|
139
217
|
break;
|
|
140
218
|
}
|
|
141
219
|
if (current.className && typeof current.className === "string") {
|
|
142
|
-
const classes = current.className.trim().split(/\s+/).filter(Boolean);
|
|
143
|
-
if (classes.length > 0) part += `.${classes.join(".")}`;
|
|
220
|
+
const classes = current.className.trim().split(/\s+/).filter(Boolean).slice(0, 4);
|
|
221
|
+
if (classes.length > 0) part += `.${classes.map(_hexclave_shared_dist_utils_dom.cssEscapeIdent).join(".")}`;
|
|
222
|
+
}
|
|
223
|
+
const parent = current.parentElement;
|
|
224
|
+
if (parent != null) {
|
|
225
|
+
const tagName = current.tagName;
|
|
226
|
+
const siblings = Array.from(parent.children).filter((child) => child.tagName === tagName);
|
|
227
|
+
if (siblings.length > 1) part += `:nth-of-type(${siblings.indexOf(current) + 1})`;
|
|
144
228
|
}
|
|
145
229
|
parts.unshift(part);
|
|
146
|
-
current =
|
|
230
|
+
current = parent;
|
|
147
231
|
depth++;
|
|
148
232
|
}
|
|
149
233
|
return parts.join(" > ");
|
|
@@ -159,6 +243,51 @@ var EventTracker = class {
|
|
|
159
243
|
_setupClickCapture() {
|
|
160
244
|
document.addEventListener("click", this._onClickCapture, { capture: true });
|
|
161
245
|
}
|
|
246
|
+
_setupDeadClickDetection() {
|
|
247
|
+
if (typeof MutationObserver !== "function") return;
|
|
248
|
+
this._deadClickMutationObserver = new MutationObserver((mutations) => {
|
|
249
|
+
if (mutations.every((mutation) => isInsideHexclaveUiNode(mutation.target))) return;
|
|
250
|
+
this._lastMutationAtMs = Date.now();
|
|
251
|
+
});
|
|
252
|
+
this._deadClickMutationObserver.observe(document.documentElement, {
|
|
253
|
+
childList: true,
|
|
254
|
+
attributes: true,
|
|
255
|
+
characterData: true,
|
|
256
|
+
subtree: true
|
|
257
|
+
});
|
|
258
|
+
document.addEventListener("scroll", this._onDeadClickScroll, {
|
|
259
|
+
capture: true,
|
|
260
|
+
passive: true
|
|
261
|
+
});
|
|
262
|
+
document.addEventListener("selectionchange", this._onDeadClickSelectionChange);
|
|
263
|
+
document.addEventListener("visibilitychange", this._onDeadClickVisibilityChange);
|
|
264
|
+
this._deadClickTimer = setInterval(() => this._checkDeadClicks(), DEAD_CLICK_CHECK_INTERVAL_MS);
|
|
265
|
+
}
|
|
266
|
+
_checkDeadClicks() {
|
|
267
|
+
const nowMs = Date.now();
|
|
268
|
+
for (const click of this._unclassifiedClicks) {
|
|
269
|
+
const signalWithin = (signalAtMs, thresholdMs) => signalAtMs != null && signalAtMs >= click.event_at_ms && signalAtMs - click.event_at_ms < thresholdMs;
|
|
270
|
+
if (signalWithin(this._lastScrollAtMs, DEAD_CLICK_SCROLL_THRESHOLD_MS) || signalWithin(this._lastSelectionChangedAtMs, DEAD_CLICK_SELECTION_CHANGED_THRESHOLD_MS) || signalWithin(this._lastVisibilityChangeAtMs, DEAD_CLICK_VISIBILITY_CHANGE_THRESHOLD_MS) || signalWithin(this._lastMutationAtMs, DEAD_CLICK_MUTATION_THRESHOLD_MS)) this._unclassifiedClicks.delete(click);
|
|
271
|
+
else if (nowMs - click.event_at_ms >= DEAD_CLICK_ABSOLUTE_TIMEOUT_MS) {
|
|
272
|
+
click.data.dead = 1;
|
|
273
|
+
this._unclassifiedClicks.delete(click);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
_teardownDeadClickDetection() {
|
|
278
|
+
if (this._deadClickTimer !== null) {
|
|
279
|
+
clearInterval(this._deadClickTimer);
|
|
280
|
+
this._deadClickTimer = null;
|
|
281
|
+
}
|
|
282
|
+
if (this._deadClickMutationObserver !== null) {
|
|
283
|
+
this._deadClickMutationObserver.disconnect();
|
|
284
|
+
this._deadClickMutationObserver = null;
|
|
285
|
+
}
|
|
286
|
+
document.removeEventListener("scroll", this._onDeadClickScroll, { capture: true });
|
|
287
|
+
document.removeEventListener("selectionchange", this._onDeadClickSelectionChange);
|
|
288
|
+
document.removeEventListener("visibilitychange", this._onDeadClickVisibilityChange);
|
|
289
|
+
this._unclassifiedClicks.clear();
|
|
290
|
+
}
|
|
162
291
|
_setupPageHideListeners() {
|
|
163
292
|
window.addEventListener("pagehide", this._onPageHide);
|
|
164
293
|
document.addEventListener("visibilitychange", this._onPageHide);
|
|
@@ -181,21 +310,24 @@ var EventTracker = class {
|
|
|
181
310
|
this._originalReplaceState = null;
|
|
182
311
|
window.removeEventListener("popstate", this._onPopState);
|
|
183
312
|
document.removeEventListener("click", this._onClickCapture, { capture: true });
|
|
313
|
+
this._teardownDeadClickDetection();
|
|
184
314
|
this._events = [];
|
|
185
315
|
this._approxBytes = 0;
|
|
186
316
|
}
|
|
187
317
|
async _flush(options) {
|
|
188
|
-
if (this.
|
|
318
|
+
if (options.keepalive) this._unclassifiedClicks.clear();
|
|
319
|
+
const events = this._events.filter((event) => !this._unclassifiedClicks.has(event));
|
|
320
|
+
if (events.length === 0) return;
|
|
321
|
+
this._events = this._events.filter((event) => this._unclassifiedClicks.has(event));
|
|
322
|
+
this._approxBytes = this._events.reduce((total, event) => total + JSON.stringify(event).length, 0);
|
|
189
323
|
const nowMs = Date.now();
|
|
190
324
|
const batchId = (0, __session_replay_js.generateUuid)();
|
|
191
325
|
const payload = {
|
|
192
326
|
session_replay_segment_id: this._sessionReplaySegmentId,
|
|
193
327
|
batch_id: batchId,
|
|
194
328
|
sent_at_ms: nowMs,
|
|
195
|
-
events
|
|
329
|
+
events
|
|
196
330
|
};
|
|
197
|
-
this._events = [];
|
|
198
|
-
this._approxBytes = 0;
|
|
199
331
|
const res = await this._deps.sendBatch(JSON.stringify(payload), { keepalive: options.keepalive });
|
|
200
332
|
if (res.status === "error") {
|
|
201
333
|
console.warn("EventTracker flush failed:", res.error);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event-tracker.js","names":[],"sources":["../../../../../src/lib/hexclave-app/apps/implementations/event-tracker.ts"],"sourcesContent":["\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template\n//===========================================\nimport { isBrowserLike } from \"@hexclave/shared/dist/utils/env\";\nimport { runAsynchronously } from \"@hexclave/shared/dist/utils/promises\";\nimport { Result } from \"@hexclave/shared/dist/utils/results\";\nimport { generateUuid } from \"./session-replay\";\n\nconst FLUSH_INTERVAL_MS = 10_000;\nconst MAX_EVENTS_PER_BATCH = 50;\nconst MAX_APPROX_BYTES_PER_BATCH = 64_000;\n\nfunction hasScreenDimensions(value: unknown): value is { width: number, height: number } {\n if (value == null || typeof value !== \"object\") {\n return false;\n }\n if (!(\"width\" in value) || !(\"height\" in value)) {\n return false;\n }\n return typeof value.width === \"number\" && typeof value.height === \"number\";\n}\n\nfunction hasHistoryMethods(value: unknown): value is { pushState: History[\"pushState\"], replaceState: History[\"replaceState\"] } {\n if (value == null || typeof value !== \"object\") {\n return false;\n }\n if (!(\"pushState\" in value) || !(\"replaceState\" in value)) {\n return false;\n }\n return typeof value.pushState === \"function\" && typeof value.replaceState === \"function\";\n}\n\nexport type EventTrackerDeps = {\n projectId: string,\n sendBatch: (body: string, options: { keepalive: boolean }) => Promise<Result<Response, Error>>,\n};\n\ntype TrackedEvent = {\n event_type: \"$page-view\" | \"$click\",\n event_at_ms: number,\n data: Record<string, unknown>,\n};\n\nexport class EventTracker {\n private _started = false;\n private _cancelled = false;\n private _detachListeners: (() => void) | null = null;\n private _flushTimer: ReturnType<typeof setInterval> | null = null;\n private _events: TrackedEvent[] = [];\n private _approxBytes = 0;\n private _lastUrl: string | null = null;\n private readonly _sessionReplaySegmentId: string;\n private readonly _deps: EventTrackerDeps;\n\n private _originalPushState: History[\"pushState\"] | null = null;\n private _originalReplaceState: History[\"replaceState\"] | null = null;\n\n constructor(deps: EventTrackerDeps) {\n this._deps = deps;\n this._sessionReplaySegmentId = generateUuid();\n }\n\n start() {\n if (this._started) return;\n if (!isBrowserLike()) return;\n if (\n typeof window.addEventListener !== \"function\"\n || typeof window.removeEventListener !== \"function\"\n || typeof document.addEventListener !== \"function\"\n || typeof document.removeEventListener !== \"function\"\n || !hasScreenDimensions(window.screen)\n ) {\n return;\n }\n this._started = true;\n\n this._setupPageViewCapture();\n this._setupClickCapture();\n this._setupPageHideListeners();\n\n this._flushTimer = setInterval(() => this._tick(), FLUSH_INTERVAL_MS);\n }\n\n stop() {\n this._cancelled = true;\n if (this._flushTimer !== null) {\n clearInterval(this._flushTimer);\n this._flushTimer = null;\n }\n runAsynchronously(() => this._flush({ keepalive: true }));\n this._teardown();\n }\n\n clearBuffer() {\n this._events = [];\n this._approxBytes = 0;\n }\n\n private _pushEvent(event: TrackedEvent) {\n this._events.push(event);\n this._approxBytes += JSON.stringify(event).length;\n if (this._events.length >= MAX_EVENTS_PER_BATCH || this._approxBytes >= MAX_APPROX_BYTES_PER_BATCH) {\n runAsynchronously(() => this._flush({ keepalive: false }));\n }\n }\n\n private _capturePageView(entryType: \"initial\" | \"push\" | \"replace\" | \"pop\") {\n const screenObject = window.screen;\n if (!hasScreenDimensions(screenObject)) {\n return;\n }\n\n const url = window.location.href;\n if (url === this._lastUrl && entryType !== \"initial\") return;\n this._lastUrl = url;\n\n this._pushEvent({\n event_type: \"$page-view\",\n event_at_ms: Date.now(),\n data: {\n url,\n path: window.location.pathname,\n referrer: document.referrer,\n title: document.title,\n entry_type: entryType,\n viewport_width: window.innerWidth,\n viewport_height: window.innerHeight,\n screen_width: screenObject.width,\n screen_height: screenObject.height,\n user_agent: typeof navigator !== \"undefined\" ? navigator.userAgent : null,\n },\n });\n }\n\n private _setupPageViewCapture() {\n // Fire initial page-view\n this._capturePageView(\"initial\");\n const historyObject = window.history;\n if (!hasHistoryMethods(historyObject)) {\n return;\n }\n const originalPushState = historyObject.pushState;\n const originalReplaceState = historyObject.replaceState;\n\n // Monkey-patch history.pushState\n this._originalPushState = (...args: Parameters<History[\"pushState\"]>) => originalPushState.apply(historyObject, args);\n historyObject.pushState = (...args: Parameters<History[\"pushState\"]>) => {\n this._originalPushState!(...args);\n this._capturePageView(\"push\");\n };\n\n // Monkey-patch history.replaceState\n this._originalReplaceState = (...args: Parameters<History[\"replaceState\"]>) => originalReplaceState.apply(historyObject, args);\n historyObject.replaceState = (...args: Parameters<History[\"replaceState\"]>) => {\n this._originalReplaceState!(...args);\n this._capturePageView(\"replace\");\n };\n\n // Listen for popstate (back/forward navigation)\n window.addEventListener(\"popstate\", this._onPopState);\n }\n\n private readonly _onPopState = () => {\n this._capturePageView(\"pop\");\n };\n\n private _buildSelector(element: Element): string {\n const parts: string[] = [];\n let current: Element | null = element;\n let depth = 0;\n\n while (current && depth < 5) {\n let part = current.tagName.toLowerCase();\n if (current.id) {\n part += `#${current.id}`;\n parts.unshift(part);\n break;\n }\n if (current.className && typeof current.className === \"string\") {\n const classes = current.className.trim().split(/\\s+/).filter(Boolean);\n if (classes.length > 0) {\n part += `.${classes.join(\".\")}`;\n }\n }\n parts.unshift(part);\n current = current.parentElement;\n depth++;\n }\n\n return parts.join(\" > \");\n }\n\n private _findNearestAnchorHref(element: Element): string | null {\n let current: Element | null = element;\n while (current) {\n if (current.tagName === \"A\" && current.hasAttribute(\"href\")) {\n return current.getAttribute(\"href\");\n }\n current = current.parentElement;\n }\n return null;\n }\n\n private readonly _onClickCapture = (event: MouseEvent) => {\n const target = event.target;\n if (!(target instanceof Element)) return;\n\n this._pushEvent({\n event_type: \"$click\",\n event_at_ms: Date.now(),\n data: {\n tag_name: target.tagName.toLowerCase(),\n text: target.textContent.trim().substring(0, 200),\n href: this._findNearestAnchorHref(target),\n selector: this._buildSelector(target),\n x: event.clientX,\n y: event.clientY,\n page_x: event.pageX,\n page_y: event.pageY,\n viewport_width: window.innerWidth,\n viewport_height: window.innerHeight,\n },\n });\n };\n\n private _setupClickCapture() {\n document.addEventListener(\"click\", this._onClickCapture, { capture: true });\n }\n\n private readonly _onPageHide = () => {\n runAsynchronously(() => this._flush({ keepalive: true }));\n };\n\n private _setupPageHideListeners() {\n window.addEventListener(\"pagehide\", this._onPageHide);\n document.addEventListener(\"visibilitychange\", this._onPageHide);\n this._detachListeners = () => {\n window.removeEventListener(\"pagehide\", this._onPageHide);\n document.removeEventListener(\"visibilitychange\", this._onPageHide);\n };\n }\n\n private _teardown() {\n if (this._detachListeners) {\n this._detachListeners();\n this._detachListeners = null;\n }\n\n // Restore history methods\n const historyObject = window.history;\n if (hasHistoryMethods(historyObject)) {\n if (this._originalPushState) {\n historyObject.pushState = this._originalPushState;\n }\n if (this._originalReplaceState) {\n historyObject.replaceState = this._originalReplaceState;\n }\n }\n this._originalPushState = null;\n this._originalReplaceState = null;\n\n window.removeEventListener(\"popstate\", this._onPopState);\n document.removeEventListener(\"click\", this._onClickCapture, { capture: true });\n\n this._events = [];\n this._approxBytes = 0;\n }\n\n private async _flush(options: { keepalive: boolean }) {\n if (this._events.length === 0) return;\n\n const nowMs = Date.now();\n\n const batchId = generateUuid();\n const payload = {\n session_replay_segment_id: this._sessionReplaySegmentId,\n batch_id: batchId,\n sent_at_ms: nowMs,\n events: this._events,\n };\n\n this._events = [];\n this._approxBytes = 0;\n\n const res = await this._deps.sendBatch(\n JSON.stringify(payload),\n { keepalive: options.keepalive },\n );\n\n if (res.status === \"error\") {\n console.warn(\"EventTracker flush failed:\", res.error);\n return;\n }\n\n if (!res.data.ok) {\n console.warn(\"EventTracker flush failed:\", res.data.status, await res.data.text());\n }\n }\n\n private _tick() {\n if (this._cancelled) return;\n if (this._events.length > 0) {\n runAsynchronously(() => this._flush({ keepalive: false }));\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"event-tracker.js","names":["ELEMENTS_CHAIN_MAX_DEPTH","DEV_TOOL_ROOT_ID","CLICKMAP_ROOT_ID","cssEscapeIdent"],"sources":["../../../../../src/lib/hexclave-app/apps/implementations/event-tracker.ts"],"sourcesContent":["\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template\n//===========================================\nimport { isBrowserLike } from \"@hexclave/shared/dist/utils/env\";\nimport { CLICKMAP_ROOT_ID, DEV_TOOL_ROOT_ID } from \"@hexclave/shared/dist/utils/dev-tool\";\nimport { cssEscapeIdent } from \"@hexclave/shared/dist/utils/dom\";\nimport { buildElementsChain, ELEMENTS_CHAIN_MAX_DEPTH } from \"@hexclave/shared/dist/utils/elements-chain\";\nimport { runAsynchronously } from \"@hexclave/shared/dist/utils/promises\";\nimport { Result } from \"@hexclave/shared/dist/utils/results\";\nimport { generateUuid } from \"./session-replay\";\n\nconst FLUSH_INTERVAL_MS = 10_000;\nconst MAX_EVENTS_PER_BATCH = 50;\nconst MAX_APPROX_BYTES_PER_BATCH = 64_000;\n\nfunction hasScreenDimensions(value: unknown): value is { width: number, height: number } {\n if (value == null || typeof value !== \"object\") {\n return false;\n }\n if (!(\"width\" in value) || !(\"height\" in value)) {\n return false;\n }\n return typeof value.width === \"number\" && typeof value.height === \"number\";\n}\n\nfunction hasHistoryMethods(value: unknown): value is { pushState: History[\"pushState\"], replaceState: History[\"replaceState\"] } {\n if (value == null || typeof value !== \"object\") {\n return false;\n }\n if (!(\"pushState\" in value) || !(\"replaceState\" in value)) {\n return false;\n }\n return typeof value.pushState === \"function\" && typeof value.replaceState === \"function\";\n}\n\n// Pixel quantization factor for x/y/viewport in stored click events. Matches the\n// SCALE_FACTOR used by the ClickHouse clickmap_events MV — keep them in sync.\nconst CLICKMAP_SCALE_FACTOR = 16;\n\n// Dead-click detection (PostHog-style). Whether an element has a click handler\n// is unknowable from page script, so a click is classified by its observable\n// consequences instead: it is \"alive\" if the page scrolled, the text selection\n// changed, or the tab visibility changed (a new tab opened) almost\n// immediately, or if the DOM mutated within a couple of seconds — and \"dead\"\n// if none of that happened by the absolute timeout.\n//\n// The $click event is buffered immediately like any other event (so\n// event_at_ms, ordering, and every query are untouched) and the sweep sets\n// data.dead=1 on it in place if nothing observable happened. _flush holds\n// back clicks that are still unclassified — classification always finishes\n// well within one FLUSH_INTERVAL_MS, so a held click rides the next flush at\n// the latest. A keepalive flush (pagehide/stop) sends them unmarked: a click\n// still pending when the page unloads led to that navigation, alive by\n// definition.\n//\n// NOTE — blocker for any future real-time / \"live clicks\" view: a click that\n// is still unclassified when its natural flush fires arrives up to one extra\n// FLUSH_INTERVAL_MS late. A surface showing clicks as they happen must either\n// accept that lag or emit a provisional $click plus a later dead-click\n// reconciliation event.\nconst DEAD_CLICK_SCROLL_THRESHOLD_MS = 100;\nconst DEAD_CLICK_SELECTION_CHANGED_THRESHOLD_MS = 100;\nconst DEAD_CLICK_VISIBILITY_CHANGE_THRESHOLD_MS = 100;\nconst DEAD_CLICK_MUTATION_THRESHOLD_MS = 2_500;\n// 1.1x the mutation threshold, mirroring posthog-js: every signal window has\n// closed before a click is declared dead.\nconst DEAD_CLICK_ABSOLUTE_TIMEOUT_MS = 2_750;\nconst DEAD_CLICK_CHECK_INTERVAL_MS = 1_000;\n// Backstop against click storms (e.g. rage clicks on a dead element): past the\n// cap, clicks are simply not classified rather than not recorded.\nconst DEAD_CLICK_MAX_PENDING = 50;\n\nfunction isPointerTargetFixed(element: Element): boolean {\n let current: Element | null = element;\n let depth = 0;\n while (current != null && depth < ELEMENTS_CHAIN_MAX_DEPTH * 2) {\n const style = window.getComputedStyle(current);\n if (style.position === \"fixed\" || style.position === \"sticky\") {\n return true;\n }\n current = current.parentElement;\n depth += 1;\n }\n return false;\n}\n\n// Clicks on Hexclave's own in-page UI (the dev tool and the standalone\n// clickmap overlay) must never be ingested as analytics events.\nfunction isInsideHexclaveUi(element: Element): boolean {\n return element.closest(`#${cssEscapeIdent(DEV_TOOL_ROOT_ID)}, #${cssEscapeIdent(CLICKMAP_ROOT_ID)}`) != null;\n}\n\n// Mutation-record targets can be text/comment nodes; resolve to the nearest\n// element before asking whether the mutation came from Hexclave's own UI.\nfunction isInsideHexclaveUiNode(node: Node | null): boolean {\n const element = node instanceof Element ? node : node?.parentElement ?? null;\n return element != null && isInsideHexclaveUi(element);\n}\n\nexport type EventTrackerDeps = {\n projectId: string,\n sendBatch: (body: string, options: { keepalive: boolean }) => Promise<Result<Response, Error>>,\n};\n\ntype TrackedEvent = {\n event_type: \"$page-view\" | \"$click\",\n event_at_ms: number,\n data: Record<string, unknown>,\n};\n\nexport class EventTracker {\n private _started = false;\n private _cancelled = false;\n private _detachListeners: (() => void) | null = null;\n private _flushTimer: ReturnType<typeof setInterval> | null = null;\n private _events: TrackedEvent[] = [];\n private _approxBytes = 0;\n private _lastUrl: string | null = null;\n private readonly _sessionReplaySegmentId: string;\n private readonly _deps: EventTrackerDeps;\n\n private _originalPushState: History[\"pushState\"] | null = null;\n private _originalReplaceState: History[\"replaceState\"] | null = null;\n\n private _deadClickTimer: ReturnType<typeof setInterval> | null = null;\n private _deadClickMutationObserver: MutationObserver | null = null;\n // Buffered $click events still awaiting dead-click classification. Always a\n // subset of _events — _flush holds these back until the sweep resolves them.\n private _unclassifiedClicks = new Set<TrackedEvent>();\n private _lastMutationAtMs: number | null = null;\n private _lastScrollAtMs: number | null = null;\n private _lastSelectionChangedAtMs: number | null = null;\n private _lastVisibilityChangeAtMs: number | null = null;\n\n constructor(deps: EventTrackerDeps) {\n this._deps = deps;\n this._sessionReplaySegmentId = generateUuid();\n }\n\n start() {\n if (this._started) return;\n if (!isBrowserLike()) return;\n if (\n typeof window.addEventListener !== \"function\"\n || typeof window.removeEventListener !== \"function\"\n || typeof document.addEventListener !== \"function\"\n || typeof document.removeEventListener !== \"function\"\n || !hasScreenDimensions(window.screen)\n ) {\n return;\n }\n this._started = true;\n\n this._setupPageViewCapture();\n this._setupClickCapture();\n this._setupDeadClickDetection();\n this._setupPageHideListeners();\n\n this._flushTimer = setInterval(() => this._tick(), FLUSH_INTERVAL_MS);\n }\n\n stop() {\n this._cancelled = true;\n if (this._flushTimer !== null) {\n clearInterval(this._flushTimer);\n this._flushTimer = null;\n }\n runAsynchronously(() => this._flush({ keepalive: true }));\n this._teardown();\n }\n\n clearBuffer() {\n this._events = [];\n this._approxBytes = 0;\n this._unclassifiedClicks.clear();\n }\n\n private _pushEvent(event: TrackedEvent) {\n this._events.push(event);\n this._approxBytes += JSON.stringify(event).length;\n if (this._events.length >= MAX_EVENTS_PER_BATCH || this._approxBytes >= MAX_APPROX_BYTES_PER_BATCH) {\n runAsynchronously(() => this._flush({ keepalive: false }));\n }\n }\n\n private _capturePageView(entryType: \"initial\" | \"push\" | \"replace\" | \"pop\") {\n const screenObject = window.screen;\n if (!hasScreenDimensions(screenObject)) {\n return;\n }\n\n const url = window.location.href;\n if (url === this._lastUrl && entryType !== \"initial\") return;\n this._lastUrl = url;\n\n this._pushEvent({\n event_type: \"$page-view\",\n event_at_ms: Date.now(),\n data: {\n url,\n path: window.location.pathname,\n referrer: document.referrer,\n title: document.title,\n entry_type: entryType,\n viewport_width: window.innerWidth,\n viewport_height: window.innerHeight,\n screen_width: screenObject.width,\n screen_height: screenObject.height,\n user_agent: typeof navigator !== \"undefined\" ? navigator.userAgent : null,\n },\n });\n }\n\n private _setupPageViewCapture() {\n // Fire initial page-view\n this._capturePageView(\"initial\");\n const historyObject = window.history;\n if (!hasHistoryMethods(historyObject)) {\n return;\n }\n const originalPushState = historyObject.pushState;\n const originalReplaceState = historyObject.replaceState;\n\n // Monkey-patch history.pushState\n this._originalPushState = (...args: Parameters<History[\"pushState\"]>) => originalPushState.apply(historyObject, args);\n historyObject.pushState = (...args: Parameters<History[\"pushState\"]>) => {\n this._originalPushState!(...args);\n this._capturePageView(\"push\");\n };\n\n // Monkey-patch history.replaceState\n this._originalReplaceState = (...args: Parameters<History[\"replaceState\"]>) => originalReplaceState.apply(historyObject, args);\n historyObject.replaceState = (...args: Parameters<History[\"replaceState\"]>) => {\n this._originalReplaceState!(...args);\n this._capturePageView(\"replace\");\n };\n\n // Listen for popstate (back/forward navigation)\n window.addEventListener(\"popstate\", this._onPopState);\n }\n\n private readonly _onPopState = () => {\n this._capturePageView(\"pop\");\n };\n\n private _buildSelector(element: Element): string {\n const parts: string[] = [];\n let current: Element | null = element;\n let depth = 0;\n\n while (current && depth < 8 && current !== document.documentElement) {\n let part = current.tagName.toLowerCase();\n let testIdAttr = \"data-testid\";\n let testId = current.getAttribute(\"data-testid\");\n if (testId == null) {\n testIdAttr = \"data-test-id\";\n testId = current.getAttribute(\"data-test-id\");\n }\n if (testId != null && testId.trim() !== \"\") {\n part += `[${testIdAttr}=\"${testId.replace(/\"/g, '\\\\\"')}\"]`;\n parts.unshift(part);\n break;\n }\n if (current.id !== \"\") {\n part += `#${cssEscapeIdent(current.id)}`;\n parts.unshift(part);\n break;\n }\n if (current.className && typeof current.className === \"string\") {\n const classes = current.className.trim().split(/\\s+/).filter(Boolean).slice(0, 4);\n if (classes.length > 0) {\n part += `.${classes.map(cssEscapeIdent).join(\".\")}`;\n }\n }\n const parent: Element | null = current.parentElement;\n if (parent != null) {\n const tagName = current.tagName;\n const siblings = Array.from(parent.children).filter((child) => child.tagName === tagName);\n if (siblings.length > 1) {\n part += `:nth-of-type(${siblings.indexOf(current) + 1})`;\n }\n }\n parts.unshift(part);\n current = parent;\n depth++;\n }\n\n return parts.join(\" > \");\n }\n\n private _findNearestAnchorHref(element: Element): string | null {\n let current: Element | null = element;\n while (current) {\n if (current.tagName === \"A\" && current.hasAttribute(\"href\")) {\n return current.getAttribute(\"href\");\n }\n current = current.parentElement;\n }\n return null;\n }\n\n private readonly _onClickCapture = (event: MouseEvent) => {\n const target = event.target;\n if (!(target instanceof Element)) return;\n if (isInsideHexclaveUi(target)) return;\n\n const viewportWidth = window.innerWidth;\n const viewportHeight = window.innerHeight;\n const pointerTargetFixed = isPointerTargetFixed(target);\n // Pre-scale at ingest so old + new rows land in identical buckets in CH.\n const xScaled = Math.round(event.pageX / CLICKMAP_SCALE_FACTOR);\n const yScaled = Math.round(event.pageY / CLICKMAP_SCALE_FACTOR);\n const clientYScaled = Math.round(event.clientY / CLICKMAP_SCALE_FACTOR);\n const relativeX = viewportWidth > 0 ? event.clientX / viewportWidth : 0;\n\n const clickEvent: TrackedEvent = {\n event_type: \"$click\",\n event_at_ms: Date.now(),\n data: {\n tag_name: target.tagName.toLowerCase(),\n text: target.textContent.trim().substring(0, 200),\n href: this._findNearestAnchorHref(target),\n selector: this._buildSelector(target),\n elements_chain: buildElementsChain(target),\n pointer_target_fixed: pointerTargetFixed ? 1 : 0,\n url: window.location.href,\n path: window.location.pathname,\n title: document.title,\n x: event.clientX,\n y: event.clientY,\n page_x: event.pageX,\n page_y: event.pageY,\n x_scaled: xScaled,\n y_scaled: yScaled,\n client_y_scaled: clientYScaled,\n pointer_relative_x: relativeX,\n viewport_width: viewportWidth,\n viewport_height: viewportHeight,\n scale_factor: CLICKMAP_SCALE_FACTOR,\n },\n };\n\n // Register for dead-click classification before buffering, so a\n // size-triggered flush from this very push already holds the click back.\n if (this._deadClickTimer !== null && this._unclassifiedClicks.size < DEAD_CLICK_MAX_PENDING) {\n this._unclassifiedClicks.add(clickEvent);\n }\n this._pushEvent(clickEvent);\n };\n\n private _setupClickCapture() {\n document.addEventListener(\"click\", this._onClickCapture, { capture: true });\n }\n\n private readonly _onDeadClickScroll = () => {\n this._lastScrollAtMs = Date.now();\n };\n\n private readonly _onDeadClickSelectionChange = () => {\n this._lastSelectionChangedAtMs = Date.now();\n };\n\n private readonly _onDeadClickVisibilityChange = () => {\n this._lastVisibilityChangeAtMs = Date.now();\n };\n\n private _setupDeadClickDetection() {\n if (typeof MutationObserver !== \"function\") return;\n\n this._deadClickMutationObserver = new MutationObserver((mutations) => {\n // The dev tool and the clickmap overlay rewrite their own DOM constantly\n // while open; their mutations must not mark host-page clicks as alive.\n if (mutations.every((mutation) => isInsideHexclaveUiNode(mutation.target))) {\n return;\n }\n this._lastMutationAtMs = Date.now();\n });\n this._deadClickMutationObserver.observe(document.documentElement, {\n childList: true,\n attributes: true,\n characterData: true,\n subtree: true,\n });\n\n // Capture phase so scrolls inside nested scroll containers count, not just\n // the document itself (scroll events don't bubble).\n document.addEventListener(\"scroll\", this._onDeadClickScroll, { capture: true, passive: true });\n document.addEventListener(\"selectionchange\", this._onDeadClickSelectionChange);\n document.addEventListener(\"visibilitychange\", this._onDeadClickVisibilityChange);\n\n this._deadClickTimer = setInterval(() => this._checkDeadClicks(), DEAD_CLICK_CHECK_INTERVAL_MS);\n }\n\n private _checkDeadClicks() {\n const nowMs = Date.now();\n for (const click of this._unclassifiedClicks) {\n const signalWithin = (signalAtMs: number | null, thresholdMs: number) =>\n signalAtMs != null && signalAtMs >= click.event_at_ms && signalAtMs - click.event_at_ms < thresholdMs;\n\n const isAlive = signalWithin(this._lastScrollAtMs, DEAD_CLICK_SCROLL_THRESHOLD_MS)\n || signalWithin(this._lastSelectionChangedAtMs, DEAD_CLICK_SELECTION_CHANGED_THRESHOLD_MS)\n || signalWithin(this._lastVisibilityChangeAtMs, DEAD_CLICK_VISIBILITY_CHANGE_THRESHOLD_MS)\n || signalWithin(this._lastMutationAtMs, DEAD_CLICK_MUTATION_THRESHOLD_MS);\n if (isAlive) {\n this._unclassifiedClicks.delete(click);\n } else if (nowMs - click.event_at_ms >= DEAD_CLICK_ABSOLUTE_TIMEOUT_MS) {\n // The already-buffered event is marked in place — no second event.\n click.data.dead = 1;\n this._unclassifiedClicks.delete(click);\n }\n }\n }\n\n private _teardownDeadClickDetection() {\n if (this._deadClickTimer !== null) {\n clearInterval(this._deadClickTimer);\n this._deadClickTimer = null;\n }\n if (this._deadClickMutationObserver !== null) {\n this._deadClickMutationObserver.disconnect();\n this._deadClickMutationObserver = null;\n }\n document.removeEventListener(\"scroll\", this._onDeadClickScroll, { capture: true });\n document.removeEventListener(\"selectionchange\", this._onDeadClickSelectionChange);\n document.removeEventListener(\"visibilitychange\", this._onDeadClickVisibilityChange);\n this._unclassifiedClicks.clear();\n }\n\n private readonly _onPageHide = () => {\n runAsynchronously(() => this._flush({ keepalive: true }));\n };\n\n private _setupPageHideListeners() {\n window.addEventListener(\"pagehide\", this._onPageHide);\n document.addEventListener(\"visibilitychange\", this._onPageHide);\n this._detachListeners = () => {\n window.removeEventListener(\"pagehide\", this._onPageHide);\n document.removeEventListener(\"visibilitychange\", this._onPageHide);\n };\n }\n\n private _teardown() {\n if (this._detachListeners) {\n this._detachListeners();\n this._detachListeners = null;\n }\n\n // Restore history methods\n const historyObject = window.history;\n if (hasHistoryMethods(historyObject)) {\n if (this._originalPushState) {\n historyObject.pushState = this._originalPushState;\n }\n if (this._originalReplaceState) {\n historyObject.replaceState = this._originalReplaceState;\n }\n }\n this._originalPushState = null;\n this._originalReplaceState = null;\n\n window.removeEventListener(\"popstate\", this._onPopState);\n document.removeEventListener(\"click\", this._onClickCapture, { capture: true });\n this._teardownDeadClickDetection();\n\n this._events = [];\n this._approxBytes = 0;\n }\n\n private async _flush(options: { keepalive: boolean }) {\n // A keepalive flush means the page is unloading — a click still awaiting\n // dead-click classification led to that unload, so it is alive by\n // definition and ships unmarked.\n if (options.keepalive) {\n this._unclassifiedClicks.clear();\n }\n\n // Clicks still awaiting classification stay buffered so the sweep can\n // mark them dead in place; classification finishes well within one flush\n // interval, so they ride the next flush at the latest.\n const events = this._events.filter((event) => !this._unclassifiedClicks.has(event));\n if (events.length === 0) return;\n this._events = this._events.filter((event) => this._unclassifiedClicks.has(event));\n this._approxBytes = this._events.reduce((total, event) => total + JSON.stringify(event).length, 0);\n\n const nowMs = Date.now();\n\n const batchId = generateUuid();\n const payload = {\n session_replay_segment_id: this._sessionReplaySegmentId,\n batch_id: batchId,\n sent_at_ms: nowMs,\n events,\n };\n\n const res = await this._deps.sendBatch(\n JSON.stringify(payload),\n { keepalive: options.keepalive },\n );\n\n if (res.status === \"error\") {\n console.warn(\"EventTracker flush failed:\", res.error);\n return;\n }\n\n if (!res.data.ok) {\n console.warn(\"EventTracker flush failed:\", res.data.status, await res.data.text());\n }\n }\n\n private _tick() {\n if (this._cancelled) return;\n if (this._events.length > 0) {\n runAsynchronously(() => this._flush({ keepalive: false }));\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -54,6 +54,227 @@ function getSentEventTypes(sentBodies) {
|
|
|
54
54
|
tracker.stop();
|
|
55
55
|
}
|
|
56
56
|
});
|
|
57
|
+
(0, vitest.it)("emits a PostHog-style elements_chain plus scaled pointer coords for $click", async () => {
|
|
58
|
+
vitest.vi.useFakeTimers();
|
|
59
|
+
document.body.innerHTML = `
|
|
60
|
+
<main>
|
|
61
|
+
<section class="card panel">
|
|
62
|
+
<button id="save-btn" data-testid="save" aria-label="Save project">Save changes</button>
|
|
63
|
+
</section>
|
|
64
|
+
</main>
|
|
65
|
+
`;
|
|
66
|
+
const sentBodies = [];
|
|
67
|
+
const tracker = new __event_tracker_js.EventTracker({
|
|
68
|
+
projectId: "internal",
|
|
69
|
+
sendBatch: async (body) => {
|
|
70
|
+
sentBodies.push(body);
|
|
71
|
+
return _hexclave_shared_dist_utils_results.Result.ok(new Response());
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
try {
|
|
75
|
+
tracker.start();
|
|
76
|
+
const button = document.querySelector("#save-btn");
|
|
77
|
+
if (button == null) throw new Error("button missing");
|
|
78
|
+
button.dispatchEvent(new MouseEvent("click", {
|
|
79
|
+
bubbles: true,
|
|
80
|
+
clientX: 100,
|
|
81
|
+
clientY: 200
|
|
82
|
+
}));
|
|
83
|
+
await advancePastFlush();
|
|
84
|
+
const click = JSON.parse(sentBodies[0] ?? "{}").events.find((event) => event.event_type === "$click");
|
|
85
|
+
if (click == null) throw new Error("no $click event captured");
|
|
86
|
+
const chain = click.data.elements_chain;
|
|
87
|
+
(0, vitest.expect)(typeof chain).toBe("string");
|
|
88
|
+
(0, vitest.expect)(chain).toContain("button");
|
|
89
|
+
(0, vitest.expect)(chain).toContain("attr__id=\"save-btn\"");
|
|
90
|
+
(0, vitest.expect)(chain).toContain("attr__data-testid=\"save\"");
|
|
91
|
+
(0, vitest.expect)(chain).toContain("attr__aria-label=\"Save project\"");
|
|
92
|
+
(0, vitest.expect)(chain).toContain("text=\"Save changes\"");
|
|
93
|
+
(0, vitest.expect)(chain).toContain("section");
|
|
94
|
+
(0, vitest.expect)(click.data.x_scaled).toBe(Math.round(100 / 16));
|
|
95
|
+
(0, vitest.expect)(click.data.y_scaled).toBe(Math.round(200 / 16));
|
|
96
|
+
(0, vitest.expect)(click.data.client_y_scaled).toBe(Math.round(200 / 16));
|
|
97
|
+
(0, vitest.expect)(click.data.scale_factor).toBe(16);
|
|
98
|
+
(0, vitest.expect)(click.data.pointer_relative_x).toBeCloseTo(100 / window.innerWidth, 4);
|
|
99
|
+
(0, vitest.expect)(click.data.pointer_target_fixed).toBe(0);
|
|
100
|
+
(0, vitest.expect)(click.data.selector).toContain("data-testid=\"save\"");
|
|
101
|
+
(0, vitest.expect)(click.data.tag_name).toBe("button");
|
|
102
|
+
} finally {
|
|
103
|
+
tracker.stop();
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
(0, vitest.it)("ignores clicks inside the Hexclave dev tool", async () => {
|
|
107
|
+
vitest.vi.useFakeTimers();
|
|
108
|
+
document.body.innerHTML = `
|
|
109
|
+
<div id="__hexclave-dev-tool-root">
|
|
110
|
+
<button>Clickmap toolbar control</button>
|
|
111
|
+
</div>
|
|
112
|
+
`;
|
|
113
|
+
const sentBodies = [];
|
|
114
|
+
const tracker = new __event_tracker_js.EventTracker({
|
|
115
|
+
projectId: "internal",
|
|
116
|
+
sendBatch: async (body) => {
|
|
117
|
+
sentBodies.push(body);
|
|
118
|
+
return _hexclave_shared_dist_utils_results.Result.ok(new Response());
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
try {
|
|
122
|
+
tracker.start();
|
|
123
|
+
document.querySelector("button")?.dispatchEvent(new MouseEvent("click", {
|
|
124
|
+
bubbles: true,
|
|
125
|
+
clientX: 100,
|
|
126
|
+
clientY: 200
|
|
127
|
+
}));
|
|
128
|
+
await advancePastFlush();
|
|
129
|
+
(0, vitest.expect)(getSentEventTypes(sentBodies)).toMatchInlineSnapshot(`
|
|
130
|
+
[
|
|
131
|
+
"$page-view",
|
|
132
|
+
]
|
|
133
|
+
`);
|
|
134
|
+
} finally {
|
|
135
|
+
tracker.stop();
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
(0, vitest.it)("flags pointer_target_fixed when the target sits under a fixed-position ancestor", async () => {
|
|
139
|
+
vitest.vi.useFakeTimers();
|
|
140
|
+
document.body.innerHTML = `
|
|
141
|
+
<header style="position: fixed; top: 0">
|
|
142
|
+
<button id="cta">Sign up</button>
|
|
143
|
+
</header>
|
|
144
|
+
`;
|
|
145
|
+
const sentBodies = [];
|
|
146
|
+
const tracker = new __event_tracker_js.EventTracker({
|
|
147
|
+
projectId: "internal",
|
|
148
|
+
sendBatch: async (body) => {
|
|
149
|
+
sentBodies.push(body);
|
|
150
|
+
return _hexclave_shared_dist_utils_results.Result.ok(new Response());
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
try {
|
|
154
|
+
tracker.start();
|
|
155
|
+
document.querySelector("#cta")?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
156
|
+
await advancePastFlush();
|
|
157
|
+
(0, vitest.expect)(JSON.parse(sentBodies[0] ?? "{}").events.find((event) => event.event_type === "$click")?.data.pointer_target_fixed).toBe(1);
|
|
158
|
+
} finally {
|
|
159
|
+
tracker.stop();
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
(0, vitest.it)("flags a click with no observable effect as dead on its single $click event", async () => {
|
|
163
|
+
vitest.vi.useFakeTimers();
|
|
164
|
+
document.body.innerHTML = "<button id=\"dead\">Does nothing</button>";
|
|
165
|
+
const sentBodies = [];
|
|
166
|
+
const tracker = new __event_tracker_js.EventTracker({
|
|
167
|
+
projectId: "internal",
|
|
168
|
+
sendBatch: async (body) => {
|
|
169
|
+
sentBodies.push(body);
|
|
170
|
+
return _hexclave_shared_dist_utils_results.Result.ok(new Response());
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
try {
|
|
174
|
+
tracker.start();
|
|
175
|
+
const clickAtMs = Date.now();
|
|
176
|
+
document.querySelector("#dead")?.dispatchEvent(new MouseEvent("click", {
|
|
177
|
+
bubbles: true,
|
|
178
|
+
clientX: 10,
|
|
179
|
+
clientY: 20
|
|
180
|
+
}));
|
|
181
|
+
await advancePastFlush();
|
|
182
|
+
const clicks = JSON.parse(sentBodies[0] ?? "{}").events.filter((event) => event.event_type === "$click");
|
|
183
|
+
(0, vitest.expect)(clicks).toHaveLength(1);
|
|
184
|
+
const click = clicks[0];
|
|
185
|
+
(0, vitest.expect)(click.data.dead).toBe(1);
|
|
186
|
+
(0, vitest.expect)(click.event_at_ms).toBe(clickAtMs);
|
|
187
|
+
} finally {
|
|
188
|
+
tracker.stop();
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
(0, vitest.it)("does not flag a click as dead when it mutates the DOM", async () => {
|
|
192
|
+
vitest.vi.useFakeTimers();
|
|
193
|
+
document.body.innerHTML = "<button id=\"live\">Adds content</button><div id=\"out\"></div>";
|
|
194
|
+
const sentBodies = [];
|
|
195
|
+
const tracker = new __event_tracker_js.EventTracker({
|
|
196
|
+
projectId: "internal",
|
|
197
|
+
sendBatch: async (body) => {
|
|
198
|
+
sentBodies.push(body);
|
|
199
|
+
return _hexclave_shared_dist_utils_results.Result.ok(new Response());
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
try {
|
|
203
|
+
tracker.start();
|
|
204
|
+
const button = document.querySelector("#live");
|
|
205
|
+
if (button == null) throw new Error("button missing");
|
|
206
|
+
button.addEventListener("click", () => {
|
|
207
|
+
document.querySelector("#out")?.appendChild(document.createElement("p"));
|
|
208
|
+
});
|
|
209
|
+
button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
210
|
+
await Promise.resolve();
|
|
211
|
+
await advancePastFlush();
|
|
212
|
+
const clicks = JSON.parse(sentBodies[0] ?? "{}").events.filter((event) => event.event_type === "$click");
|
|
213
|
+
(0, vitest.expect)(clicks).toHaveLength(1);
|
|
214
|
+
(0, vitest.expect)(clicks[0].data.dead).toBeUndefined();
|
|
215
|
+
} finally {
|
|
216
|
+
tracker.stop();
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
(0, vitest.it)("drains held clicks as alive on pagehide so navigation clicks are never lost", async () => {
|
|
220
|
+
vitest.vi.useFakeTimers();
|
|
221
|
+
document.body.innerHTML = "<a id=\"nav\" href=\"/pricing\">Pricing</a>";
|
|
222
|
+
const sentBodies = [];
|
|
223
|
+
const tracker = new __event_tracker_js.EventTracker({
|
|
224
|
+
projectId: "internal",
|
|
225
|
+
sendBatch: async (body) => {
|
|
226
|
+
sentBodies.push(body);
|
|
227
|
+
return _hexclave_shared_dist_utils_results.Result.ok(new Response());
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
try {
|
|
231
|
+
tracker.start();
|
|
232
|
+
const clickAtMs = Date.now();
|
|
233
|
+
document.querySelector("#nav")?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
234
|
+
window.dispatchEvent(new Event("pagehide"));
|
|
235
|
+
await Promise.resolve();
|
|
236
|
+
await Promise.resolve();
|
|
237
|
+
const clicks = JSON.parse(sentBodies[0] ?? "{}").events.filter((event) => event.event_type === "$click");
|
|
238
|
+
(0, vitest.expect)(clicks).toHaveLength(1);
|
|
239
|
+
(0, vitest.expect)(clicks[0].data.dead).toBeUndefined();
|
|
240
|
+
(0, vitest.expect)(clicks[0].event_at_ms).toBe(clickAtMs);
|
|
241
|
+
} finally {
|
|
242
|
+
tracker.stop();
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
(0, vitest.it)("holds an unclassified click out of a flush and ships it on the next one", async () => {
|
|
246
|
+
vitest.vi.useFakeTimers();
|
|
247
|
+
document.body.innerHTML = "<button id=\"late\">Late click</button>";
|
|
248
|
+
const sentBodies = [];
|
|
249
|
+
const tracker = new __event_tracker_js.EventTracker({
|
|
250
|
+
projectId: "internal",
|
|
251
|
+
sendBatch: async (body) => {
|
|
252
|
+
sentBodies.push(body);
|
|
253
|
+
return _hexclave_shared_dist_utils_results.Result.ok(new Response());
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
try {
|
|
257
|
+
tracker.start();
|
|
258
|
+
await vitest.vi.advanceTimersByTimeAsync(9500);
|
|
259
|
+
document.querySelector("#late")?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
260
|
+
await vitest.vi.advanceTimersByTimeAsync(500);
|
|
261
|
+
(0, vitest.expect)(getSentEventTypes(sentBodies)).toMatchInlineSnapshot(`
|
|
262
|
+
[
|
|
263
|
+
"$page-view",
|
|
264
|
+
]
|
|
265
|
+
`);
|
|
266
|
+
await vitest.vi.advanceTimersByTimeAsync(1e4);
|
|
267
|
+
const second = JSON.parse(sentBodies[1] ?? "{}");
|
|
268
|
+
(0, vitest.expect)(second.events.map((event) => event.event_type)).toMatchInlineSnapshot(`
|
|
269
|
+
[
|
|
270
|
+
"$click",
|
|
271
|
+
]
|
|
272
|
+
`);
|
|
273
|
+
(0, vitest.expect)(second.events[0].data.dead).toBe(1);
|
|
274
|
+
} finally {
|
|
275
|
+
tracker.stop();
|
|
276
|
+
}
|
|
277
|
+
});
|
|
57
278
|
(0, vitest.it)("captures client-side navigations when history is exposed as an accessor descriptor", async () => {
|
|
58
279
|
vitest.vi.useFakeTimers();
|
|
59
280
|
const historyDescriptor = Object.getOwnPropertyDescriptor(window, "history");
|