@clue-ai/browser-sdk 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +100 -0
  2. package/dist/authoring/overlay.d.ts +12 -0
  3. package/dist/authoring/overlay.js +468 -0
  4. package/dist/authoring/recording.d.ts +125 -0
  5. package/dist/authoring/recording.js +481 -0
  6. package/dist/authoring/service-logo.d.ts +1 -0
  7. package/dist/authoring/service-logo.generated.d.ts +1 -0
  8. package/dist/authoring/service-logo.generated.js +3 -0
  9. package/dist/authoring/service-logo.js +1 -0
  10. package/dist/authoring/session.d.ts +23 -0
  11. package/dist/authoring/session.js +127 -0
  12. package/dist/authoring/surface.d.ts +11 -0
  13. package/dist/authoring/surface.js +63 -0
  14. package/dist/authoring/toolbar-constants.d.ts +23 -0
  15. package/dist/authoring/toolbar-constants.js +42 -0
  16. package/dist/authoring/toolbar-drag.d.ts +29 -0
  17. package/dist/authoring/toolbar-drag.js +270 -0
  18. package/dist/authoring/toolbar-view.d.ts +21 -0
  19. package/dist/authoring/toolbar-view.js +2584 -0
  20. package/dist/capture/action.d.ts +2 -0
  21. package/dist/capture/action.js +62 -0
  22. package/dist/capture/dom.d.ts +23 -0
  23. package/dist/capture/dom.js +329 -0
  24. package/dist/capture/drag.d.ts +2 -0
  25. package/dist/capture/drag.js +75 -0
  26. package/dist/capture/error.d.ts +2 -0
  27. package/dist/capture/error.js +193 -0
  28. package/dist/capture/form.d.ts +2 -0
  29. package/dist/capture/form.js +137 -0
  30. package/dist/capture/frustration.d.ts +2 -0
  31. package/dist/capture/frustration.js +171 -0
  32. package/dist/capture/input.d.ts +2 -0
  33. package/dist/capture/input.js +109 -0
  34. package/dist/capture/location.d.ts +10 -0
  35. package/dist/capture/location.js +42 -0
  36. package/dist/capture/navigation.d.ts +2 -0
  37. package/dist/capture/navigation.js +100 -0
  38. package/dist/capture/network.d.ts +13 -0
  39. package/dist/capture/network.js +903 -0
  40. package/dist/capture/page.d.ts +2 -0
  41. package/dist/capture/page.js +78 -0
  42. package/dist/capture/performance.d.ts +2 -0
  43. package/dist/capture/performance.js +268 -0
  44. package/dist/context/account.d.ts +12 -0
  45. package/dist/context/account.js +129 -0
  46. package/dist/context/environment.d.ts +42 -0
  47. package/dist/context/environment.js +208 -0
  48. package/dist/context/identity.d.ts +14 -0
  49. package/dist/context/identity.js +123 -0
  50. package/dist/context/session.d.ts +28 -0
  51. package/dist/context/session.js +155 -0
  52. package/dist/context/tab.d.ts +22 -0
  53. package/dist/context/tab.js +142 -0
  54. package/dist/context/trace.d.ts +32 -0
  55. package/dist/context/trace.js +65 -0
  56. package/dist/core/config.d.ts +4 -0
  57. package/dist/core/config.js +199 -0
  58. package/dist/core/constants.d.ts +43 -0
  59. package/dist/core/constants.js +109 -0
  60. package/dist/core/contracts.d.ts +58 -0
  61. package/dist/core/contracts.js +53 -0
  62. package/dist/core/sdk.d.ts +2 -0
  63. package/dist/core/sdk.js +831 -0
  64. package/dist/core/types.d.ts +413 -0
  65. package/dist/core/types.js +1 -0
  66. package/dist/core/usage-governor.d.ts +7 -0
  67. package/dist/core/usage-governor.js +127 -0
  68. package/dist/index.d.ts +17 -0
  69. package/dist/index.js +36 -0
  70. package/dist/integrations/next-router.d.ts +16 -0
  71. package/dist/integrations/next-router.js +18 -0
  72. package/dist/integrations/react-router.d.ts +7 -0
  73. package/dist/integrations/react-router.js +37 -0
  74. package/dist/internal/metrics.d.ts +9 -0
  75. package/dist/internal/metrics.js +38 -0
  76. package/dist/normalize/builders.d.ts +15 -0
  77. package/dist/normalize/builders.js +786 -0
  78. package/dist/normalize/canonical.d.ts +13 -0
  79. package/dist/normalize/canonical.js +77 -0
  80. package/dist/normalize/event-id.d.ts +8 -0
  81. package/dist/normalize/event-id.js +39 -0
  82. package/dist/normalize/path-template.d.ts +1 -0
  83. package/dist/normalize/path-template.js +33 -0
  84. package/dist/privacy/local-minimization.d.ts +29 -0
  85. package/dist/privacy/local-minimization.js +88 -0
  86. package/dist/privacy/mask.d.ts +7 -0
  87. package/dist/privacy/mask.js +60 -0
  88. package/dist/privacy/parameter-snapshot.d.ts +14 -0
  89. package/dist/privacy/parameter-snapshot.js +206 -0
  90. package/dist/privacy/sanitize.d.ts +11 -0
  91. package/dist/privacy/sanitize.js +145 -0
  92. package/dist/privacy/schema-evidence.d.ts +20 -0
  93. package/dist/privacy/schema-evidence.js +238 -0
  94. package/dist/transport/batch.d.ts +37 -0
  95. package/dist/transport/batch.js +182 -0
  96. package/dist/transport/client.d.ts +61 -0
  97. package/dist/transport/client.js +267 -0
  98. package/dist/transport/queue.d.ts +22 -0
  99. package/dist/transport/queue.js +56 -0
  100. package/dist/transport/retry.d.ts +14 -0
  101. package/dist/transport/retry.js +46 -0
  102. package/package.json +38 -0
@@ -0,0 +1,2 @@
1
+ import type { CaptureModuleContext, CaptureStopHandle } from "../core/types";
2
+ export declare const startPageCapture: (context: CaptureModuleContext) => CaptureStopHandle;
@@ -0,0 +1,78 @@
1
+ import { getCurrentPageSnapshot } from "./location";
2
+ export const startPageCapture = (context) => {
3
+ if (typeof window === "undefined" || typeof document === "undefined") {
4
+ return { stop: () => undefined };
5
+ }
6
+ let pageEnteredAtMs = Date.now();
7
+ const emitPageView = () => {
8
+ if (context.getConsent() === "denied") {
9
+ return;
10
+ }
11
+ const snapshot = getCurrentPageSnapshot();
12
+ context.onEvent({
13
+ type: "page_view",
14
+ occurredAtMs: Date.now(),
15
+ payload: {
16
+ viewId: context.getViewId(),
17
+ screenKey: snapshot.screenKey,
18
+ path: snapshot.path,
19
+ urlCanonical: snapshot.urlCanonical,
20
+ pageTitleCandidate: snapshot.pageTitleCandidate,
21
+ routeNameCandidate: null,
22
+ primaryHeadingCandidate: snapshot.primaryHeadingCandidate,
23
+ },
24
+ });
25
+ };
26
+ const handlePageLeave = () => {
27
+ if (context.getConsent() === "denied") {
28
+ return;
29
+ }
30
+ const snapshot = getCurrentPageSnapshot();
31
+ context.onEvent({
32
+ type: "page_leave",
33
+ occurredAtMs: Date.now(),
34
+ payload: {
35
+ viewId: context.getViewId(),
36
+ screenKey: snapshot.screenKey,
37
+ path: snapshot.path,
38
+ timeOnPageMs: Math.max(0, Date.now() - pageEnteredAtMs),
39
+ },
40
+ });
41
+ pageEnteredAtMs = Date.now();
42
+ };
43
+ try {
44
+ emitPageView();
45
+ }
46
+ catch (error) {
47
+ context.onInternalError("capture/page:initial_page_view", error);
48
+ }
49
+ const visibilityHandler = () => {
50
+ if (document.visibilityState !== "hidden") {
51
+ return;
52
+ }
53
+ try {
54
+ handlePageLeave();
55
+ }
56
+ catch (error) {
57
+ context.onInternalError("capture/page:visibilitychange", error);
58
+ }
59
+ };
60
+ const pageHideHandler = () => {
61
+ try {
62
+ handlePageLeave();
63
+ }
64
+ catch (error) {
65
+ context.onInternalError("capture/page:pagehide", error);
66
+ }
67
+ };
68
+ document.addEventListener("visibilitychange", visibilityHandler, true);
69
+ window.addEventListener("pagehide", pageHideHandler, true);
70
+ window.addEventListener("beforeunload", pageHideHandler, true);
71
+ return {
72
+ stop: () => {
73
+ document.removeEventListener("visibilitychange", visibilityHandler, true);
74
+ window.removeEventListener("pagehide", pageHideHandler, true);
75
+ window.removeEventListener("beforeunload", pageHideHandler, true);
76
+ },
77
+ };
78
+ };
@@ -0,0 +1,2 @@
1
+ import type { CaptureModuleContext, CaptureStopHandle } from "../core/types";
2
+ export declare const startPerformanceCapture: (context: CaptureModuleContext) => CaptureStopHandle;
@@ -0,0 +1,268 @@
1
+ import { FRONTEND_EVENT_NAME } from "../core/contracts";
2
+ const SUMMARY_WINDOW_MS = 60000;
3
+ const LONG_TASK_THRESHOLD_MS = 250;
4
+ const INTERACTION_LONG_TASK_THRESHOLD_MS = 100;
5
+ const INTERACTION_LONG_TASK_WINDOW_MS = 2000;
6
+ const createInitialState = () => ({
7
+ windowStartedAtMs: Date.now(),
8
+ summaryCount: 0,
9
+ droppedCount: 0,
10
+ longTaskCount: 0,
11
+ resourceFailureCount: 0,
12
+ worstDurationMs: 0,
13
+ worstMetricName: null,
14
+ webVitals: {
15
+ lcp_ms: null,
16
+ cls: null,
17
+ fcp_ms: null,
18
+ ttfb_ms: null,
19
+ inp_ms: null,
20
+ },
21
+ loadDurationMs: null,
22
+ interactionReadyMs: null,
23
+ lastInteractionAtMs: null,
24
+ });
25
+ const durationBucket = (value) => {
26
+ if (value < 100) {
27
+ return "lt_100ms";
28
+ }
29
+ if (value < 250) {
30
+ return "100_249ms";
31
+ }
32
+ if (value < 1000) {
33
+ return "250_999ms";
34
+ }
35
+ if (value < 2500) {
36
+ return "1000_2499ms";
37
+ }
38
+ return "gte_2500ms";
39
+ };
40
+ const countBucket = (value) => {
41
+ if (value <= 0) {
42
+ return "0";
43
+ }
44
+ if (value === 1) {
45
+ return "1";
46
+ }
47
+ if (value <= 3) {
48
+ return "2_3";
49
+ }
50
+ if (value <= 10) {
51
+ return "4_10";
52
+ }
53
+ return "gt_10";
54
+ };
55
+ const optionalDurationBucket = (value) => value === null ? null : durationBucket(value);
56
+ const ratingForSummary = (state) => {
57
+ const lcp = state.webVitals.lcp_ms;
58
+ const cls = state.webVitals.cls;
59
+ const inp = state.webVitals.inp_ms;
60
+ if ((lcp !== null && lcp > 4000) ||
61
+ (cls !== null && cls > 0.25) ||
62
+ (inp !== null && inp > 500) ||
63
+ state.worstDurationMs >= 2500) {
64
+ return "poor";
65
+ }
66
+ if ((lcp !== null && lcp > 2500) ||
67
+ (cls !== null && cls > 0.1) ||
68
+ (inp !== null && inp > 200) ||
69
+ state.worstDurationMs >= 1000) {
70
+ return "needs_improvement";
71
+ }
72
+ return "good";
73
+ };
74
+ const updateWorstDuration = (state, metricName, valueMs) => {
75
+ if (!Number.isFinite(valueMs) || valueMs < state.worstDurationMs) {
76
+ return;
77
+ }
78
+ state.worstDurationMs = Math.round(valueMs);
79
+ state.worstMetricName = metricName;
80
+ };
81
+ const observeEntryType = (observers, entryType, callback) => {
82
+ if (typeof PerformanceObserver === "undefined" ||
83
+ !PerformanceObserver.supportedEntryTypes?.includes(entryType)) {
84
+ return;
85
+ }
86
+ try {
87
+ const observer = new PerformanceObserver(callback);
88
+ observer.observe({ type: entryType, buffered: true });
89
+ observers.push(observer);
90
+ }
91
+ catch {
92
+ // Some browsers expose an entry type but still reject buffered observation.
93
+ }
94
+ };
95
+ export const startPerformanceCapture = (context) => {
96
+ if (typeof window === "undefined" || typeof document === "undefined") {
97
+ return { stop: () => undefined };
98
+ }
99
+ let state = createInitialState();
100
+ const observers = [];
101
+ const emitSummary = (flushReason) => {
102
+ if (context.getConsent() === "denied") {
103
+ state = createInitialState();
104
+ return;
105
+ }
106
+ const summaryCount = state.summaryCount;
107
+ if (summaryCount === 0 && state.droppedCount === 0) {
108
+ return;
109
+ }
110
+ const viewId = context.getViewId();
111
+ const summaryWindowMs = Math.max(0, Date.now() - state.windowStartedAtMs);
112
+ context.onEvent({
113
+ type: FRONTEND_EVENT_NAME.performanceSummaryObserved,
114
+ occurredAtMs: Date.now(),
115
+ payload: {
116
+ viewId,
117
+ pageUrl: window.location.href,
118
+ collectorName: "performance_rum",
119
+ aggregationKind: "per_view_window",
120
+ summaryWindowMs,
121
+ summaryCount,
122
+ droppedCount: state.droppedCount,
123
+ sampleRate: 1,
124
+ budgetWindowMs: SUMMARY_WINDOW_MS,
125
+ rateLimitKey: viewId
126
+ ? `view:${viewId}:performance`
127
+ : "view:unknown:performance",
128
+ performanceKind: "view",
129
+ rating: ratingForSummary(state),
130
+ metricName: state.worstMetricName ?? "performance_summary",
131
+ metricValueBucket: durationBucket(state.worstDurationMs),
132
+ loadDurationBucket: optionalDurationBucket(state.loadDurationMs),
133
+ interactionReadyMsBucket: optionalDurationBucket(state.interactionReadyMs),
134
+ longTaskCountBucket: countBucket(state.longTaskCount),
135
+ resourceFailureCountBucket: countBucket(state.resourceFailureCount),
136
+ longTaskCount: state.longTaskCount,
137
+ resourceFailureCount: state.resourceFailureCount,
138
+ slowResourceCount: 0,
139
+ worstDurationBucket: durationBucket(state.worstDurationMs),
140
+ flushReason,
141
+ webVitalBuckets: {
142
+ lcp: optionalDurationBucket(state.webVitals.lcp_ms),
143
+ fcp: optionalDurationBucket(state.webVitals.fcp_ms),
144
+ ttfb: optionalDurationBucket(state.webVitals.ttfb_ms),
145
+ inp: optionalDurationBucket(state.webVitals.inp_ms),
146
+ cls: state.webVitals.cls === null
147
+ ? null
148
+ : state.webVitals.cls > 0.25
149
+ ? "poor"
150
+ : state.webVitals.cls > 0.1
151
+ ? "needs_improvement"
152
+ : "good",
153
+ },
154
+ },
155
+ });
156
+ state = createInitialState();
157
+ };
158
+ const intervalId = window.setInterval(() => {
159
+ emitSummary("interval");
160
+ }, SUMMARY_WINDOW_MS);
161
+ const pageHideHandler = () => {
162
+ emitSummary("pagehide");
163
+ };
164
+ const visibilityHandler = () => {
165
+ if (document.visibilityState === "hidden") {
166
+ emitSummary("visibility_hidden");
167
+ }
168
+ };
169
+ const resourceErrorHandler = (event) => {
170
+ if (context.getConsent() === "denied") {
171
+ return;
172
+ }
173
+ if (!(event.target instanceof Element)) {
174
+ return;
175
+ }
176
+ state.resourceFailureCount += 1;
177
+ state.summaryCount += 1;
178
+ };
179
+ const interactionHandler = () => {
180
+ state.lastInteractionAtMs = Date.now();
181
+ };
182
+ window.addEventListener("error", resourceErrorHandler, true);
183
+ window.addEventListener("pointerdown", interactionHandler, true);
184
+ window.addEventListener("pagehide", pageHideHandler, true);
185
+ document.addEventListener("visibilitychange", visibilityHandler, true);
186
+ observeEntryType(observers, "navigation", (list) => {
187
+ for (const entry of list.getEntries()) {
188
+ const navigation = entry;
189
+ state.summaryCount += 1;
190
+ state.loadDurationMs = Math.max(0, Math.round(navigation.duration));
191
+ updateWorstDuration(state, "page_load", navigation.duration);
192
+ const ttfb = Math.max(0, navigation.responseStart - navigation.requestStart);
193
+ state.webVitals.ttfb_ms = Math.round(ttfb);
194
+ updateWorstDuration(state, "ttfb", ttfb);
195
+ }
196
+ });
197
+ observeEntryType(observers, "paint", (list) => {
198
+ for (const entry of list.getEntries()) {
199
+ if (entry.name !== "first-contentful-paint") {
200
+ continue;
201
+ }
202
+ state.summaryCount += 1;
203
+ state.webVitals.fcp_ms = Math.round(entry.startTime);
204
+ updateWorstDuration(state, "fcp", entry.startTime);
205
+ }
206
+ });
207
+ observeEntryType(observers, "largest-contentful-paint", (list) => {
208
+ for (const entry of list.getEntries()) {
209
+ const lcp = entry;
210
+ const value = lcp.renderTime || lcp.loadTime || lcp.startTime;
211
+ state.summaryCount += 1;
212
+ state.webVitals.lcp_ms = Math.round(value);
213
+ updateWorstDuration(state, "lcp", value);
214
+ }
215
+ });
216
+ observeEntryType(observers, "layout-shift", (list) => {
217
+ for (const entry of list.getEntries()) {
218
+ const shift = entry;
219
+ if (shift.hadRecentInput === true || typeof shift.value !== "number") {
220
+ continue;
221
+ }
222
+ state.summaryCount += 1;
223
+ state.webVitals.cls = Number(((state.webVitals.cls ?? 0) + shift.value).toFixed(4));
224
+ }
225
+ });
226
+ observeEntryType(observers, "event", (list) => {
227
+ for (const entry of list.getEntries()) {
228
+ const eventTiming = entry;
229
+ if (eventTiming.interactionId === undefined) {
230
+ continue;
231
+ }
232
+ state.summaryCount += 1;
233
+ const value = Math.max(0, eventTiming.duration);
234
+ state.webVitals.inp_ms = Math.max(state.webVitals.inp_ms ?? 0, Math.round(value));
235
+ state.interactionReadyMs = Math.max(state.interactionReadyMs ?? 0, Math.round(value));
236
+ updateWorstDuration(state, "inp", value);
237
+ }
238
+ });
239
+ observeEntryType(observers, "longtask", (list) => {
240
+ for (const entry of list.getEntries()) {
241
+ const nearInteraction = state.lastInteractionAtMs !== null &&
242
+ Date.now() - state.lastInteractionAtMs <=
243
+ INTERACTION_LONG_TASK_WINDOW_MS;
244
+ const threshold = nearInteraction
245
+ ? INTERACTION_LONG_TASK_THRESHOLD_MS
246
+ : LONG_TASK_THRESHOLD_MS;
247
+ if (entry.duration < threshold) {
248
+ continue;
249
+ }
250
+ state.longTaskCount += 1;
251
+ state.summaryCount += 1;
252
+ updateWorstDuration(state, "long_task", entry.duration);
253
+ }
254
+ });
255
+ return {
256
+ stop: () => {
257
+ emitSummary("stop");
258
+ window.clearInterval(intervalId);
259
+ window.removeEventListener("error", resourceErrorHandler, true);
260
+ window.removeEventListener("pointerdown", interactionHandler, true);
261
+ window.removeEventListener("pagehide", pageHideHandler, true);
262
+ document.removeEventListener("visibilitychange", visibilityHandler, true);
263
+ for (const observer of observers) {
264
+ observer.disconnect();
265
+ }
266
+ },
267
+ };
268
+ };
@@ -0,0 +1,12 @@
1
+ import type { AccountContext } from "../core/types";
2
+ type StorageLike = Pick<Storage, "getItem" | "setItem" | "removeItem">;
3
+ export declare class AccountManager {
4
+ private readonly storage;
5
+ private state;
6
+ constructor(storage?: StorageLike);
7
+ getContext(): AccountContext;
8
+ setAccount(accountId: string, traits?: Record<string, unknown>): void;
9
+ reset(): void;
10
+ private loadState;
11
+ }
12
+ export {};
@@ -0,0 +1,129 @@
1
+ import { STORAGE_KEYS } from "../core/constants";
2
+ const createMemoryStorage = () => {
3
+ const store = new Map();
4
+ return {
5
+ getItem: (key) => (store.has(key) ? store.get(key) : null),
6
+ setItem: (key, value) => {
7
+ store.set(key, value);
8
+ },
9
+ removeItem: (key) => {
10
+ store.delete(key);
11
+ },
12
+ };
13
+ };
14
+ const resolveStorage = () => {
15
+ try {
16
+ if (typeof globalThis.localStorage !== "undefined") {
17
+ const probeKey = "clue:account_probe";
18
+ globalThis.localStorage.setItem(probeKey, "1");
19
+ globalThis.localStorage.removeItem(probeKey);
20
+ return globalThis.localStorage;
21
+ }
22
+ }
23
+ catch {
24
+ // Ignore and fallback.
25
+ }
26
+ return createMemoryStorage();
27
+ };
28
+ const safeJsonParse = (value) => {
29
+ if (!value) {
30
+ return {};
31
+ }
32
+ try {
33
+ const parsed = JSON.parse(value);
34
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
35
+ return parsed;
36
+ }
37
+ }
38
+ catch {
39
+ // Ignore and fallback.
40
+ }
41
+ return {};
42
+ };
43
+ const detectWorkspaceId = (traits) => {
44
+ const workspaceId = traits.workspace_id ?? traits.workspaceId;
45
+ if (typeof workspaceId === "string" && workspaceId.trim()) {
46
+ return workspaceId;
47
+ }
48
+ return null;
49
+ };
50
+ const detectOrganizationId = (traits) => {
51
+ const organizationId = traits.organization_id ?? traits.organizationId;
52
+ if (typeof organizationId === "string" && organizationId.trim()) {
53
+ return organizationId;
54
+ }
55
+ return null;
56
+ };
57
+ export class AccountManager {
58
+ constructor(storage) {
59
+ this.storage = storage ?? resolveStorage();
60
+ this.state = this.loadState();
61
+ }
62
+ getContext() {
63
+ return {
64
+ accountId: this.state.accountId,
65
+ organizationId: this.state.organizationId,
66
+ workspaceId: this.state.workspaceId,
67
+ traits: { ...this.state.traits },
68
+ };
69
+ }
70
+ setAccount(accountId, traits) {
71
+ const previousTraits = this.state.accountId === accountId ? this.state.traits : {};
72
+ this.state.accountId = accountId;
73
+ this.state.traits = {
74
+ ...previousTraits,
75
+ ...(traits ?? {}),
76
+ };
77
+ this.state.organizationId = detectOrganizationId(this.state.traits);
78
+ this.state.workspaceId = detectWorkspaceId(this.state.traits);
79
+ try {
80
+ this.storage.setItem(STORAGE_KEYS.accountId, accountId);
81
+ this.storage.setItem(STORAGE_KEYS.accountTraits, JSON.stringify(this.state.traits));
82
+ if (this.state.organizationId) {
83
+ this.storage.setItem(STORAGE_KEYS.organizationId, this.state.organizationId);
84
+ }
85
+ else {
86
+ this.storage.removeItem(STORAGE_KEYS.organizationId);
87
+ }
88
+ if (this.state.workspaceId) {
89
+ this.storage.setItem(STORAGE_KEYS.workspaceId, this.state.workspaceId);
90
+ }
91
+ else {
92
+ this.storage.removeItem(STORAGE_KEYS.workspaceId);
93
+ }
94
+ }
95
+ catch {
96
+ // Swallow to keep SDK non-blocking.
97
+ }
98
+ }
99
+ reset() {
100
+ this.state = {
101
+ accountId: null,
102
+ organizationId: null,
103
+ workspaceId: null,
104
+ traits: {},
105
+ };
106
+ try {
107
+ this.storage.removeItem(STORAGE_KEYS.accountId);
108
+ this.storage.removeItem(STORAGE_KEYS.organizationId);
109
+ this.storage.removeItem(STORAGE_KEYS.workspaceId);
110
+ this.storage.removeItem(STORAGE_KEYS.accountTraits);
111
+ }
112
+ catch {
113
+ // Swallow to keep SDK non-blocking.
114
+ }
115
+ }
116
+ loadState() {
117
+ const accountId = this.storage.getItem(STORAGE_KEYS.accountId);
118
+ const traits = safeJsonParse(this.storage.getItem(STORAGE_KEYS.accountTraits));
119
+ const organizationId = this.storage.getItem(STORAGE_KEYS.organizationId) ??
120
+ detectOrganizationId(traits);
121
+ const workspaceId = this.storage.getItem(STORAGE_KEYS.workspaceId) ?? detectWorkspaceId(traits);
122
+ return {
123
+ accountId,
124
+ organizationId,
125
+ workspaceId,
126
+ traits,
127
+ };
128
+ }
129
+ }
@@ -0,0 +1,42 @@
1
+ import type { ClueEnvironment } from "../core/types";
2
+ type EnvironmentOptions = {
3
+ environment: ClueEnvironment;
4
+ frontendRelease: string | null;
5
+ featureFlagsProvider?: () => string[];
6
+ experimentVariantProvider?: () => string | null;
7
+ };
8
+ export type EnvironmentContextValue = {
9
+ environment: ClueEnvironment;
10
+ frontendRelease: string | null;
11
+ featureFlags: string[];
12
+ experimentVariant: string | null;
13
+ locale: string | null;
14
+ country: string | null;
15
+ deviceType: string | null;
16
+ browser: string | null;
17
+ os: string | null;
18
+ referrer: string | null;
19
+ browserFamily: string | null;
20
+ browserMajorVersion: string | null;
21
+ browserEngine: string | null;
22
+ osFamily: string | null;
23
+ osMajorVersion: string | null;
24
+ timezone: string | null;
25
+ pointerTypeCandidate: string | null;
26
+ touchCapable: boolean | null;
27
+ cookiesEnabled: boolean | null;
28
+ localStorageAvailable: boolean | null;
29
+ sessionStorageAvailable: boolean | null;
30
+ serviceWorkerSupported: boolean | null;
31
+ webglSupported: boolean | null;
32
+ reducedMotion: string | null;
33
+ colorScheme: string | null;
34
+ };
35
+ export declare class EnvironmentManager {
36
+ private readonly options;
37
+ constructor(options: EnvironmentOptions);
38
+ getContext(): EnvironmentContextValue;
39
+ private safeFeatureFlags;
40
+ private safeExperimentVariant;
41
+ }
42
+ export {};