@hexclave/next 1.0.16 → 1.0.18
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/dev-tool/dev-tool-styles.js +7 -14
- package/dist/dev-tool/dev-tool-styles.js.map +1 -1
- package/dist/esm/dev-tool/dev-tool-styles.js +7 -14
- package/dist/esm/dev-tool/dev-tool-styles.js.map +1 -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/client-app-impl.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js +4 -3
- 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 +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.d.ts +1 -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 +15 -1
- 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 +31 -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/session-replay.d.ts +6 -4
- package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.js +21 -2
- package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.test.js +64 -2
- package/dist/esm/lib/hexclave-app/apps/implementations/session-replay.test.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.d.ts +2 -2
- package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.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 +3 -2
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/common.js +1 -1
- package/dist/lib/hexclave-app/apps/implementations/event-tracker.d.ts +1 -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 +15 -1
- package/dist/lib/hexclave-app/apps/implementations/event-tracker.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/event-tracker.test.js +31 -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/session-replay.d.ts +6 -4
- package/dist/lib/hexclave-app/apps/implementations/session-replay.d.ts.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/session-replay.js +21 -1
- package/dist/lib/hexclave-app/apps/implementations/session-replay.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/session-replay.test.js +62 -0
- package/dist/lib/hexclave-app/apps/implementations/session-replay.test.js.map +1 -1
- package/dist/lib/hexclave-app/apps/interfaces/client-app.d.ts +2 -2
- package/dist/lib/hexclave-app/apps/interfaces/client-app.js.map +1 -1
- package/package.json +4 -4
- package/src/dev-tool/dev-tool-styles.ts +7 -14
- package/src/lib/hexclave-app/apps/implementations/client-app-impl.ts +4 -3
- package/src/lib/hexclave-app/apps/implementations/event-tracker.test.ts +41 -0
- package/src/lib/hexclave-app/apps/implementations/event-tracker.ts +16 -0
- package/src/lib/hexclave-app/apps/implementations/session-replay.test.ts +85 -2
- package/src/lib/hexclave-app/apps/implementations/session-replay.ts +24 -3
- package/src/lib/hexclave-app/apps/interfaces/client-app.ts +2 -2
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
const require_chunk = require('../../../../chunk-BE-pF4vm.js');
|
|
2
2
|
let vitest = require("vitest");
|
|
3
|
+
let _hexclave_shared_dist_utils_results = require("@hexclave/shared/dist/utils/results");
|
|
3
4
|
let __session_replay_js = require("./session-replay.js");
|
|
4
5
|
|
|
5
6
|
//#region src/lib/hexclave-app/apps/implementations/session-replay.test.ts
|
|
7
|
+
// @vitest-environment jsdom
|
|
8
|
+
(0, vitest.describe)("session replay options", () => {
|
|
9
|
+
(0, vitest.it)("enables replays by default", () => {
|
|
10
|
+
(0, vitest.expect)((0, __session_replay_js.getSessionReplayOptions)(void 0).enabled).toBe(true);
|
|
11
|
+
(0, vitest.expect)((0, __session_replay_js.getSessionReplayOptions)({}).enabled).toBe(true);
|
|
12
|
+
(0, vitest.expect)((0, __session_replay_js.getSessionReplayOptions)({ replays: {} }).enabled).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
(0, vitest.it)("preserves explicit replay opt-out", () => {
|
|
15
|
+
(0, vitest.expect)((0, __session_replay_js.getSessionReplayOptions)({ replays: { enabled: false } }).enabled).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
6
18
|
(0, vitest.describe)("analytics option JSON conversion", () => {
|
|
7
19
|
(0, vitest.it)("preserves top-level analytics options when serializing replay block classes", () => {
|
|
8
20
|
const json = (0, __session_replay_js.analyticsOptionsToJson)({
|
|
@@ -24,6 +36,56 @@ let __session_replay_js = require("./session-replay.js");
|
|
|
24
36
|
(0, vitest.expect)(roundTripped?.replays?.blockClass).toEqual(/stack-sensitive/u);
|
|
25
37
|
});
|
|
26
38
|
});
|
|
39
|
+
(0, vitest.describe)("SessionRecorder flush", () => {
|
|
40
|
+
(0, vitest.it)("silently disables when server responds with ANALYTICS_NOT_ENABLED", async () => {
|
|
41
|
+
vitest.vi.useFakeTimers();
|
|
42
|
+
const storageKey = `hexclave:session-replay:v1:test-project`;
|
|
43
|
+
localStorage.setItem(storageKey, JSON.stringify({
|
|
44
|
+
session_id: "test-session",
|
|
45
|
+
created_at_ms: Date.now(),
|
|
46
|
+
last_activity_ms: Date.now()
|
|
47
|
+
}));
|
|
48
|
+
const sentBodies = [];
|
|
49
|
+
const recorder = new __session_replay_js.SessionRecorder({
|
|
50
|
+
projectId: "test-project",
|
|
51
|
+
sendBatch: async (body) => {
|
|
52
|
+
sentBodies.push(body);
|
|
53
|
+
return _hexclave_shared_dist_utils_results.Result.ok(new Response(JSON.stringify({
|
|
54
|
+
code: "ANALYTICS_NOT_ENABLED",
|
|
55
|
+
error: "Analytics is not enabled for this project."
|
|
56
|
+
}), {
|
|
57
|
+
status: 400,
|
|
58
|
+
headers: { "x-stack-known-error": "ANALYTICS_NOT_ENABLED" }
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
}, {});
|
|
62
|
+
const warnSpy = vitest.vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
63
|
+
try {
|
|
64
|
+
recorder._events = [{
|
|
65
|
+
type: 2,
|
|
66
|
+
timestamp: Date.now(),
|
|
67
|
+
data: {}
|
|
68
|
+
}];
|
|
69
|
+
recorder._tick();
|
|
70
|
+
await vitest.vi.advanceTimersByTimeAsync(0);
|
|
71
|
+
(0, vitest.expect)(sentBodies).toHaveLength(1);
|
|
72
|
+
(0, vitest.expect)(warnSpy.mock.calls.filter((args) => typeof args[0] === "string" && args[0].includes("SessionRecorder"))).toHaveLength(0);
|
|
73
|
+
recorder._events = [{
|
|
74
|
+
type: 3,
|
|
75
|
+
timestamp: Date.now(),
|
|
76
|
+
data: {}
|
|
77
|
+
}];
|
|
78
|
+
recorder._tick();
|
|
79
|
+
await vitest.vi.advanceTimersByTimeAsync(0);
|
|
80
|
+
(0, vitest.expect)(sentBodies).toHaveLength(1);
|
|
81
|
+
} finally {
|
|
82
|
+
recorder.stop();
|
|
83
|
+
warnSpy.mockRestore();
|
|
84
|
+
localStorage.removeItem(storageKey);
|
|
85
|
+
vitest.vi.useRealTimers();
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
});
|
|
27
89
|
|
|
28
90
|
//#endregion
|
|
29
91
|
//# sourceMappingURL=session-replay.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-replay.test.js","names":[],"sources":["../../../../../src/lib/hexclave-app/apps/implementations/session-replay.test.ts"],"sourcesContent":["\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template\n//===========================================\nimport { describe, expect, it } from \"vitest\";\nimport { analyticsOptionsFromJson, analyticsOptionsToJson } from \"./session-replay\";\n\ndescribe(\"analytics option JSON conversion\", () => {\n it(\"preserves top-level analytics options when serializing replay block classes\", () => {\n const json = analyticsOptionsToJson({\n enabled: false,\n replays: {\n enabled: true,\n blockClass: /stack-sensitive/u,\n },\n });\n\n expect(json?.enabled).toBe(false);\n expect(json?.replays?.enabled).toBe(true);\n });\n\n it(\"preserves top-level analytics options when deserializing replay block classes\", () => {\n const roundTripped = analyticsOptionsFromJson(analyticsOptionsToJson({\n enabled: false,\n replays: {\n blockClass: /stack-sensitive/u,\n },\n }));\n\n expect(roundTripped?.enabled).toBe(false);\n expect(roundTripped?.replays?.blockClass).toEqual(/stack-sensitive/u);\n });\n});\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"session-replay.test.js","names":["SessionRecorder","Result","vi"],"sources":["../../../../../src/lib/hexclave-app/apps/implementations/session-replay.test.ts"],"sourcesContent":["\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template\n//===========================================\n// @vitest-environment jsdom\n\nimport { describe, expect, it, vi } from \"vitest\";\nimport { Result } from \"@hexclave/shared/dist/utils/results\";\nimport { analyticsOptionsFromJson, analyticsOptionsToJson, getSessionReplayOptions, SessionRecorder } from \"./session-replay\";\n\ndescribe(\"session replay options\", () => {\n it(\"enables replays by default\", () => {\n expect(getSessionReplayOptions(undefined).enabled).toBe(true);\n expect(getSessionReplayOptions({}).enabled).toBe(true);\n expect(getSessionReplayOptions({ replays: {} }).enabled).toBe(true);\n });\n\n it(\"preserves explicit replay opt-out\", () => {\n expect(getSessionReplayOptions({ replays: { enabled: false } }).enabled).toBe(false);\n });\n});\n\ndescribe(\"analytics option JSON conversion\", () => {\n it(\"preserves top-level analytics options when serializing replay block classes\", () => {\n const json = analyticsOptionsToJson({\n enabled: false,\n replays: {\n enabled: true,\n blockClass: /stack-sensitive/u,\n },\n });\n\n expect(json?.enabled).toBe(false);\n expect(json?.replays?.enabled).toBe(true);\n });\n\n it(\"preserves top-level analytics options when deserializing replay block classes\", () => {\n const roundTripped = analyticsOptionsFromJson(analyticsOptionsToJson({\n enabled: false,\n replays: {\n blockClass: /stack-sensitive/u,\n },\n }));\n\n expect(roundTripped?.enabled).toBe(false);\n expect(roundTripped?.replays?.blockClass).toEqual(/stack-sensitive/u);\n });\n});\n\ndescribe(\"SessionRecorder flush\", () => {\n it(\"silently disables when server responds with ANALYTICS_NOT_ENABLED\", async () => {\n vi.useFakeTimers();\n\n // Seed localStorage with a valid session so _flush doesn't fail on getOrRotateSession\n const storageKey = `hexclave:session-replay:v1:test-project`;\n localStorage.setItem(storageKey, JSON.stringify({\n session_id: \"test-session\",\n created_at_ms: Date.now(),\n last_activity_ms: Date.now(),\n }));\n\n const sentBodies: string[] = [];\n const recorder = new SessionRecorder(\n {\n projectId: \"test-project\",\n sendBatch: async (body) => {\n sentBodies.push(body);\n return Result.ok(new Response(\n JSON.stringify({ code: \"ANALYTICS_NOT_ENABLED\", error: \"Analytics is not enabled for this project.\" }),\n {\n status: 400,\n headers: { \"x-stack-known-error\": \"ANALYTICS_NOT_ENABLED\" },\n },\n ));\n },\n },\n {},\n );\n\n const warnSpy = vi.spyOn(console, \"warn\").mockImplementation(() => {});\n\n try {\n // Inject an event directly into the recorder's buffer to test flush behavior\n // without needing rrweb. We access private fields for testing purposes.\n // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access\n (recorder as any)._events = [{ type: 2, timestamp: Date.now(), data: {} }];\n\n // Manually trigger a tick (which calls _flush)\n // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call\n (recorder as any)._tick();\n await vi.advanceTimersByTimeAsync(0);\n\n // One batch should have been sent\n expect(sentBodies).toHaveLength(1);\n\n // No console.warn about \"SessionRecorder flush failed\" should have been emitted\n const flushWarnings = warnSpy.mock.calls.filter(\n (args) => typeof args[0] === \"string\" && args[0].includes(\"SessionRecorder\")\n );\n expect(flushWarnings).toHaveLength(0);\n\n // After disabling, pushing new events and triggering another tick should not send\n // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access\n (recorder as any)._events = [{ type: 3, timestamp: Date.now(), data: {} }];\n // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call\n (recorder as any)._tick();\n await vi.advanceTimersByTimeAsync(0);\n expect(sentBodies).toHaveLength(1);\n } finally {\n recorder.stop();\n warnSpy.mockRestore();\n localStorage.removeItem(storageKey);\n vi.useRealTimers();\n }\n });\n});\n"],"mappings":";;;;;;;qBAUS,gCAAgC;AACvC,gBAAG,oCAAoC;AACrC,sEAA+B,OAAU,CAAC,QAAQ,CAAC,KAAK,KAAK;AAC7D,sEAA+B,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,KAAK;AACtD,sEAA+B,EAAE,SAAS,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,KAAK;GACnE;AAEF,gBAAG,2CAA2C;AAC5C,sEAA+B,EAAE,SAAS,EAAE,SAAS,OAAO,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,MAAM;GACpF;EACF;qBAEO,0CAA0C;AACjD,gBAAG,qFAAqF;EACtF,MAAM,uDAA8B;GAClC,SAAS;GACT,SAAS;IACP,SAAS;IACT,YAAY;IACb;GACF,CAAC;AAEF,qBAAO,MAAM,QAAQ,CAAC,KAAK,MAAM;AACjC,qBAAO,MAAM,SAAS,QAAQ,CAAC,KAAK,KAAK;GACzC;AAEF,gBAAG,uFAAuF;EACxF,MAAM,iHAA+D;GACnE,SAAS;GACT,SAAS,EACP,YAAY,oBACb;GACF,CAAC,CAAC;AAEH,qBAAO,cAAc,QAAQ,CAAC,KAAK,MAAM;AACzC,qBAAO,cAAc,SAAS,WAAW,CAAC,QAAQ,mBAAmB;GACrE;EACF;qBAEO,+BAA+B;AACtC,gBAAG,qEAAqE,YAAY;AAClF,YAAG,eAAe;EAGlB,MAAM,aAAa;AACnB,eAAa,QAAQ,YAAY,KAAK,UAAU;GAC9C,YAAY;GACZ,eAAe,KAAK,KAAK;GACzB,kBAAkB,KAAK,KAAK;GAC7B,CAAC,CAAC;EAEH,MAAM,aAAuB,EAAE;EAC/B,MAAM,WAAW,IAAIA,oCACnB;GACE,WAAW;GACX,WAAW,OAAO,SAAS;AACzB,eAAW,KAAK,KAAK;AACrB,WAAOC,2CAAO,GAAG,IAAI,SACnB,KAAK,UAAU;KAAE,MAAM;KAAyB,OAAO;KAA8C,CAAC,EACtG;KACE,QAAQ;KACR,SAAS,EAAE,uBAAuB,yBAAyB;KAC5D,CACF,CAAC;;GAEL,EACD,EAAE,CACH;EAED,MAAM,UAAUC,UAAG,MAAM,SAAS,OAAO,CAAC,yBAAyB,GAAG;AAEtE,MAAI;AAIF,GAAC,SAAiB,UAAU,CAAC;IAAE,MAAM;IAAG,WAAW,KAAK,KAAK;IAAE,MAAM,EAAE;IAAE,CAAC;AAI1E,GAAC,SAAiB,OAAO;AACzB,SAAMA,UAAG,yBAAyB,EAAE;AAGpC,sBAAO,WAAW,CAAC,aAAa,EAAE;AAMlC,sBAHsB,QAAQ,KAAK,MAAM,QACtC,SAAS,OAAO,KAAK,OAAO,YAAY,KAAK,GAAG,SAAS,kBAAkB,CAC7E,CACoB,CAAC,aAAa,EAAE;AAIrC,GAAC,SAAiB,UAAU,CAAC;IAAE,MAAM;IAAG,WAAW,KAAK,KAAK;IAAE,MAAM,EAAE;IAAE,CAAC;AAE1E,GAAC,SAAiB,OAAO;AACzB,SAAMA,UAAG,yBAAyB,EAAE;AACpC,sBAAO,WAAW,CAAC,aAAa,EAAE;YAC1B;AACR,YAAS,MAAM;AACf,WAAQ,aAAa;AACrB,gBAAa,WAAW,WAAW;AACnC,aAAG,eAAe;;GAEpB;EACF"}
|
|
@@ -36,8 +36,8 @@ type StackClientAppConstructorOptions<HasTokenStore extends boolean, ProjectId e
|
|
|
36
36
|
*/
|
|
37
37
|
noAutomaticPrefetch?: boolean;
|
|
38
38
|
/**
|
|
39
|
-
* Options for analytics and session recording. Replays are
|
|
40
|
-
* set `{ replays: { enabled:
|
|
39
|
+
* Options for analytics and session recording. Replays are enabled by default;
|
|
40
|
+
* set `{ replays: { enabled: false } }` to opt out.
|
|
41
41
|
*/
|
|
42
42
|
analytics?: AnalyticsOptions;
|
|
43
43
|
} & ({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client-app.js","names":["_HexclaveClientAppImpl"],"sources":["../../../../../src/lib/hexclave-app/apps/interfaces/client-app.ts"],"sourcesContent":["\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template\n//===========================================\nimport { KnownErrors } from \"@hexclave/shared\";\nimport { CurrentUserCrud } from \"@hexclave/shared/dist/interface/crud/current-user\";\nimport { Result } from \"@hexclave/shared/dist/utils/results\";\nimport { AsyncStoreProperty, AuthLike, GetCurrentPartialUserOptions, GetCurrentUserOptions, HandlerUrlOptions, HandlerUrls, OAuthScopesOnSignIn, RedirectMethod, RedirectToOptions, ResolvedHandlerUrls, hexclaveAppInternalsSymbol, TokenStoreInit } from \"../../common\";\nimport type { RequestListener } from \"@hexclave/shared/dist/interface/client-interface\";\nimport { CustomerInvoicesList, CustomerInvoicesRequestOptions, CustomerProductsList, CustomerProductsRequestOptions, Item } from \"../../customers\";\nimport { Project } from \"../../projects\";\nimport { ProjectCurrentUser, SyncedPartialUser, TokenPartialUser } from \"../../users\";\nimport { _HexclaveClientAppImpl } from \"../implementations\";\nimport { AnalyticsOptions } from \"../implementations/session-replay\";\n\n/** @deprecated Use `HexclaveClientAppConstructorOptions` from the `@hexclave/*` package instead — same symbol, new brand name. See https://docs.hexclave.com/migration. */\nexport type StackClientAppConstructorOptions<HasTokenStore extends boolean, ProjectId extends string> = {\n baseUrl?: string | { browser: string, server: string },\n extraRequestHeaders?: Record<string, string>,\n projectId?: ProjectId,\n publishableClientKey?: string,\n urls?: HandlerUrlOptions,\n oauthScopesOnSignIn?: Partial<OAuthScopesOnSignIn>,\n tokenStore?: TokenStoreInit<HasTokenStore>,\n redirectMethod?: RedirectMethod,\n inheritsFrom?: StackClientApp<any, any>,\n\n /**\n * Whether to show the Hexclave dev tool indicator in browser-like development environments.\n *\n * Defaults to true.\n */\n devTool?: boolean,\n\n /**\n * By default, the Stack app will automatically prefetch some data from Stack's server when this app is first\n * constructed. This improves the performance of your app, but will create network requests that are unnecessary if\n * the app is never used or disposed of immediately. To disable this behavior, set this option to true.\n */\n noAutomaticPrefetch?: boolean,\n\n /**\n * Options for analytics and session recording. Replays are disabled by default;\n * set `{ replays: { enabled: true } }` to opt in.\n */\n analytics?: AnalyticsOptions,\n} & (\n { tokenStore: TokenStoreInit<HasTokenStore> } | { tokenStore?: undefined, inheritsFrom: StackClientApp<HasTokenStore, any> }\n) & (\n string extends ProjectId ? unknown : ({ projectId: ProjectId } | { inheritsFrom: StackClientApp<any, ProjectId> })\n);\n\n\n/** @deprecated Use `HexclaveClientAppJson` from the `@hexclave/*` package instead — same symbol, new brand name. See https://docs.hexclave.com/migration. */\nexport type StackClientAppJson<HasTokenStore extends boolean, ProjectId extends string> = StackClientAppConstructorOptions<HasTokenStore, ProjectId> & { inheritsFrom?: undefined } & {\n uniqueIdentifier: string,\n // note: if you add more fields here, make sure to ensure the checkString in the constructor has/doesn't have them\n};\n\n/** @deprecated Use `HexclaveClientApp` from the `@hexclave/*` package instead — same symbol, new brand name. See https://docs.hexclave.com/migration. */\nexport type StackClientApp<HasTokenStore extends boolean = boolean, ProjectId extends string = string> = (\n & {\n readonly projectId: ProjectId,\n\n /**\n * The version of the Hexclave SDK.\n */\n readonly version: string,\n\n /**\n * @deprecated `app.urls` is static and does not include runtime redirect-back parameters.\n * For navigation, prefer `redirectToXyz()` methods (for example `redirectToSignIn()`).\n */\n readonly urls: Readonly<ResolvedHandlerUrls>,\n\n signInWithOAuth(provider: string, options?: { returnTo?: string }): Promise<void>,\n signInWithCredential(options: { email: string, password: string, noRedirect?: boolean }): Promise<Result<undefined, KnownErrors[\"EmailPasswordMismatch\"] | KnownErrors[\"InvalidTotpCode\"]>>,\n signUpWithCredential(options: {\n email: string,\n password: string,\n noRedirect?: boolean,\n } & ({ noVerificationCallback: true } | { noVerificationCallback?: false, verificationCallbackUrl?: string })): Promise<Result<undefined, KnownErrors[\"UserWithEmailAlreadyExists\"] | KnownErrors[\"PasswordRequirementsNotMet\"] | KnownErrors[\"BotChallengeFailed\"]>>,\n signInWithPasskey(): Promise<Result<undefined, KnownErrors[\"PasskeyAuthenticationFailed\"] | KnownErrors[\"InvalidTotpCode\"] | KnownErrors[\"PasskeyWebAuthnError\"]>>,\n callOAuthCallback(): Promise<boolean>,\n promptCliLogin(options: { appUrl: string, expiresInMillis?: number, anonRefreshToken?: string, promptLink?: (url: string, loginCode: string) => void }): Promise<Result<string, KnownErrors[\"CliAuthError\"] | KnownErrors[\"CliAuthExpiredError\"] | KnownErrors[\"CliAuthUsedError\"]>>,\n sendForgotPasswordEmail(email: string, options?: { callbackUrl?: string }): Promise<Result<undefined, KnownErrors[\"UserNotFound\"]>>,\n sendMagicLinkEmail(email: string, options?: { callbackUrl?: string }): Promise<Result<{ nonce: string }, KnownErrors[\"RedirectUrlNotWhitelisted\"] | KnownErrors[\"BotChallengeFailed\"]>>,\n resetPassword(options: { code: string, password: string }): Promise<Result<undefined, KnownErrors[\"VerificationCodeError\"]>>,\n verifyPasswordResetCode(code: string): Promise<Result<undefined, KnownErrors[\"VerificationCodeError\"]>>,\n verifyTeamInvitationCode(code: string): Promise<Result<undefined, KnownErrors[\"VerificationCodeError\"] | KnownErrors[\"TeamInvitationEmailMismatch\"]>>,\n acceptTeamInvitation(code: string): Promise<Result<undefined, KnownErrors[\"VerificationCodeError\"] | KnownErrors[\"TeamInvitationEmailMismatch\"]>>,\n getTeamInvitationDetails(code: string): Promise<Result<{ teamDisplayName: string }, KnownErrors[\"VerificationCodeError\"] | KnownErrors[\"TeamInvitationEmailMismatch\"]>>,\n verifyEmail(code: string): Promise<Result<undefined, KnownErrors[\"VerificationCodeError\"]>>,\n signInWithMagicLink(code: string, options?: { noRedirect?: boolean }): Promise<Result<undefined, KnownErrors[\"VerificationCodeError\"] | KnownErrors[\"InvalidTotpCode\"]>>,\n signInWithMfa(otp: string, code: string, options?: { noRedirect?: boolean }): Promise<Result<undefined, KnownErrors[\"VerificationCodeError\"] | KnownErrors[\"InvalidTotpCode\"]>>,\n\n redirectToOAuthCallback(): Promise<void>,\n\n getConvexClientAuth(options: HasTokenStore extends false ? { tokenStore: TokenStoreInit } : { tokenStore?: TokenStoreInit }): (args: { forceRefreshToken: boolean }) => Promise<string | null>,\n getConvexHttpClientAuth(options: { tokenStore: TokenStoreInit }): Promise<string>,\n\n useUser(options: GetCurrentUserOptions<HasTokenStore> & { or: 'redirect' }): ProjectCurrentUser<ProjectId>,\n useUser(options: GetCurrentUserOptions<HasTokenStore> & { or: 'throw' }): ProjectCurrentUser<ProjectId>,\n useUser(options: GetCurrentUserOptions<HasTokenStore> & { or: 'anonymous' }): ProjectCurrentUser<ProjectId>,\n useUser(options?: GetCurrentUserOptions<HasTokenStore>): ProjectCurrentUser<ProjectId> | null,\n\n getUser(options: GetCurrentUserOptions<HasTokenStore> & { or: 'redirect' }): Promise<ProjectCurrentUser<ProjectId>>,\n getUser(options: GetCurrentUserOptions<HasTokenStore> & { or: 'throw' }): Promise<ProjectCurrentUser<ProjectId>>,\n getUser(options: GetCurrentUserOptions<HasTokenStore> & { or: 'anonymous' }): Promise<ProjectCurrentUser<ProjectId>>,\n getUser(options?: GetCurrentUserOptions<HasTokenStore>): Promise<ProjectCurrentUser<ProjectId> | null>,\n\n cancelSubscription(options: { productId: string, subscriptionId?: string } | { productId: string, subscriptionId?: string, teamId: string }): Promise<void>,\n\n // note: we don't special-case 'anonymous' here to return non-null, see GetPartialUserOptions for more details\n getPartialUser(options: GetCurrentPartialUserOptions<HasTokenStore> & { from: 'token' }): Promise<TokenPartialUser | null>,\n getPartialUser(options: GetCurrentPartialUserOptions<HasTokenStore> & { from: 'convex' }): Promise<TokenPartialUser | null>,\n getPartialUser(options: GetCurrentPartialUserOptions<HasTokenStore>): Promise<SyncedPartialUser | TokenPartialUser | null>,\n usePartialUser(options: GetCurrentPartialUserOptions<HasTokenStore> & { from: 'token' }): TokenPartialUser | null,\n usePartialUser(options: GetCurrentPartialUserOptions<HasTokenStore> & { from: 'convex' }): TokenPartialUser | null,\n usePartialUser(options: GetCurrentPartialUserOptions<HasTokenStore>): SyncedPartialUser | TokenPartialUser | null,\n useNavigate(): (to: string) => void, // THIS_LINE_PLATFORM react-like\n\n [hexclaveAppInternalsSymbol]: {\n toClientJson(): StackClientAppJson<HasTokenStore, ProjectId>,\n setCurrentUser(userJsonPromise: Promise<CurrentUserCrud['Client']['Read'] | null>): void,\n getConstructorOptions(): StackClientAppConstructorOptions<HasTokenStore, ProjectId> & { inheritsFrom?: undefined },\n sendSessionReplayBatch(body: string, options: { keepalive: boolean }): Promise<Result<Response, Error>>,\n sendAnalyticsEventBatch(body: string, options: { keepalive: boolean }): Promise<Result<Response, Error>>,\n addRequestListener(listener: RequestListener): () => void,\n sendRequest(path: string, requestOptions: RequestInit, requestType?: \"client\" | \"server\" | \"admin\"): Promise<Response>,\n getRedirectMethod(): RedirectMethod,\n redirectToUrl(url: string | URL, options?: { replace?: boolean }): Promise<void>,\n redirectToHandler(handlerName: keyof HandlerUrls, options?: RedirectToOptions): Promise<void>,\n signInWithTokens(tokens: { accessToken: string, refreshToken: string }): Promise<void>,\n },\n }\n & AsyncStoreProperty<\"project\", [], Project, false>\n & AsyncStoreProperty<\n \"item\",\n [{ itemId: string, userId: string } | { itemId: string, teamId: string } | { itemId: string, customCustomerId: string }],\n Item,\n false\n >\n & AsyncStoreProperty<\n \"products\",\n [options: CustomerProductsRequestOptions],\n CustomerProductsList,\n true\n >\n & AsyncStoreProperty<\n \"invoices\",\n [options: CustomerInvoicesRequestOptions],\n CustomerInvoicesList,\n true\n >\n & { [K in `redirectTo${Capitalize<keyof Omit<HandlerUrls, 'handler' | 'oauthCallback'>>}`]: (options?: RedirectToOptions) => Promise<void> }\n & AuthLike<HasTokenStore extends false ? { tokenStore: TokenStoreInit } : { tokenStore?: TokenStoreInit }>\n);\n/** @deprecated Use `HexclaveClientAppConstructor` from the `@hexclave/*` package instead — same symbol, new brand name. See https://docs.hexclave.com/migration. */\nexport type StackClientAppConstructor = {\n new <\n TokenStoreType extends string,\n HasTokenStore extends (TokenStoreType extends {} ? true : boolean),\n ProjectId extends string\n >(options: StackClientAppConstructorOptions<HasTokenStore, ProjectId>): StackClientApp<HasTokenStore, ProjectId>,\n new(options: StackClientAppConstructorOptions<boolean, string>): StackClientApp<boolean, string>,\n\n [hexclaveAppInternalsSymbol]: {\n fromClientJson<HasTokenStore extends boolean, ProjectId extends string>(\n json: StackClientAppJson<HasTokenStore, ProjectId>\n ): StackClientApp<HasTokenStore, ProjectId>,\n },\n};\nexport type HexclaveClientAppConstructorOptions<HasTokenStore extends boolean, ProjectId extends string> = StackClientAppConstructorOptions<HasTokenStore, ProjectId>;\nexport type HexclaveClientAppJson<HasTokenStore extends boolean, ProjectId extends string> = StackClientAppJson<HasTokenStore, ProjectId>;\nexport type HexclaveClientApp<HasTokenStore extends boolean = boolean, ProjectId extends string = string> = StackClientApp<HasTokenStore, ProjectId>;\nexport type HexclaveClientAppConstructor = StackClientAppConstructor;\nexport const HexclaveClientApp: HexclaveClientAppConstructor = _HexclaveClientAppImpl;\n/** @deprecated Use `HexclaveClientApp` from the `@hexclave/*` package instead — same symbol, new brand name. See https://docs.hexclave.com/migration. */\nexport const StackClientApp: StackClientAppConstructor = HexclaveClientApp;\n"],"mappings":";;;;;AAiLA,MAAa,oBAAkDA;;AAE/D,MAAa,iBAA4C"}
|
|
1
|
+
{"version":3,"file":"client-app.js","names":["_HexclaveClientAppImpl"],"sources":["../../../../../src/lib/hexclave-app/apps/interfaces/client-app.ts"],"sourcesContent":["\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template\n//===========================================\nimport { KnownErrors } from \"@hexclave/shared\";\nimport { CurrentUserCrud } from \"@hexclave/shared/dist/interface/crud/current-user\";\nimport { Result } from \"@hexclave/shared/dist/utils/results\";\nimport { AsyncStoreProperty, AuthLike, GetCurrentPartialUserOptions, GetCurrentUserOptions, HandlerUrlOptions, HandlerUrls, OAuthScopesOnSignIn, RedirectMethod, RedirectToOptions, ResolvedHandlerUrls, hexclaveAppInternalsSymbol, TokenStoreInit } from \"../../common\";\nimport type { RequestListener } from \"@hexclave/shared/dist/interface/client-interface\";\nimport { CustomerInvoicesList, CustomerInvoicesRequestOptions, CustomerProductsList, CustomerProductsRequestOptions, Item } from \"../../customers\";\nimport { Project } from \"../../projects\";\nimport { ProjectCurrentUser, SyncedPartialUser, TokenPartialUser } from \"../../users\";\nimport { _HexclaveClientAppImpl } from \"../implementations\";\nimport { AnalyticsOptions } from \"../implementations/session-replay\";\n\n/** @deprecated Use `HexclaveClientAppConstructorOptions` from the `@hexclave/*` package instead — same symbol, new brand name. See https://docs.hexclave.com/migration. */\nexport type StackClientAppConstructorOptions<HasTokenStore extends boolean, ProjectId extends string> = {\n baseUrl?: string | { browser: string, server: string },\n extraRequestHeaders?: Record<string, string>,\n projectId?: ProjectId,\n publishableClientKey?: string,\n urls?: HandlerUrlOptions,\n oauthScopesOnSignIn?: Partial<OAuthScopesOnSignIn>,\n tokenStore?: TokenStoreInit<HasTokenStore>,\n redirectMethod?: RedirectMethod,\n inheritsFrom?: StackClientApp<any, any>,\n\n /**\n * Whether to show the Hexclave dev tool indicator in browser-like development environments.\n *\n * Defaults to true.\n */\n devTool?: boolean,\n\n /**\n * By default, the Stack app will automatically prefetch some data from Stack's server when this app is first\n * constructed. This improves the performance of your app, but will create network requests that are unnecessary if\n * the app is never used or disposed of immediately. To disable this behavior, set this option to true.\n */\n noAutomaticPrefetch?: boolean,\n\n /**\n * Options for analytics and session recording. Replays are enabled by default;\n * set `{ replays: { enabled: false } }` to opt out.\n */\n analytics?: AnalyticsOptions,\n} & (\n { tokenStore: TokenStoreInit<HasTokenStore> } | { tokenStore?: undefined, inheritsFrom: StackClientApp<HasTokenStore, any> }\n) & (\n string extends ProjectId ? unknown : ({ projectId: ProjectId } | { inheritsFrom: StackClientApp<any, ProjectId> })\n);\n\n\n/** @deprecated Use `HexclaveClientAppJson` from the `@hexclave/*` package instead — same symbol, new brand name. See https://docs.hexclave.com/migration. */\nexport type StackClientAppJson<HasTokenStore extends boolean, ProjectId extends string> = StackClientAppConstructorOptions<HasTokenStore, ProjectId> & { inheritsFrom?: undefined } & {\n uniqueIdentifier: string,\n // note: if you add more fields here, make sure to ensure the checkString in the constructor has/doesn't have them\n};\n\n/** @deprecated Use `HexclaveClientApp` from the `@hexclave/*` package instead — same symbol, new brand name. See https://docs.hexclave.com/migration. */\nexport type StackClientApp<HasTokenStore extends boolean = boolean, ProjectId extends string = string> = (\n & {\n readonly projectId: ProjectId,\n\n /**\n * The version of the Hexclave SDK.\n */\n readonly version: string,\n\n /**\n * @deprecated `app.urls` is static and does not include runtime redirect-back parameters.\n * For navigation, prefer `redirectToXyz()` methods (for example `redirectToSignIn()`).\n */\n readonly urls: Readonly<ResolvedHandlerUrls>,\n\n signInWithOAuth(provider: string, options?: { returnTo?: string }): Promise<void>,\n signInWithCredential(options: { email: string, password: string, noRedirect?: boolean }): Promise<Result<undefined, KnownErrors[\"EmailPasswordMismatch\"] | KnownErrors[\"InvalidTotpCode\"]>>,\n signUpWithCredential(options: {\n email: string,\n password: string,\n noRedirect?: boolean,\n } & ({ noVerificationCallback: true } | { noVerificationCallback?: false, verificationCallbackUrl?: string })): Promise<Result<undefined, KnownErrors[\"UserWithEmailAlreadyExists\"] | KnownErrors[\"PasswordRequirementsNotMet\"] | KnownErrors[\"BotChallengeFailed\"]>>,\n signInWithPasskey(): Promise<Result<undefined, KnownErrors[\"PasskeyAuthenticationFailed\"] | KnownErrors[\"InvalidTotpCode\"] | KnownErrors[\"PasskeyWebAuthnError\"]>>,\n callOAuthCallback(): Promise<boolean>,\n promptCliLogin(options: { appUrl: string, expiresInMillis?: number, anonRefreshToken?: string, promptLink?: (url: string, loginCode: string) => void }): Promise<Result<string, KnownErrors[\"CliAuthError\"] | KnownErrors[\"CliAuthExpiredError\"] | KnownErrors[\"CliAuthUsedError\"]>>,\n sendForgotPasswordEmail(email: string, options?: { callbackUrl?: string }): Promise<Result<undefined, KnownErrors[\"UserNotFound\"]>>,\n sendMagicLinkEmail(email: string, options?: { callbackUrl?: string }): Promise<Result<{ nonce: string }, KnownErrors[\"RedirectUrlNotWhitelisted\"] | KnownErrors[\"BotChallengeFailed\"]>>,\n resetPassword(options: { code: string, password: string }): Promise<Result<undefined, KnownErrors[\"VerificationCodeError\"]>>,\n verifyPasswordResetCode(code: string): Promise<Result<undefined, KnownErrors[\"VerificationCodeError\"]>>,\n verifyTeamInvitationCode(code: string): Promise<Result<undefined, KnownErrors[\"VerificationCodeError\"] | KnownErrors[\"TeamInvitationEmailMismatch\"]>>,\n acceptTeamInvitation(code: string): Promise<Result<undefined, KnownErrors[\"VerificationCodeError\"] | KnownErrors[\"TeamInvitationEmailMismatch\"]>>,\n getTeamInvitationDetails(code: string): Promise<Result<{ teamDisplayName: string }, KnownErrors[\"VerificationCodeError\"] | KnownErrors[\"TeamInvitationEmailMismatch\"]>>,\n verifyEmail(code: string): Promise<Result<undefined, KnownErrors[\"VerificationCodeError\"]>>,\n signInWithMagicLink(code: string, options?: { noRedirect?: boolean }): Promise<Result<undefined, KnownErrors[\"VerificationCodeError\"] | KnownErrors[\"InvalidTotpCode\"]>>,\n signInWithMfa(otp: string, code: string, options?: { noRedirect?: boolean }): Promise<Result<undefined, KnownErrors[\"VerificationCodeError\"] | KnownErrors[\"InvalidTotpCode\"]>>,\n\n redirectToOAuthCallback(): Promise<void>,\n\n getConvexClientAuth(options: HasTokenStore extends false ? { tokenStore: TokenStoreInit } : { tokenStore?: TokenStoreInit }): (args: { forceRefreshToken: boolean }) => Promise<string | null>,\n getConvexHttpClientAuth(options: { tokenStore: TokenStoreInit }): Promise<string>,\n\n useUser(options: GetCurrentUserOptions<HasTokenStore> & { or: 'redirect' }): ProjectCurrentUser<ProjectId>,\n useUser(options: GetCurrentUserOptions<HasTokenStore> & { or: 'throw' }): ProjectCurrentUser<ProjectId>,\n useUser(options: GetCurrentUserOptions<HasTokenStore> & { or: 'anonymous' }): ProjectCurrentUser<ProjectId>,\n useUser(options?: GetCurrentUserOptions<HasTokenStore>): ProjectCurrentUser<ProjectId> | null,\n\n getUser(options: GetCurrentUserOptions<HasTokenStore> & { or: 'redirect' }): Promise<ProjectCurrentUser<ProjectId>>,\n getUser(options: GetCurrentUserOptions<HasTokenStore> & { or: 'throw' }): Promise<ProjectCurrentUser<ProjectId>>,\n getUser(options: GetCurrentUserOptions<HasTokenStore> & { or: 'anonymous' }): Promise<ProjectCurrentUser<ProjectId>>,\n getUser(options?: GetCurrentUserOptions<HasTokenStore>): Promise<ProjectCurrentUser<ProjectId> | null>,\n\n cancelSubscription(options: { productId: string, subscriptionId?: string } | { productId: string, subscriptionId?: string, teamId: string }): Promise<void>,\n\n // note: we don't special-case 'anonymous' here to return non-null, see GetPartialUserOptions for more details\n getPartialUser(options: GetCurrentPartialUserOptions<HasTokenStore> & { from: 'token' }): Promise<TokenPartialUser | null>,\n getPartialUser(options: GetCurrentPartialUserOptions<HasTokenStore> & { from: 'convex' }): Promise<TokenPartialUser | null>,\n getPartialUser(options: GetCurrentPartialUserOptions<HasTokenStore>): Promise<SyncedPartialUser | TokenPartialUser | null>,\n usePartialUser(options: GetCurrentPartialUserOptions<HasTokenStore> & { from: 'token' }): TokenPartialUser | null,\n usePartialUser(options: GetCurrentPartialUserOptions<HasTokenStore> & { from: 'convex' }): TokenPartialUser | null,\n usePartialUser(options: GetCurrentPartialUserOptions<HasTokenStore>): SyncedPartialUser | TokenPartialUser | null,\n useNavigate(): (to: string) => void, // THIS_LINE_PLATFORM react-like\n\n [hexclaveAppInternalsSymbol]: {\n toClientJson(): StackClientAppJson<HasTokenStore, ProjectId>,\n setCurrentUser(userJsonPromise: Promise<CurrentUserCrud['Client']['Read'] | null>): void,\n getConstructorOptions(): StackClientAppConstructorOptions<HasTokenStore, ProjectId> & { inheritsFrom?: undefined },\n sendSessionReplayBatch(body: string, options: { keepalive: boolean }): Promise<Result<Response, Error>>,\n sendAnalyticsEventBatch(body: string, options: { keepalive: boolean }): Promise<Result<Response, Error>>,\n addRequestListener(listener: RequestListener): () => void,\n sendRequest(path: string, requestOptions: RequestInit, requestType?: \"client\" | \"server\" | \"admin\"): Promise<Response>,\n getRedirectMethod(): RedirectMethod,\n redirectToUrl(url: string | URL, options?: { replace?: boolean }): Promise<void>,\n redirectToHandler(handlerName: keyof HandlerUrls, options?: RedirectToOptions): Promise<void>,\n signInWithTokens(tokens: { accessToken: string, refreshToken: string }): Promise<void>,\n },\n }\n & AsyncStoreProperty<\"project\", [], Project, false>\n & AsyncStoreProperty<\n \"item\",\n [{ itemId: string, userId: string } | { itemId: string, teamId: string } | { itemId: string, customCustomerId: string }],\n Item,\n false\n >\n & AsyncStoreProperty<\n \"products\",\n [options: CustomerProductsRequestOptions],\n CustomerProductsList,\n true\n >\n & AsyncStoreProperty<\n \"invoices\",\n [options: CustomerInvoicesRequestOptions],\n CustomerInvoicesList,\n true\n >\n & { [K in `redirectTo${Capitalize<keyof Omit<HandlerUrls, 'handler' | 'oauthCallback'>>}`]: (options?: RedirectToOptions) => Promise<void> }\n & AuthLike<HasTokenStore extends false ? { tokenStore: TokenStoreInit } : { tokenStore?: TokenStoreInit }>\n);\n/** @deprecated Use `HexclaveClientAppConstructor` from the `@hexclave/*` package instead — same symbol, new brand name. See https://docs.hexclave.com/migration. */\nexport type StackClientAppConstructor = {\n new <\n TokenStoreType extends string,\n HasTokenStore extends (TokenStoreType extends {} ? true : boolean),\n ProjectId extends string\n >(options: StackClientAppConstructorOptions<HasTokenStore, ProjectId>): StackClientApp<HasTokenStore, ProjectId>,\n new(options: StackClientAppConstructorOptions<boolean, string>): StackClientApp<boolean, string>,\n\n [hexclaveAppInternalsSymbol]: {\n fromClientJson<HasTokenStore extends boolean, ProjectId extends string>(\n json: StackClientAppJson<HasTokenStore, ProjectId>\n ): StackClientApp<HasTokenStore, ProjectId>,\n },\n};\nexport type HexclaveClientAppConstructorOptions<HasTokenStore extends boolean, ProjectId extends string> = StackClientAppConstructorOptions<HasTokenStore, ProjectId>;\nexport type HexclaveClientAppJson<HasTokenStore extends boolean, ProjectId extends string> = StackClientAppJson<HasTokenStore, ProjectId>;\nexport type HexclaveClientApp<HasTokenStore extends boolean = boolean, ProjectId extends string = string> = StackClientApp<HasTokenStore, ProjectId>;\nexport type HexclaveClientAppConstructor = StackClientAppConstructor;\nexport const HexclaveClientApp: HexclaveClientAppConstructor = _HexclaveClientAppImpl;\n/** @deprecated Use `HexclaveClientApp` from the `@hexclave/*` package instead — same symbol, new brand name. See https://docs.hexclave.com/migration. */\nexport const StackClientApp: StackClientAppConstructor = HexclaveClientApp;\n"],"mappings":";;;;;AAiLA,MAAa,oBAAkDA;;AAE/D,MAAa,iBAA4C"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"//": "THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template (FOR package.json FILES, PLEASE EDIT package-template.json)",
|
|
3
3
|
"name": "@hexclave/next",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.18",
|
|
5
5
|
"repository": "https://github.com/hexclave/hexclave",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"main": "./dist/index.js",
|
|
@@ -75,9 +75,9 @@
|
|
|
75
75
|
"rrweb": "^1.1.3",
|
|
76
76
|
"tsx": "^4.21.0",
|
|
77
77
|
"yup": "^1.7.1",
|
|
78
|
-
"@hexclave/sc": "1.0.
|
|
79
|
-
"@hexclave/shared": "1.0.
|
|
80
|
-
"@hexclave/ui": "1.0.
|
|
78
|
+
"@hexclave/sc": "1.0.18",
|
|
79
|
+
"@hexclave/shared": "1.0.18",
|
|
80
|
+
"@hexclave/ui": "1.0.18"
|
|
81
81
|
},
|
|
82
82
|
"peerDependencies": {
|
|
83
83
|
"@types/react": ">=18.0.0",
|
|
@@ -306,11 +306,14 @@ export const devToolCSS = getInPageUiBaseCSS('.hexclave-devtool') + `
|
|
|
306
306
|
.hexclave-devtool .sdt-tab-pane {
|
|
307
307
|
position: absolute;
|
|
308
308
|
inset: 0;
|
|
309
|
+
display: none;
|
|
309
310
|
overflow-y: auto;
|
|
310
311
|
overflow-x: hidden;
|
|
311
312
|
padding: 16px;
|
|
312
|
-
|
|
313
|
+
background: var(--sdt-bg);
|
|
314
|
+
opacity: 0;
|
|
313
315
|
pointer-events: none;
|
|
316
|
+
z-index: 0;
|
|
314
317
|
}
|
|
315
318
|
|
|
316
319
|
.hexclave-devtool .sdt-tab-pane-iframe {
|
|
@@ -319,20 +322,10 @@ export const devToolCSS = getInPageUiBaseCSS('.hexclave-devtool') + `
|
|
|
319
322
|
}
|
|
320
323
|
|
|
321
324
|
.hexclave-devtool .sdt-tab-pane-active {
|
|
322
|
-
|
|
325
|
+
display: block;
|
|
326
|
+
opacity: 1;
|
|
323
327
|
pointer-events: auto;
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
@keyframes sdt-tab-fade-in {
|
|
328
|
-
from {
|
|
329
|
-
opacity: 0;
|
|
330
|
-
transform: translateY(6px);
|
|
331
|
-
}
|
|
332
|
-
to {
|
|
333
|
-
opacity: 1;
|
|
334
|
-
transform: translateY(0);
|
|
335
|
-
}
|
|
328
|
+
z-index: 1;
|
|
336
329
|
}
|
|
337
330
|
|
|
338
331
|
/* ===== Overview tab — single column ===== */
|
|
@@ -67,7 +67,7 @@ import { EventTracker } from "./event-tracker";
|
|
|
67
67
|
import type { CrossDomainHandoffParams } from "./redirect-page-urls";
|
|
68
68
|
import { crossDomainAuthQueryParams, getCrossDomainHandoffParamsFromCurrentUrl, planRedirectToHandler } from "./redirect-page-urls";
|
|
69
69
|
import { subscribeSessionRefresh } from "./session-refresh-subscription";
|
|
70
|
-
import { AnalyticsOptions, SessionRecorder, analyticsOptionsFromJson, analyticsOptionsToJson } from "./session-replay";
|
|
70
|
+
import { AnalyticsOptions, SessionRecorder, analyticsOptionsFromJson, analyticsOptionsToJson, getSessionReplayOptions } from "./session-replay";
|
|
71
71
|
|
|
72
72
|
import { useAsyncCache } from "./common";
|
|
73
73
|
import { mountClickmapOverlay } from "../../../../clickmap";
|
|
@@ -658,13 +658,14 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
658
658
|
|
|
659
659
|
const analyticsEnabled = this._analyticsOptions?.enabled !== false;
|
|
660
660
|
|
|
661
|
-
|
|
661
|
+
const sessionReplayOptions = getSessionReplayOptions(this._analyticsOptions);
|
|
662
|
+
if (analyticsEnabled && isBrowserLike() && this._hasPersistentTokenStore() && sessionReplayOptions.enabled) {
|
|
662
663
|
this._sessionRecorder = new SessionRecorder({
|
|
663
664
|
projectId: this.projectId,
|
|
664
665
|
sendBatch: async (body, opts) => {
|
|
665
666
|
return await this._interface.sendSessionReplayBatch(body, await getAnalyticsSession(), opts);
|
|
666
667
|
},
|
|
667
|
-
},
|
|
668
|
+
}, sessionReplayOptions);
|
|
668
669
|
this._sessionRecorder.start();
|
|
669
670
|
}
|
|
670
671
|
|
|
@@ -389,4 +389,45 @@ describe("EventTracker", () => {
|
|
|
389
389
|
tracker.stop();
|
|
390
390
|
}
|
|
391
391
|
});
|
|
392
|
+
|
|
393
|
+
it("silently disables when server responds with ANALYTICS_NOT_ENABLED", async () => {
|
|
394
|
+
vi.useFakeTimers();
|
|
395
|
+
document.body.innerHTML = "<button>Click me</button>";
|
|
396
|
+
|
|
397
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
398
|
+
const sentBodies: string[] = [];
|
|
399
|
+
const tracker = new EventTracker({
|
|
400
|
+
projectId: "internal",
|
|
401
|
+
sendBatch: async (body) => {
|
|
402
|
+
sentBodies.push(body);
|
|
403
|
+
return Result.ok(new Response(
|
|
404
|
+
JSON.stringify({ code: "ANALYTICS_NOT_ENABLED", error: "Analytics is not enabled for this project." }),
|
|
405
|
+
{
|
|
406
|
+
status: 400,
|
|
407
|
+
headers: { "x-stack-known-error": "ANALYTICS_NOT_ENABLED" },
|
|
408
|
+
},
|
|
409
|
+
));
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
tracker.start();
|
|
415
|
+
|
|
416
|
+
// First flush sends the initial page-view event; server rejects it.
|
|
417
|
+
await advancePastFlush();
|
|
418
|
+
expect(sentBodies).toHaveLength(1);
|
|
419
|
+
|
|
420
|
+
// No console.warn should have been emitted.
|
|
421
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
422
|
+
|
|
423
|
+
// After disabling, new events should not accumulate or trigger further
|
|
424
|
+
// flushes.
|
|
425
|
+
document.querySelector("button")?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
426
|
+
await advancePastFlush();
|
|
427
|
+
expect(sentBodies).toHaveLength(1);
|
|
428
|
+
} finally {
|
|
429
|
+
tracker.stop();
|
|
430
|
+
warnSpy.mockRestore();
|
|
431
|
+
}
|
|
432
|
+
});
|
|
392
433
|
});
|
|
@@ -112,6 +112,7 @@ type TrackedEvent = {
|
|
|
112
112
|
export class EventTracker {
|
|
113
113
|
private _started = false;
|
|
114
114
|
private _cancelled = false;
|
|
115
|
+
private _disabled = false;
|
|
115
116
|
private _detachListeners: (() => void) | null = null;
|
|
116
117
|
private _flushTimer: ReturnType<typeof setInterval> | null = null;
|
|
117
118
|
private _events: TrackedEvent[] = [];
|
|
@@ -177,6 +178,7 @@ export class EventTracker {
|
|
|
177
178
|
}
|
|
178
179
|
|
|
179
180
|
private _pushEvent(event: TrackedEvent) {
|
|
181
|
+
if (this._disabled) return;
|
|
180
182
|
this._events.push(event);
|
|
181
183
|
this._approxBytes += JSON.stringify(event).length;
|
|
182
184
|
if (this._events.length >= MAX_EVENTS_PER_BATCH || this._approxBytes >= MAX_APPROX_BYTES_PER_BATCH) {
|
|
@@ -468,6 +470,8 @@ export class EventTracker {
|
|
|
468
470
|
}
|
|
469
471
|
|
|
470
472
|
private async _flush(options: { keepalive: boolean }) {
|
|
473
|
+
if (this._disabled) return;
|
|
474
|
+
|
|
471
475
|
// A keepalive flush means the page is unloading — a click still awaiting
|
|
472
476
|
// dead-click classification led to that unload, so it is alive by
|
|
473
477
|
// definition and ships unmarked.
|
|
@@ -504,6 +508,18 @@ export class EventTracker {
|
|
|
504
508
|
}
|
|
505
509
|
|
|
506
510
|
if (!res.data.ok) {
|
|
511
|
+
// If the server tells us analytics is not enabled for this project,
|
|
512
|
+
// silently disable the tracker — no point retrying or warning the user.
|
|
513
|
+
const knownError = res.data.headers.get("x-hexclave-known-error") ?? res.data.headers.get("x-stack-known-error");
|
|
514
|
+
if (knownError === "ANALYTICS_NOT_ENABLED") {
|
|
515
|
+
this._disabled = true;
|
|
516
|
+
if (this._flushTimer !== null) {
|
|
517
|
+
clearInterval(this._flushTimer);
|
|
518
|
+
this._flushTimer = null;
|
|
519
|
+
}
|
|
520
|
+
this._teardown();
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
507
523
|
console.warn("EventTracker flush failed:", res.data.status, await res.data.text());
|
|
508
524
|
}
|
|
509
525
|
}
|
|
@@ -2,8 +2,23 @@
|
|
|
2
2
|
//===========================================
|
|
3
3
|
// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template
|
|
4
4
|
//===========================================
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
// @vitest-environment jsdom
|
|
6
|
+
|
|
7
|
+
import { describe, expect, it, vi } from "vitest";
|
|
8
|
+
import { Result } from "@hexclave/shared/dist/utils/results";
|
|
9
|
+
import { analyticsOptionsFromJson, analyticsOptionsToJson, getSessionReplayOptions, SessionRecorder } from "./session-replay";
|
|
10
|
+
|
|
11
|
+
describe("session replay options", () => {
|
|
12
|
+
it("enables replays by default", () => {
|
|
13
|
+
expect(getSessionReplayOptions(undefined).enabled).toBe(true);
|
|
14
|
+
expect(getSessionReplayOptions({}).enabled).toBe(true);
|
|
15
|
+
expect(getSessionReplayOptions({ replays: {} }).enabled).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("preserves explicit replay opt-out", () => {
|
|
19
|
+
expect(getSessionReplayOptions({ replays: { enabled: false } }).enabled).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
7
22
|
|
|
8
23
|
describe("analytics option JSON conversion", () => {
|
|
9
24
|
it("preserves top-level analytics options when serializing replay block classes", () => {
|
|
@@ -31,3 +46,71 @@ describe("analytics option JSON conversion", () => {
|
|
|
31
46
|
expect(roundTripped?.replays?.blockClass).toEqual(/stack-sensitive/u);
|
|
32
47
|
});
|
|
33
48
|
});
|
|
49
|
+
|
|
50
|
+
describe("SessionRecorder flush", () => {
|
|
51
|
+
it("silently disables when server responds with ANALYTICS_NOT_ENABLED", async () => {
|
|
52
|
+
vi.useFakeTimers();
|
|
53
|
+
|
|
54
|
+
// Seed localStorage with a valid session so _flush doesn't fail on getOrRotateSession
|
|
55
|
+
const storageKey = `hexclave:session-replay:v1:test-project`;
|
|
56
|
+
localStorage.setItem(storageKey, JSON.stringify({
|
|
57
|
+
session_id: "test-session",
|
|
58
|
+
created_at_ms: Date.now(),
|
|
59
|
+
last_activity_ms: Date.now(),
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
const sentBodies: string[] = [];
|
|
63
|
+
const recorder = new SessionRecorder(
|
|
64
|
+
{
|
|
65
|
+
projectId: "test-project",
|
|
66
|
+
sendBatch: async (body) => {
|
|
67
|
+
sentBodies.push(body);
|
|
68
|
+
return Result.ok(new Response(
|
|
69
|
+
JSON.stringify({ code: "ANALYTICS_NOT_ENABLED", error: "Analytics is not enabled for this project." }),
|
|
70
|
+
{
|
|
71
|
+
status: 400,
|
|
72
|
+
headers: { "x-stack-known-error": "ANALYTICS_NOT_ENABLED" },
|
|
73
|
+
},
|
|
74
|
+
));
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{},
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
// Inject an event directly into the recorder's buffer to test flush behavior
|
|
84
|
+
// without needing rrweb. We access private fields for testing purposes.
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
86
|
+
(recorder as any)._events = [{ type: 2, timestamp: Date.now(), data: {} }];
|
|
87
|
+
|
|
88
|
+
// Manually trigger a tick (which calls _flush)
|
|
89
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
|
90
|
+
(recorder as any)._tick();
|
|
91
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
92
|
+
|
|
93
|
+
// One batch should have been sent
|
|
94
|
+
expect(sentBodies).toHaveLength(1);
|
|
95
|
+
|
|
96
|
+
// No console.warn about "SessionRecorder flush failed" should have been emitted
|
|
97
|
+
const flushWarnings = warnSpy.mock.calls.filter(
|
|
98
|
+
(args) => typeof args[0] === "string" && args[0].includes("SessionRecorder")
|
|
99
|
+
);
|
|
100
|
+
expect(flushWarnings).toHaveLength(0);
|
|
101
|
+
|
|
102
|
+
// After disabling, pushing new events and triggering another tick should not send
|
|
103
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
104
|
+
(recorder as any)._events = [{ type: 3, timestamp: Date.now(), data: {} }];
|
|
105
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
|
106
|
+
(recorder as any)._tick();
|
|
107
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
108
|
+
expect(sentBodies).toHaveLength(1);
|
|
109
|
+
} finally {
|
|
110
|
+
recorder.stop();
|
|
111
|
+
warnSpy.mockRestore();
|
|
112
|
+
localStorage.removeItem(storageKey);
|
|
113
|
+
vi.useRealTimers();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -11,7 +11,7 @@ export type AnalyticsReplayOptions = {
|
|
|
11
11
|
/**
|
|
12
12
|
* Whether session replays are enabled.
|
|
13
13
|
*
|
|
14
|
-
* @default
|
|
14
|
+
* @default true
|
|
15
15
|
*/
|
|
16
16
|
enabled?: boolean,
|
|
17
17
|
/**
|
|
@@ -44,12 +44,19 @@ export type AnalyticsOptions = {
|
|
|
44
44
|
*/
|
|
45
45
|
enabled?: boolean,
|
|
46
46
|
/**
|
|
47
|
-
* Options for session replay recording. Replays are
|
|
48
|
-
* set `enabled:
|
|
47
|
+
* Options for session replay recording. Replays are enabled by default;
|
|
48
|
+
* set `enabled: false` to opt out.
|
|
49
49
|
*/
|
|
50
50
|
replays?: AnalyticsReplayOptions,
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
+
export function getSessionReplayOptions(analyticsOptions: AnalyticsOptions | undefined): AnalyticsReplayOptions {
|
|
54
|
+
return {
|
|
55
|
+
...analyticsOptions?.replays,
|
|
56
|
+
enabled: analyticsOptions?.replays?.enabled ?? true,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
53
60
|
/**
|
|
54
61
|
* Converts AnalyticsOptions to a JSON-safe representation.
|
|
55
62
|
* RegExp blockClass values are serialized as `{ __regexp, __flags }` objects.
|
|
@@ -161,6 +168,7 @@ export type SessionRecorderDeps = {
|
|
|
161
168
|
export class SessionRecorder {
|
|
162
169
|
private _started = false;
|
|
163
170
|
private _cancelled = false;
|
|
171
|
+
private _disabled = false;
|
|
164
172
|
private _stopRecording: (() => void) | null = null;
|
|
165
173
|
private _detachListeners: (() => void) | null = null;
|
|
166
174
|
private _flushTimer: ReturnType<typeof setInterval> | null = null;
|
|
@@ -228,6 +236,7 @@ export class SessionRecorder {
|
|
|
228
236
|
}
|
|
229
237
|
|
|
230
238
|
private async _flush(options: { keepalive: boolean }) {
|
|
239
|
+
if (this._disabled) return;
|
|
231
240
|
if (this._events.length === 0) return;
|
|
232
241
|
// Prevent concurrent in-flight HTTP requests. When a flush is already
|
|
233
242
|
// in-flight, a second batch could race on the server (both call
|
|
@@ -265,6 +274,18 @@ export class SessionRecorder {
|
|
|
265
274
|
}
|
|
266
275
|
|
|
267
276
|
if (!res.data.ok) {
|
|
277
|
+
// If the server tells us analytics is not enabled for this project,
|
|
278
|
+
// silently disable the recorder — no point retrying or warning the user.
|
|
279
|
+
const knownError = res.data.headers.get("x-hexclave-known-error") ?? res.data.headers.get("x-stack-known-error");
|
|
280
|
+
if (knownError === "ANALYTICS_NOT_ENABLED") {
|
|
281
|
+
this._disabled = true;
|
|
282
|
+
if (this._flushTimer !== null) {
|
|
283
|
+
clearInterval(this._flushTimer);
|
|
284
|
+
this._flushTimer = null;
|
|
285
|
+
}
|
|
286
|
+
this._stopCurrentRecording();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
268
289
|
captureWarning("SessionRecorder.flush", new Error(`SessionRecorder flush failed: ${res.data.status} ${await res.data.text()}`));
|
|
269
290
|
}
|
|
270
291
|
} finally {
|
|
@@ -40,8 +40,8 @@ export type StackClientAppConstructorOptions<HasTokenStore extends boolean, Proj
|
|
|
40
40
|
noAutomaticPrefetch?: boolean,
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
* Options for analytics and session recording. Replays are
|
|
44
|
-
* set `{ replays: { enabled:
|
|
43
|
+
* Options for analytics and session recording. Replays are enabled by default;
|
|
44
|
+
* set `{ replays: { enabled: false } }` to opt out.
|
|
45
45
|
*/
|
|
46
46
|
analytics?: AnalyticsOptions,
|
|
47
47
|
} & (
|