@guidelytech/sdk-core 0.7.0 → 0.9.0

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.
@@ -0,0 +1,214 @@
1
+ import {
2
+ readJson,
3
+ writeJson
4
+ } from "./chunk-CYAG6ZYX.js";
5
+ import {
6
+ __publicField,
7
+ backoffMs
8
+ } from "./chunk-AKNE7D5M.js";
9
+
10
+ // src/queue.ts
11
+ var EventQueue = class {
12
+ constructor(opts) {
13
+ __publicField(this, "opts", opts);
14
+ __publicField(this, "events", []);
15
+ __publicField(this, "timer", null);
16
+ __publicField(this, "retryTimer", null);
17
+ __publicField(this, "inflight", false);
18
+ __publicField(this, "inflightIds", /* @__PURE__ */ new Set());
19
+ __publicField(this, "attempt", 0);
20
+ __publicField(this, "destroyed", false);
21
+ __publicField(this, "flushIntervalMs");
22
+ __publicField(this, "maxQueue");
23
+ __publicField(this, "batchSize");
24
+ __publicField(this, "maxAttempts");
25
+ this.flushIntervalMs = opts.flushIntervalMs ?? 2e3;
26
+ this.maxQueue = opts.maxQueue ?? 200;
27
+ this.batchSize = opts.batchSize ?? 50;
28
+ this.maxAttempts = opts.maxAttempts ?? 5;
29
+ const persisted = readJson(opts.store, opts.storageKey, []);
30
+ if (Array.isArray(persisted)) this.events = persisted.filter((e) => e && typeof e.eventId === "string").slice(-this.maxQueue);
31
+ if (this.events.length) this.schedule();
32
+ }
33
+ get size() {
34
+ return this.events.length;
35
+ }
36
+ /** Pending events (read-only copy, for debugging). */
37
+ peek() {
38
+ return this.events.slice();
39
+ }
40
+ enqueue(event) {
41
+ if (this.destroyed) return;
42
+ if (this.events.some((e) => e.eventId === event.eventId)) return;
43
+ this.events.push(event);
44
+ if (this.events.length > this.maxQueue) this.events.splice(0, this.events.length - this.maxQueue);
45
+ this.persist();
46
+ this.schedule();
47
+ }
48
+ persist() {
49
+ writeJson(this.opts.store, this.opts.storageKey, this.events);
50
+ }
51
+ schedule() {
52
+ if (this.timer || this.retryTimer || this.destroyed) return;
53
+ this.timer = setTimeout(() => {
54
+ this.timer = null;
55
+ void this.flush();
56
+ }, this.flushIntervalMs);
57
+ }
58
+ /** Sends queued events now (one batch at a time). Safe to call at any time. */
59
+ async flush() {
60
+ if (this.destroyed || this.inflight || !this.events.length || !this.opts.enabled()) return;
61
+ if (this.timer) {
62
+ clearTimeout(this.timer);
63
+ this.timer = null;
64
+ }
65
+ this.inflight = true;
66
+ const batch = this.events.slice(0, this.batchSize);
67
+ this.inflightIds = new Set(batch.map((e) => e.eventId));
68
+ let ok = false;
69
+ let retry = true;
70
+ try {
71
+ const res = await this.opts.send(batch);
72
+ ok = res.ok;
73
+ retry = res.retry ?? !res.ok;
74
+ } catch (err) {
75
+ this.opts.onError?.(err);
76
+ }
77
+ this.inflight = false;
78
+ this.inflightIds.clear();
79
+ if (this.destroyed) return;
80
+ if (ok || !retry) {
81
+ const ids = new Set(batch.map((e) => e.eventId));
82
+ this.events = this.events.filter((e) => !ids.has(e.eventId));
83
+ this.attempt = 0;
84
+ this.persist();
85
+ if (this.events.length) this.schedule();
86
+ return;
87
+ }
88
+ this.attempt += 1;
89
+ if (this.attempt >= this.maxAttempts) {
90
+ this.attempt = 0;
91
+ return;
92
+ }
93
+ this.retryTimer = setTimeout(() => {
94
+ this.retryTimer = null;
95
+ void this.flush();
96
+ }, backoffMs(this.attempt - 1));
97
+ }
98
+ /** Best-effort synchronous flush on unload using `sendBeacon`. */
99
+ flushSync() {
100
+ if (this.destroyed || !this.opts.enabled() || !this.opts.beacon) return;
101
+ const batch = this.events.filter((e) => !this.inflightIds.has(e.eventId)).slice(0, this.batchSize);
102
+ if (!batch.length) return;
103
+ if (this.opts.beacon(batch)) {
104
+ const ids = new Set(batch.map((e) => e.eventId));
105
+ this.events = this.events.filter((e) => !ids.has(e.eventId));
106
+ this.persist();
107
+ }
108
+ }
109
+ clear() {
110
+ this.events = [];
111
+ this.persist();
112
+ }
113
+ destroy() {
114
+ this.destroyed = true;
115
+ if (this.timer) clearTimeout(this.timer);
116
+ if (this.retryTimer) clearTimeout(this.retryTimer);
117
+ this.timer = null;
118
+ this.retryTimer = null;
119
+ this.persist();
120
+ }
121
+ };
122
+
123
+ // src/emitter.ts
124
+ var Emitter = class {
125
+ constructor(onError) {
126
+ __publicField(this, "onError", onError);
127
+ __publicField(this, "listeners", /* @__PURE__ */ new Map());
128
+ }
129
+ on(event, cb) {
130
+ let set = this.listeners.get(event);
131
+ if (!set) {
132
+ set = /* @__PURE__ */ new Set();
133
+ this.listeners.set(event, set);
134
+ }
135
+ set.add(cb);
136
+ return () => this.off(event, cb);
137
+ }
138
+ off(event, cb) {
139
+ this.listeners.get(event)?.delete(cb);
140
+ }
141
+ emit(event, ...args) {
142
+ const set = this.listeners.get(event);
143
+ if (!set) return;
144
+ for (const cb of Array.from(set)) {
145
+ try {
146
+ cb(...args);
147
+ } catch (err) {
148
+ this.onError?.(err);
149
+ }
150
+ }
151
+ }
152
+ clear() {
153
+ this.listeners.clear();
154
+ }
155
+ };
156
+
157
+ // src/frequency.ts
158
+ var HOUR = 3600 * 1e3;
159
+ function isFrequencyEligible(freq, state, shownThisSession, now = Date.now()) {
160
+ const s = state ?? { impressions: 0 };
161
+ if (freq.maxImpressions != null && s.impressions >= freq.maxImpressions) return false;
162
+ if (s.completedAt && freq.mode !== "every_time") return false;
163
+ if (s.dismissedAt) {
164
+ if (!freq.reshowAfterDismiss) return false;
165
+ if (freq.cooldownHours > 0 && now - Date.parse(s.dismissedAt) < freq.cooldownHours * HOUR) return false;
166
+ }
167
+ switch (freq.mode) {
168
+ case "once":
169
+ return s.impressions === 0 || !!s.dismissedAt && freq.reshowAfterDismiss;
170
+ case "once_per_session":
171
+ return !shownThisSession;
172
+ case "interval": {
173
+ if (!s.lastShownAt) return true;
174
+ const hours = freq.intervalHours ?? 24;
175
+ return now - Date.parse(s.lastShownAt) >= hours * HOUR;
176
+ }
177
+ case "every_time":
178
+ default:
179
+ return true;
180
+ }
181
+ }
182
+ function isExclusiveGuide(guide) {
183
+ if (guide.kind === "banner" || guide.kind === "checklist") return false;
184
+ const steps = guide.config.steps;
185
+ if (steps.length > 0 && steps.every((s) => s.type === "hotspot" || s.type === "banner")) return false;
186
+ return true;
187
+ }
188
+ function areMutuallyExclusive(a, b) {
189
+ const la = a.config.frequency.mutuallyExclusiveWith;
190
+ const lb = b.config.frequency.mutuallyExclusiveWith;
191
+ return la.includes(b.id) || la.includes(b.key) || lb.includes(a.id) || lb.includes(a.key);
192
+ }
193
+ function isWithinSchedule(guide, now = Date.now()) {
194
+ const s = guide.config.survey;
195
+ if (!s) return true;
196
+ if (s.startAt && now < Date.parse(s.startAt)) return false;
197
+ if (s.endAt && now > Date.parse(s.endAt)) return false;
198
+ return true;
199
+ }
200
+ function dismissedAfterMerge(local, server) {
201
+ if (server.dismissResetAt && server.dismissResetAt !== local?.dismissResetAt) return server.dismissedAt ?? null;
202
+ return local?.dismissedAt ?? server.dismissedAt ?? null;
203
+ }
204
+
205
+ export {
206
+ EventQueue,
207
+ Emitter,
208
+ isFrequencyEligible,
209
+ isExclusiveGuide,
210
+ areMutuallyExclusive,
211
+ isWithinSchedule,
212
+ dismissedAfterMerge
213
+ };
214
+ //# sourceMappingURL=chunk-544KSOC3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/queue.ts","../src/emitter.ts","../src/frequency.ts"],"sourcesContent":["import type { SdkEvent } from \"@guidelytech/shared\";\nimport { backoffMs } from \"./http\";\nimport { readJson, writeJson, type KeyValueStore } from \"./storage\";\n\nexport interface EventQueueOptions {\n store: KeyValueStore;\n storageKey: string;\n /** Sends one batch. Resolve `{ ok: true }` on success, `{ ok: false, retry }` otherwise. */\n send: (events: SdkEvent[]) => Promise<{ ok: boolean; retry?: boolean }>;\n /** Synchronous best-effort send used on page hide. */\n beacon?: (events: SdkEvent[]) => boolean;\n /** Should events be flushed right now? (analytics enabled + consent granted). */\n enabled: () => boolean;\n flushIntervalMs?: number;\n maxQueue?: number;\n batchSize?: number;\n maxAttempts?: number;\n onError?: (err: unknown) => void;\n}\n\n/**\n * Bounded, persisted event queue. Batches ≤50 events every 2s (or on demand), dedupes by\n * `eventId`, retries with exponential backoff (1s→30s, 5 attempts) and keeps failed events for\n * the next flush. Drops the oldest events once the queue exceeds 200.\n */\nexport class EventQueue {\n private events: SdkEvent[] = [];\n private timer: ReturnType<typeof setTimeout> | null = null;\n private retryTimer: ReturnType<typeof setTimeout> | null = null;\n private inflight = false;\n private inflightIds = new Set<string>();\n private attempt = 0;\n private destroyed = false;\n private readonly flushIntervalMs: number;\n private readonly maxQueue: number;\n private readonly batchSize: number;\n private readonly maxAttempts: number;\n\n constructor(private readonly opts: EventQueueOptions) {\n this.flushIntervalMs = opts.flushIntervalMs ?? 2000;\n this.maxQueue = opts.maxQueue ?? 200;\n this.batchSize = opts.batchSize ?? 50;\n this.maxAttempts = opts.maxAttempts ?? 5;\n const persisted = readJson<SdkEvent[]>(opts.store, opts.storageKey, []);\n if (Array.isArray(persisted)) this.events = persisted.filter((e) => e && typeof e.eventId === \"string\").slice(-this.maxQueue);\n // Events left over from a previous page load are flushed on the next tick.\n if (this.events.length) this.schedule();\n }\n\n get size(): number {\n return this.events.length;\n }\n\n /** Pending events (read-only copy, for debugging). */\n peek(): SdkEvent[] {\n return this.events.slice();\n }\n\n enqueue(event: SdkEvent): void {\n if (this.destroyed) return;\n if (this.events.some((e) => e.eventId === event.eventId)) return;\n this.events.push(event);\n if (this.events.length > this.maxQueue) this.events.splice(0, this.events.length - this.maxQueue);\n this.persist();\n this.schedule();\n }\n\n private persist(): void {\n writeJson(this.opts.store, this.opts.storageKey, this.events);\n }\n\n private schedule(): void {\n if (this.timer || this.retryTimer || this.destroyed) return;\n this.timer = setTimeout(() => {\n this.timer = null;\n void this.flush();\n }, this.flushIntervalMs);\n }\n\n /** Sends queued events now (one batch at a time). Safe to call at any time. */\n async flush(): Promise<void> {\n if (this.destroyed || this.inflight || !this.events.length || !this.opts.enabled()) return;\n if (this.timer) {\n clearTimeout(this.timer);\n this.timer = null;\n }\n this.inflight = true;\n const batch = this.events.slice(0, this.batchSize);\n this.inflightIds = new Set(batch.map((e) => e.eventId));\n let ok = false;\n let retry = true;\n try {\n const res = await this.opts.send(batch);\n ok = res.ok;\n retry = res.retry ?? !res.ok;\n } catch (err) {\n this.opts.onError?.(err);\n }\n this.inflight = false;\n this.inflightIds.clear();\n if (this.destroyed) return;\n if (ok || !retry) {\n const ids = new Set(batch.map((e) => e.eventId));\n this.events = this.events.filter((e) => !ids.has(e.eventId));\n this.attempt = 0;\n this.persist();\n if (this.events.length) this.schedule();\n return;\n }\n this.attempt += 1;\n if (this.attempt >= this.maxAttempts) {\n // Give up for now; events stay queued until the next flush trigger (navigation, timer, unload).\n this.attempt = 0;\n return;\n }\n this.retryTimer = setTimeout(() => {\n this.retryTimer = null;\n void this.flush();\n }, backoffMs(this.attempt - 1));\n }\n\n /** Best-effort synchronous flush on unload using `sendBeacon`. */\n flushSync(): void {\n if (this.destroyed || !this.opts.enabled() || !this.opts.beacon) return;\n // Events already in flight (fetch with keepalive) survive unload; only beacon the rest to avoid duplicate batches.\n const batch = this.events.filter((e) => !this.inflightIds.has(e.eventId)).slice(0, this.batchSize);\n if (!batch.length) return;\n if (this.opts.beacon(batch)) {\n const ids = new Set(batch.map((e) => e.eventId));\n this.events = this.events.filter((e) => !ids.has(e.eventId));\n this.persist();\n }\n }\n\n clear(): void {\n this.events = [];\n this.persist();\n }\n\n destroy(): void {\n this.destroyed = true;\n if (this.timer) clearTimeout(this.timer);\n if (this.retryTimer) clearTimeout(this.retryTimer);\n this.timer = null;\n this.retryTimer = null;\n this.persist();\n }\n}\n","type AnyFn = (...args: any[]) => void;\n\n/** Tiny typed event emitter. Listener errors are reported, never thrown into the caller. */\nexport class Emitter<Events extends { [K in keyof Events]: AnyFn }> {\n private listeners = new Map<keyof Events, Set<AnyFn>>();\n constructor(private readonly onError?: (err: unknown) => void) {}\n\n on<K extends keyof Events>(event: K, cb: Events[K]): () => void {\n let set = this.listeners.get(event);\n if (!set) {\n set = new Set();\n this.listeners.set(event, set);\n }\n set.add(cb);\n return () => this.off(event, cb);\n }\n\n off<K extends keyof Events>(event: K, cb: Events[K]): void {\n this.listeners.get(event)?.delete(cb);\n }\n\n emit<K extends keyof Events>(event: K, ...args: Parameters<Events[K]>): void {\n const set = this.listeners.get(event);\n if (!set) return;\n for (const cb of Array.from(set)) {\n try {\n cb(...args);\n } catch (err) {\n this.onError?.(err);\n }\n }\n }\n\n clear(): void {\n this.listeners.clear();\n }\n}\n","import type { FrequencyConfig, PublishedGuide } from \"@guidelytech/shared\";\n\n/** Per-guide state kept locally and merged with the server's `IdentifyResponse.guideState`. */\nexport interface GuideLocalState {\n impressions: number;\n completedAt?: string | null;\n dismissedAt?: string | null;\n lastShownAt?: string | null;\n /** Last step reached in a multi-step flow (local only), so the messenger can offer to resume it. */\n lastStepId?: string | null;\n}\n\nconst HOUR = 3600 * 1000;\n\n/**\n * Pure frequency evaluation. `shownThisSession` comes from session storage.\n * - `once`: a single impression ever (unless `reshowAfterDismiss` and it was only dismissed).\n * - `once_per_session`: at most once per browser session.\n * - `every_time`: on every trigger (still blocked by completion/dismissal rules below).\n * - `interval`: at most once every `intervalHours`.\n * `maxImpressions` caps total impressions; dismissal blocks re-showing unless `reshowAfterDismiss`\n * (then `cooldownHours` must have elapsed); completion blocks unless mode is `every_time`.\n */\nexport function isFrequencyEligible(freq: FrequencyConfig, state: GuideLocalState | undefined, shownThisSession: boolean, now: number = Date.now()): boolean {\n const s = state ?? { impressions: 0 };\n if (freq.maxImpressions != null && s.impressions >= freq.maxImpressions) return false;\n if (s.completedAt && freq.mode !== \"every_time\") return false;\n if (s.dismissedAt) {\n if (!freq.reshowAfterDismiss) return false;\n if (freq.cooldownHours > 0 && now - Date.parse(s.dismissedAt) < freq.cooldownHours * HOUR) return false;\n }\n switch (freq.mode) {\n case \"once\":\n return s.impressions === 0 || (!!s.dismissedAt && freq.reshowAfterDismiss);\n case \"once_per_session\":\n return !shownThisSession;\n case \"interval\": {\n if (!s.lastShownAt) return true;\n const hours = freq.intervalHours ?? 24;\n return now - Date.parse(s.lastShownAt) >= hours * HOUR;\n }\n case \"every_time\":\n default:\n return true;\n }\n}\n\n/**\n * Exclusive guides (flows, modals, tooltips, announcements, surveys) run one at a time.\n * Banners, checklists and hotspot-only tooltips can coexist with an active flow.\n */\nexport function isExclusiveGuide(guide: PublishedGuide): boolean {\n if (guide.kind === \"banner\" || guide.kind === \"checklist\") return false;\n const steps = guide.config.steps;\n if (steps.length > 0 && steps.every((s) => s.type === \"hotspot\" || s.type === \"banner\")) return false;\n return true;\n}\n\n/** Is `a` mutually exclusive with `b` (by id or key, in either direction)? */\nexport function areMutuallyExclusive(a: PublishedGuide, b: PublishedGuide): boolean {\n const la = a.config.frequency.mutuallyExclusiveWith;\n const lb = b.config.frequency.mutuallyExclusiveWith;\n return la.includes(b.id) || la.includes(b.key) || lb.includes(a.id) || lb.includes(a.key);\n}\n\n/** Survey scheduling window. */\nexport function isWithinSchedule(guide: PublishedGuide, now: number = Date.now()): boolean {\n const s = guide.config.survey;\n if (!s) return true;\n if (s.startAt && now < Date.parse(s.startAt)) return false;\n if (s.endAt && now > Date.parse(s.endAt)) return false;\n return true;\n}\n\n/**\n * A checklist's dismissal after merging server state. A local dismissal normally wins (it may not have\n * synced yet), but when the server reports a reset this device hasn't applied, the server's value wins:\n * null, or a dismissal made on another device after the reset. Compared as tokens, not times, so a\n * skewed device clock can't keep a reset dismissal.\n */\nexport function dismissedAfterMerge(local: { dismissedAt?: string | null; dismissResetAt?: string | null } | undefined, server: { dismissedAt?: string | null; dismissResetAt?: string | null }): string | null {\n if (server.dismissResetAt && server.dismissResetAt !== local?.dismissResetAt) return server.dismissedAt ?? null;\n return local?.dismissedAt ?? server.dismissedAt ?? null;\n}\n"],"mappings":"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","names":[]}
package/dist/headless.cjs CHANGED
@@ -28,6 +28,7 @@ __export(headless_exports, {
28
28
  KEYS: () => KEYS,
29
29
  areMutuallyExclusive: () => areMutuallyExclusive,
30
30
  backoffMs: () => backoffMs,
31
+ dismissedAfterMerge: () => dismissedAfterMerge,
31
32
  isExclusiveGuide: () => isExclusiveGuide,
32
33
  isFrequencyEligible: () => isFrequencyEligible,
33
34
  isWithinSchedule: () => isWithinSchedule,
@@ -416,6 +417,10 @@ function isWithinSchedule(guide, now = Date.now()) {
416
417
  if (s.endAt && now > Date.parse(s.endAt)) return false;
417
418
  return true;
418
419
  }
420
+ function dismissedAfterMerge(local, server) {
421
+ if (server.dismissResetAt && server.dismissResetAt !== local?.dismissResetAt) return server.dismissedAt ?? null;
422
+ return local?.dismissedAt ?? server.dismissedAt ?? null;
423
+ }
419
424
  // Annotate the CommonJS export names for ESM import in node:
420
425
  0 && (module.exports = {
421
426
  Api,
@@ -424,6 +429,7 @@ function isWithinSchedule(guide, now = Date.now()) {
424
429
  KEYS,
425
430
  areMutuallyExclusive,
426
431
  backoffMs,
432
+ dismissedAfterMerge,
427
433
  isExclusiveGuide,
428
434
  isFrequencyEligible,
429
435
  isWithinSchedule,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/headless.ts","../src/http.ts","../src/storage.ts","../src/queue.ts","../src/emitter.ts","../src/frequency.ts"],"sourcesContent":["/**\n * `@guidelytech/sdk-core/headless` — the DOM-free building blocks of the SDK (HTTP client, event\n * queue, frequency rules, emitter, key/value storage helpers). Used by the React Native SDK, which\n * renders guides natively instead of through `@guidelytech/guide-runtime`.\n */\nexport { Api, postWithRetry, backoffMs, sleep } from \"./http\";\nexport type { ApiResponse, FetchLike } from \"./http\";\nexport { EventQueue } from \"./queue\";\nexport type { EventQueueOptions } from \"./queue\";\nexport { Emitter } from \"./emitter\";\nexport { isFrequencyEligible, isExclusiveGuide, areMutuallyExclusive, isWithinSchedule } from \"./frequency\";\nexport type { GuideLocalState } from \"./frequency\";\nexport { memoryStore, readJson, writeJson, KEYS } from \"./storage\";\nexport type { KeyValueStore } from \"./storage\";\nexport type { ChecklistProgressState, PersistedState, SessionState, ConsentState, GuidelyAccount, GuidelyUser, GuidelyEnvironment } from \"./types\";\n","export interface ApiResponse<T> {\n status: number;\n ok: boolean;\n data: T | null;\n etag: string | null;\n}\n\nexport type FetchLike = typeof fetch;\n\n/** Thin HTTP client for the public SDK API. Adds the public key as query + header. */\nexport class Api {\n constructor(\n private readonly baseUrl: string,\n private readonly key: string,\n private readonly fetchImpl?: FetchLike,\n ) {}\n\n url(path: string, params: Record<string, string | undefined> = {}): string {\n // Built by hand rather than with URL/searchParams, which React Native does not implement.\n const query = Object.entries({ key: this.key, ...params })\n .filter((e): e is [string, string] => e[1] != null)\n .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)\n .join(\"&\");\n return `${this.baseUrl.replace(/\\/$/, \"\")}/api/v1/sdk${path}?${query}`;\n }\n\n private get fetcher(): FetchLike | null {\n if (this.fetchImpl) return this.fetchImpl;\n return typeof fetch === \"function\" ? fetch.bind(globalThis) : null;\n }\n\n async request<T>(method: \"GET\" | \"POST\", path: string, options: { body?: unknown; etag?: string | null; params?: Record<string, string | undefined>; keepalive?: boolean; timeoutMs?: number; headers?: Record<string, string> } = {}): Promise<ApiResponse<T>> {\n const f = this.fetcher;\n if (!f) throw new Error(\"fetch is not available\");\n const headers: Record<string, string> = { ...options.headers, \"X-Guidely-Key\": this.key, Accept: \"application/json\" };\n // Multipart bodies set their own Content-Type (with the boundary).\n const form = typeof FormData !== \"undefined\" && options.body instanceof FormData;\n if (options.body !== undefined && !form) headers[\"Content-Type\"] = \"application/json\";\n if (options.etag) headers[\"If-None-Match\"] = options.etag;\n const ac = typeof AbortController !== \"undefined\" ? new AbortController() : null;\n const timer = ac ? setTimeout(() => ac.abort(), options.timeoutMs ?? 10000) : null;\n try {\n const res = await f(this.url(path, options.params), {\n method,\n headers,\n body: options.body === undefined ? undefined : form ? (options.body as FormData) : JSON.stringify(options.body),\n keepalive: options.keepalive,\n signal: ac?.signal,\n credentials: \"omit\",\n mode: \"cors\",\n });\n let data: T | null = null;\n if (res.status !== 304 && res.status !== 204) {\n try {\n data = (await res.json()) as T;\n } catch {\n data = null;\n }\n }\n return { status: res.status, ok: res.ok, data, etag: res.headers?.get?.(\"etag\") ?? null };\n } finally {\n if (timer) clearTimeout(timer);\n }\n }\n\n get<T>(path: string, options?: { etag?: string | null; params?: Record<string, string | undefined>; headers?: Record<string, string> }): Promise<ApiResponse<T>> {\n return this.request<T>(\"GET\", path, options);\n }\n\n post<T>(path: string, body: unknown, options?: { params?: Record<string, string | undefined>; keepalive?: boolean; headers?: Record<string, string> }): Promise<ApiResponse<T>> {\n return this.request<T>(\"POST\", path, { body, ...options });\n }\n\n /** Multipart POST (file uploads). No Content-Type header: the browser adds it with the boundary. */\n postForm<T>(path: string, form: FormData, options?: { params?: Record<string, string | undefined>; timeoutMs?: number }): Promise<ApiResponse<T>> {\n return this.request<T>(\"POST\", path, { body: form, ...options });\n }\n\n /**\n * POST that may answer with server-sent events. Each event is passed to `onEvent`; a JSON answer is\n * returned instead. No timeout: Scout replies can take a while and the server keeps the stream alive.\n */\n async stream<T>(path: string, body: unknown, onEvent: (event: string, data: unknown) => void, headers: Record<string, string> = {}): Promise<ApiResponse<T>> {\n const f = this.fetcher;\n if (!f) throw new Error(\"fetch is not available\");\n const res = await f(this.url(path), {\n method: \"POST\",\n headers: { ...headers, \"X-Guidely-Key\": this.key, Accept: \"text/event-stream, application/json\", \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n credentials: \"omit\",\n mode: \"cors\",\n });\n const type = res.headers?.get?.(\"content-type\") ?? \"\";\n if (!type.includes(\"text/event-stream\") || !res.body) {\n let data: T | null = null;\n try {\n data = (await res.json()) as T;\n } catch {\n data = null;\n }\n return { status: res.status, ok: res.ok, data, etag: null };\n }\n await readEventStream(res.body, onEvent);\n return { status: res.status, ok: res.ok, data: null, etag: null };\n }\n\n /** `navigator.sendBeacon` when available (page unload); returns false when it could not be queued. */\n beacon(path: string, body: unknown): boolean {\n try {\n if (typeof navigator === \"undefined\" || typeof navigator.sendBeacon !== \"function\") return false;\n // text/plain is a CORS-safelisted type: cross-origin beacons need no preflight (which\n // browsers drop during unload). The API parses the body regardless of content type.\n const blob = new Blob([JSON.stringify(body)], { type: \"text/plain;charset=UTF-8\" });\n return navigator.sendBeacon(this.url(path), blob);\n } catch {\n return false;\n }\n }\n}\n\nexport const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));\n\n/** Exponential backoff helper: 1s → 2s → 4s … capped. */\nexport function backoffMs(attempt: number, baseMs = 1000, maxMs = 30000): number {\n return Math.min(maxMs, baseMs * 2 ** Math.max(0, attempt));\n}\n\n/**\n * Sends with bounded retries: network errors, 429 and 5xx are retried with backoff; success and\n * other 4xx answers are returned as they are. Null when every attempt failed.\n */\nexport async function withRetry<T>(send: () => Promise<ApiResponse<T>>, attempts = 3, baseMs = 500): Promise<ApiResponse<T> | null> {\n for (let i = 0; i < attempts; i++) {\n try {\n const res = await send();\n if (res.ok || (res.status >= 400 && res.status < 500 && res.status !== 429)) return res;\n } catch {\n /* network error → retry */\n }\n if (i < attempts - 1) await sleep(backoffMs(i, baseMs, 8000));\n }\n return null;\n}\n\n/** Fire-and-forget POST with bounded retries (used for state sync; never blocks UI). */\nexport function postWithRetry<T>(api: Api, path: string, body: unknown, attempts = 3, baseMs = 500): Promise<ApiResponse<T> | null> {\n return withRetry(() => api.post<T>(path, body), attempts, baseMs);\n}\n\n/** Parses a `text/event-stream` body (`event:` + single-line JSON `data:`; comment lines ignored). */\nexport async function readEventStream(body: ReadableStream<Uint8Array>, onEvent: (event: string, data: unknown) => void): Promise<void> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n const dispatch = (raw: string) => {\n let event = \"message\";\n const data: string[] = [];\n for (const line of raw.split(\"\\n\")) {\n if (!line || line.startsWith(\":\")) continue;\n const i = line.indexOf(\":\");\n const field = i < 0 ? line : line.slice(0, i);\n const value = i < 0 ? \"\" : line.slice(i + 1).replace(/^ /, \"\");\n if (field === \"event\") event = value;\n else if (field === \"data\") data.push(value);\n }\n if (!data.length) return;\n try {\n onEvent(event, JSON.parse(data.join(\"\\n\")));\n } catch {\n /* ignore malformed events */\n }\n };\n for (;;) {\n const { done, value } = await reader.read();\n if (value) buffer += decoder.decode(value, { stream: true }).replace(/\\r\\n?/g, \"\\n\");\n let sep: number;\n while ((sep = buffer.indexOf(\"\\n\\n\")) >= 0) {\n dispatch(buffer.slice(0, sep));\n buffer = buffer.slice(sep + 2);\n }\n if (done) break;\n }\n if (buffer.trim()) dispatch(buffer);\n}\n","export type StorageKind = \"local\" | \"session\" | \"memory\";\n\n/** Minimal synchronous key/value store; never throws (private mode, quota, SSR). */\nexport interface KeyValueStore {\n get(key: string): string | null;\n set(key: string, value: string): void;\n remove(key: string): void;\n}\n\nexport function memoryStore(): KeyValueStore {\n const map = new Map<string, string>();\n return { get: (k) => map.get(k) ?? null, set: (k, v) => void map.set(k, v), remove: (k) => void map.delete(k) };\n}\n\nfunction webStore(area: \"localStorage\" | \"sessionStorage\"): KeyValueStore | null {\n try {\n if (typeof window === \"undefined\") return null;\n const s = window[area];\n if (!s) return null;\n const probe = \"__guidely_probe__\";\n s.setItem(probe, \"1\");\n s.removeItem(probe);\n return {\n get: (k) => {\n try {\n return s.getItem(k);\n } catch {\n return null;\n }\n },\n set: (k, v) => {\n try {\n s.setItem(k, v);\n } catch {\n /* quota / private mode */\n }\n },\n remove: (k) => {\n try {\n s.removeItem(k);\n } catch {\n /* ignore */\n }\n },\n };\n } catch {\n return null;\n }\n}\n\n/** Creates a store of the requested kind, degrading to memory when unavailable. */\nexport function createStorage(kind: StorageKind = \"local\"): KeyValueStore {\n if (kind === \"memory\") return memoryStore();\n return webStore(kind === \"local\" ? \"localStorage\" : \"sessionStorage\") ?? memoryStore();\n}\n\nexport function readJson<T>(store: KeyValueStore, key: string, fallback: T): T {\n const raw = store.get(key);\n if (raw == null) return fallback;\n try {\n return JSON.parse(raw) as T;\n } catch {\n return fallback;\n }\n}\n\nexport function writeJson(store: KeyValueStore, key: string, value: unknown): void {\n try {\n store.set(key, JSON.stringify(value));\n } catch {\n /* ignore */\n }\n}\n\n/** Storage keys used by the SDK (documented for integrators). */\nexport const KEYS = {\n anon: \"guidely:anon\",\n seen: \"guidely:seen\",\n config: (key: string) => `guidely:config:${key}`,\n etag: (key: string) => `guidely:etag:${key}`,\n state: (key: string) => `guidely:state:${key}`,\n user: (key: string) => `guidely:user:${key}`,\n queue: (key: string) => `guidely:queue:${key}`,\n session: \"guidely:session\",\n active: \"guidely:active\",\n authoring: \"guidely:authoring\",\n /** How-to capture session marker ({ token, … }) and its not-yet-uploaded steps. */\n capture: \"guidely:capture\",\n capturePending: \"guidely:capture:pending\",\n /** Messenger conversations this browser can read: [{ id, token }]. */\n conversations: (key: string) => `guidely:conversations:${key}`,\n /** Announcement ids opened in the messenger's News tab. */\n newsSeen: (key: string) => `guidely:news:${key}`,\n} as const;\n","import type { SdkEvent } from \"@guidelytech/shared\";\nimport { backoffMs } from \"./http\";\nimport { readJson, writeJson, type KeyValueStore } from \"./storage\";\n\nexport interface EventQueueOptions {\n store: KeyValueStore;\n storageKey: string;\n /** Sends one batch. Resolve `{ ok: true }` on success, `{ ok: false, retry }` otherwise. */\n send: (events: SdkEvent[]) => Promise<{ ok: boolean; retry?: boolean }>;\n /** Synchronous best-effort send used on page hide. */\n beacon?: (events: SdkEvent[]) => boolean;\n /** Should events be flushed right now? (analytics enabled + consent granted). */\n enabled: () => boolean;\n flushIntervalMs?: number;\n maxQueue?: number;\n batchSize?: number;\n maxAttempts?: number;\n onError?: (err: unknown) => void;\n}\n\n/**\n * Bounded, persisted event queue. Batches ≤50 events every 2s (or on demand), dedupes by\n * `eventId`, retries with exponential backoff (1s→30s, 5 attempts) and keeps failed events for\n * the next flush. Drops the oldest events once the queue exceeds 200.\n */\nexport class EventQueue {\n private events: SdkEvent[] = [];\n private timer: ReturnType<typeof setTimeout> | null = null;\n private retryTimer: ReturnType<typeof setTimeout> | null = null;\n private inflight = false;\n private inflightIds = new Set<string>();\n private attempt = 0;\n private destroyed = false;\n private readonly flushIntervalMs: number;\n private readonly maxQueue: number;\n private readonly batchSize: number;\n private readonly maxAttempts: number;\n\n constructor(private readonly opts: EventQueueOptions) {\n this.flushIntervalMs = opts.flushIntervalMs ?? 2000;\n this.maxQueue = opts.maxQueue ?? 200;\n this.batchSize = opts.batchSize ?? 50;\n this.maxAttempts = opts.maxAttempts ?? 5;\n const persisted = readJson<SdkEvent[]>(opts.store, opts.storageKey, []);\n if (Array.isArray(persisted)) this.events = persisted.filter((e) => e && typeof e.eventId === \"string\").slice(-this.maxQueue);\n // Events left over from a previous page load are flushed on the next tick.\n if (this.events.length) this.schedule();\n }\n\n get size(): number {\n return this.events.length;\n }\n\n /** Pending events (read-only copy, for debugging). */\n peek(): SdkEvent[] {\n return this.events.slice();\n }\n\n enqueue(event: SdkEvent): void {\n if (this.destroyed) return;\n if (this.events.some((e) => e.eventId === event.eventId)) return;\n this.events.push(event);\n if (this.events.length > this.maxQueue) this.events.splice(0, this.events.length - this.maxQueue);\n this.persist();\n this.schedule();\n }\n\n private persist(): void {\n writeJson(this.opts.store, this.opts.storageKey, this.events);\n }\n\n private schedule(): void {\n if (this.timer || this.retryTimer || this.destroyed) return;\n this.timer = setTimeout(() => {\n this.timer = null;\n void this.flush();\n }, this.flushIntervalMs);\n }\n\n /** Sends queued events now (one batch at a time). Safe to call at any time. */\n async flush(): Promise<void> {\n if (this.destroyed || this.inflight || !this.events.length || !this.opts.enabled()) return;\n if (this.timer) {\n clearTimeout(this.timer);\n this.timer = null;\n }\n this.inflight = true;\n const batch = this.events.slice(0, this.batchSize);\n this.inflightIds = new Set(batch.map((e) => e.eventId));\n let ok = false;\n let retry = true;\n try {\n const res = await this.opts.send(batch);\n ok = res.ok;\n retry = res.retry ?? !res.ok;\n } catch (err) {\n this.opts.onError?.(err);\n }\n this.inflight = false;\n this.inflightIds.clear();\n if (this.destroyed) return;\n if (ok || !retry) {\n const ids = new Set(batch.map((e) => e.eventId));\n this.events = this.events.filter((e) => !ids.has(e.eventId));\n this.attempt = 0;\n this.persist();\n if (this.events.length) this.schedule();\n return;\n }\n this.attempt += 1;\n if (this.attempt >= this.maxAttempts) {\n // Give up for now; events stay queued until the next flush trigger (navigation, timer, unload).\n this.attempt = 0;\n return;\n }\n this.retryTimer = setTimeout(() => {\n this.retryTimer = null;\n void this.flush();\n }, backoffMs(this.attempt - 1));\n }\n\n /** Best-effort synchronous flush on unload using `sendBeacon`. */\n flushSync(): void {\n if (this.destroyed || !this.opts.enabled() || !this.opts.beacon) return;\n // Events already in flight (fetch with keepalive) survive unload; only beacon the rest to avoid duplicate batches.\n const batch = this.events.filter((e) => !this.inflightIds.has(e.eventId)).slice(0, this.batchSize);\n if (!batch.length) return;\n if (this.opts.beacon(batch)) {\n const ids = new Set(batch.map((e) => e.eventId));\n this.events = this.events.filter((e) => !ids.has(e.eventId));\n this.persist();\n }\n }\n\n clear(): void {\n this.events = [];\n this.persist();\n }\n\n destroy(): void {\n this.destroyed = true;\n if (this.timer) clearTimeout(this.timer);\n if (this.retryTimer) clearTimeout(this.retryTimer);\n this.timer = null;\n this.retryTimer = null;\n this.persist();\n }\n}\n","type AnyFn = (...args: any[]) => void;\n\n/** Tiny typed event emitter. Listener errors are reported, never thrown into the caller. */\nexport class Emitter<Events extends { [K in keyof Events]: AnyFn }> {\n private listeners = new Map<keyof Events, Set<AnyFn>>();\n constructor(private readonly onError?: (err: unknown) => void) {}\n\n on<K extends keyof Events>(event: K, cb: Events[K]): () => void {\n let set = this.listeners.get(event);\n if (!set) {\n set = new Set();\n this.listeners.set(event, set);\n }\n set.add(cb);\n return () => this.off(event, cb);\n }\n\n off<K extends keyof Events>(event: K, cb: Events[K]): void {\n this.listeners.get(event)?.delete(cb);\n }\n\n emit<K extends keyof Events>(event: K, ...args: Parameters<Events[K]>): void {\n const set = this.listeners.get(event);\n if (!set) return;\n for (const cb of Array.from(set)) {\n try {\n cb(...args);\n } catch (err) {\n this.onError?.(err);\n }\n }\n }\n\n clear(): void {\n this.listeners.clear();\n }\n}\n","import type { FrequencyConfig, PublishedGuide } from \"@guidelytech/shared\";\n\n/** Per-guide state kept locally and merged with the server's `IdentifyResponse.guideState`. */\nexport interface GuideLocalState {\n impressions: number;\n completedAt?: string | null;\n dismissedAt?: string | null;\n lastShownAt?: string | null;\n /** Last step reached in a multi-step flow (local only), so the messenger can offer to resume it. */\n lastStepId?: string | null;\n}\n\nconst HOUR = 3600 * 1000;\n\n/**\n * Pure frequency evaluation. `shownThisSession` comes from session storage.\n * - `once`: a single impression ever (unless `reshowAfterDismiss` and it was only dismissed).\n * - `once_per_session`: at most once per browser session.\n * - `every_time`: on every trigger (still blocked by completion/dismissal rules below).\n * - `interval`: at most once every `intervalHours`.\n * `maxImpressions` caps total impressions; dismissal blocks re-showing unless `reshowAfterDismiss`\n * (then `cooldownHours` must have elapsed); completion blocks unless mode is `every_time`.\n */\nexport function isFrequencyEligible(freq: FrequencyConfig, state: GuideLocalState | undefined, shownThisSession: boolean, now: number = Date.now()): boolean {\n const s = state ?? { impressions: 0 };\n if (freq.maxImpressions != null && s.impressions >= freq.maxImpressions) return false;\n if (s.completedAt && freq.mode !== \"every_time\") return false;\n if (s.dismissedAt) {\n if (!freq.reshowAfterDismiss) return false;\n if (freq.cooldownHours > 0 && now - Date.parse(s.dismissedAt) < freq.cooldownHours * HOUR) return false;\n }\n switch (freq.mode) {\n case \"once\":\n return s.impressions === 0 || (!!s.dismissedAt && freq.reshowAfterDismiss);\n case \"once_per_session\":\n return !shownThisSession;\n case \"interval\": {\n if (!s.lastShownAt) return true;\n const hours = freq.intervalHours ?? 24;\n return now - Date.parse(s.lastShownAt) >= hours * HOUR;\n }\n case \"every_time\":\n default:\n return true;\n }\n}\n\n/**\n * Exclusive guides (flows, modals, tooltips, announcements, surveys) run one at a time.\n * Banners, checklists and hotspot-only tooltips can coexist with an active flow.\n */\nexport function isExclusiveGuide(guide: PublishedGuide): boolean {\n if (guide.kind === \"banner\" || guide.kind === \"checklist\") return false;\n const steps = guide.config.steps;\n if (steps.length > 0 && steps.every((s) => s.type === \"hotspot\" || s.type === \"banner\")) return false;\n return true;\n}\n\n/** Is `a` mutually exclusive with `b` (by id or key, in either direction)? */\nexport function areMutuallyExclusive(a: PublishedGuide, b: PublishedGuide): boolean {\n const la = a.config.frequency.mutuallyExclusiveWith;\n const lb = b.config.frequency.mutuallyExclusiveWith;\n return la.includes(b.id) || la.includes(b.key) || lb.includes(a.id) || lb.includes(a.key);\n}\n\n/** Survey scheduling window. */\nexport function isWithinSchedule(guide: PublishedGuide, now: number = Date.now()): boolean {\n const s = guide.config.survey;\n if (!s) return true;\n if (s.startAt && now < Date.parse(s.startAt)) return false;\n if (s.endAt && now > Date.parse(s.endAt)) return false;\n return true;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/headless.ts","../src/http.ts","../src/storage.ts","../src/queue.ts","../src/emitter.ts","../src/frequency.ts"],"sourcesContent":["/**\n * `@guidelytech/sdk-core/headless` — the DOM-free building blocks of the SDK (HTTP client, event\n * queue, frequency rules, emitter, key/value storage helpers). Used by the React Native SDK, which\n * renders guides natively instead of through `@guidelytech/guide-runtime`.\n */\nexport { Api, postWithRetry, backoffMs, sleep } from \"./http\";\nexport type { ApiResponse, FetchLike } from \"./http\";\nexport { EventQueue } from \"./queue\";\nexport type { EventQueueOptions } from \"./queue\";\nexport { Emitter } from \"./emitter\";\nexport { isFrequencyEligible, isExclusiveGuide, areMutuallyExclusive, isWithinSchedule, dismissedAfterMerge } from \"./frequency\";\nexport type { GuideLocalState } from \"./frequency\";\nexport { memoryStore, readJson, writeJson, KEYS } from \"./storage\";\nexport type { KeyValueStore } from \"./storage\";\nexport type { ChecklistProgressState, PersistedState, SessionState, ConsentState, GuidelyAccount, GuidelyUser, GuidelyEnvironment } from \"./types\";\n","export interface ApiResponse<T> {\n status: number;\n ok: boolean;\n data: T | null;\n etag: string | null;\n}\n\nexport type FetchLike = typeof fetch;\n\n/** Thin HTTP client for the public SDK API. Adds the public key as query + header. */\nexport class Api {\n constructor(\n private readonly baseUrl: string,\n private readonly key: string,\n private readonly fetchImpl?: FetchLike,\n ) {}\n\n url(path: string, params: Record<string, string | undefined> = {}): string {\n // Built by hand rather than with URL/searchParams, which React Native does not implement.\n const query = Object.entries({ key: this.key, ...params })\n .filter((e): e is [string, string] => e[1] != null)\n .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)\n .join(\"&\");\n return `${this.baseUrl.replace(/\\/$/, \"\")}/api/v1/sdk${path}?${query}`;\n }\n\n private get fetcher(): FetchLike | null {\n if (this.fetchImpl) return this.fetchImpl;\n return typeof fetch === \"function\" ? fetch.bind(globalThis) : null;\n }\n\n async request<T>(method: \"GET\" | \"POST\", path: string, options: { body?: unknown; etag?: string | null; params?: Record<string, string | undefined>; keepalive?: boolean; timeoutMs?: number; headers?: Record<string, string> } = {}): Promise<ApiResponse<T>> {\n const f = this.fetcher;\n if (!f) throw new Error(\"fetch is not available\");\n const headers: Record<string, string> = { ...options.headers, \"X-Guidely-Key\": this.key, Accept: \"application/json\" };\n // Multipart bodies set their own Content-Type (with the boundary).\n const form = typeof FormData !== \"undefined\" && options.body instanceof FormData;\n if (options.body !== undefined && !form) headers[\"Content-Type\"] = \"application/json\";\n if (options.etag) headers[\"If-None-Match\"] = options.etag;\n const ac = typeof AbortController !== \"undefined\" ? new AbortController() : null;\n const timer = ac ? setTimeout(() => ac.abort(), options.timeoutMs ?? 10000) : null;\n try {\n const res = await f(this.url(path, options.params), {\n method,\n headers,\n body: options.body === undefined ? undefined : form ? (options.body as FormData) : JSON.stringify(options.body),\n keepalive: options.keepalive,\n signal: ac?.signal,\n credentials: \"omit\",\n mode: \"cors\",\n });\n let data: T | null = null;\n if (res.status !== 304 && res.status !== 204) {\n try {\n data = (await res.json()) as T;\n } catch {\n data = null;\n }\n }\n return { status: res.status, ok: res.ok, data, etag: res.headers?.get?.(\"etag\") ?? null };\n } finally {\n if (timer) clearTimeout(timer);\n }\n }\n\n get<T>(path: string, options?: { etag?: string | null; params?: Record<string, string | undefined>; headers?: Record<string, string> }): Promise<ApiResponse<T>> {\n return this.request<T>(\"GET\", path, options);\n }\n\n post<T>(path: string, body: unknown, options?: { params?: Record<string, string | undefined>; keepalive?: boolean; headers?: Record<string, string> }): Promise<ApiResponse<T>> {\n return this.request<T>(\"POST\", path, { body, ...options });\n }\n\n /** Multipart POST (file uploads). No Content-Type header: the browser adds it with the boundary. */\n postForm<T>(path: string, form: FormData, options?: { params?: Record<string, string | undefined>; timeoutMs?: number }): Promise<ApiResponse<T>> {\n return this.request<T>(\"POST\", path, { body: form, ...options });\n }\n\n /**\n * POST that may answer with server-sent events. Each event is passed to `onEvent`; a JSON answer is\n * returned instead. No timeout: Scout replies can take a while and the server keeps the stream alive.\n */\n async stream<T>(path: string, body: unknown, onEvent: (event: string, data: unknown) => void, headers: Record<string, string> = {}): Promise<ApiResponse<T>> {\n const f = this.fetcher;\n if (!f) throw new Error(\"fetch is not available\");\n const res = await f(this.url(path), {\n method: \"POST\",\n headers: { ...headers, \"X-Guidely-Key\": this.key, Accept: \"text/event-stream, application/json\", \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n credentials: \"omit\",\n mode: \"cors\",\n });\n const type = res.headers?.get?.(\"content-type\") ?? \"\";\n if (!type.includes(\"text/event-stream\") || !res.body) {\n let data: T | null = null;\n try {\n data = (await res.json()) as T;\n } catch {\n data = null;\n }\n return { status: res.status, ok: res.ok, data, etag: null };\n }\n await readEventStream(res.body, onEvent);\n return { status: res.status, ok: res.ok, data: null, etag: null };\n }\n\n /** `navigator.sendBeacon` when available (page unload); returns false when it could not be queued. */\n beacon(path: string, body: unknown): boolean {\n try {\n if (typeof navigator === \"undefined\" || typeof navigator.sendBeacon !== \"function\") return false;\n // text/plain is a CORS-safelisted type: cross-origin beacons need no preflight (which\n // browsers drop during unload). The API parses the body regardless of content type.\n const blob = new Blob([JSON.stringify(body)], { type: \"text/plain;charset=UTF-8\" });\n return navigator.sendBeacon(this.url(path), blob);\n } catch {\n return false;\n }\n }\n}\n\nexport const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));\n\n/** Exponential backoff helper: 1s → 2s → 4s … capped. */\nexport function backoffMs(attempt: number, baseMs = 1000, maxMs = 30000): number {\n return Math.min(maxMs, baseMs * 2 ** Math.max(0, attempt));\n}\n\n/**\n * Sends with bounded retries: network errors, 429 and 5xx are retried with backoff; success and\n * other 4xx answers are returned as they are. Null when every attempt failed.\n */\nexport async function withRetry<T>(send: () => Promise<ApiResponse<T>>, attempts = 3, baseMs = 500): Promise<ApiResponse<T> | null> {\n for (let i = 0; i < attempts; i++) {\n try {\n const res = await send();\n if (res.ok || (res.status >= 400 && res.status < 500 && res.status !== 429)) return res;\n } catch {\n /* network error → retry */\n }\n if (i < attempts - 1) await sleep(backoffMs(i, baseMs, 8000));\n }\n return null;\n}\n\n/** Fire-and-forget POST with bounded retries (used for state sync; never blocks UI). */\nexport function postWithRetry<T>(api: Api, path: string, body: unknown, attempts = 3, baseMs = 500): Promise<ApiResponse<T> | null> {\n return withRetry(() => api.post<T>(path, body), attempts, baseMs);\n}\n\n/** Parses a `text/event-stream` body (`event:` + single-line JSON `data:`; comment lines ignored). */\nexport async function readEventStream(body: ReadableStream<Uint8Array>, onEvent: (event: string, data: unknown) => void): Promise<void> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n const dispatch = (raw: string) => {\n let event = \"message\";\n const data: string[] = [];\n for (const line of raw.split(\"\\n\")) {\n if (!line || line.startsWith(\":\")) continue;\n const i = line.indexOf(\":\");\n const field = i < 0 ? line : line.slice(0, i);\n const value = i < 0 ? \"\" : line.slice(i + 1).replace(/^ /, \"\");\n if (field === \"event\") event = value;\n else if (field === \"data\") data.push(value);\n }\n if (!data.length) return;\n try {\n onEvent(event, JSON.parse(data.join(\"\\n\")));\n } catch {\n /* ignore malformed events */\n }\n };\n for (;;) {\n const { done, value } = await reader.read();\n if (value) buffer += decoder.decode(value, { stream: true }).replace(/\\r\\n?/g, \"\\n\");\n let sep: number;\n while ((sep = buffer.indexOf(\"\\n\\n\")) >= 0) {\n dispatch(buffer.slice(0, sep));\n buffer = buffer.slice(sep + 2);\n }\n if (done) break;\n }\n if (buffer.trim()) dispatch(buffer);\n}\n","export type StorageKind = \"local\" | \"session\" | \"memory\";\n\n/** Minimal synchronous key/value store; never throws (private mode, quota, SSR). */\nexport interface KeyValueStore {\n get(key: string): string | null;\n set(key: string, value: string): void;\n remove(key: string): void;\n}\n\nexport function memoryStore(): KeyValueStore {\n const map = new Map<string, string>();\n return { get: (k) => map.get(k) ?? null, set: (k, v) => void map.set(k, v), remove: (k) => void map.delete(k) };\n}\n\nfunction webStore(area: \"localStorage\" | \"sessionStorage\"): KeyValueStore | null {\n try {\n if (typeof window === \"undefined\") return null;\n const s = window[area];\n if (!s) return null;\n const probe = \"__guidely_probe__\";\n s.setItem(probe, \"1\");\n s.removeItem(probe);\n return {\n get: (k) => {\n try {\n return s.getItem(k);\n } catch {\n return null;\n }\n },\n set: (k, v) => {\n try {\n s.setItem(k, v);\n } catch {\n /* quota / private mode */\n }\n },\n remove: (k) => {\n try {\n s.removeItem(k);\n } catch {\n /* ignore */\n }\n },\n };\n } catch {\n return null;\n }\n}\n\n/** Creates a store of the requested kind, degrading to memory when unavailable. */\nexport function createStorage(kind: StorageKind = \"local\"): KeyValueStore {\n if (kind === \"memory\") return memoryStore();\n return webStore(kind === \"local\" ? \"localStorage\" : \"sessionStorage\") ?? memoryStore();\n}\n\nexport function readJson<T>(store: KeyValueStore, key: string, fallback: T): T {\n const raw = store.get(key);\n if (raw == null) return fallback;\n try {\n return JSON.parse(raw) as T;\n } catch {\n return fallback;\n }\n}\n\nexport function writeJson(store: KeyValueStore, key: string, value: unknown): void {\n try {\n store.set(key, JSON.stringify(value));\n } catch {\n /* ignore */\n }\n}\n\n/** Storage keys used by the SDK (documented for integrators). */\nexport const KEYS = {\n anon: \"guidely:anon\",\n seen: \"guidely:seen\",\n config: (key: string) => `guidely:config:${key}`,\n etag: (key: string) => `guidely:etag:${key}`,\n state: (key: string) => `guidely:state:${key}`,\n user: (key: string) => `guidely:user:${key}`,\n queue: (key: string) => `guidely:queue:${key}`,\n session: \"guidely:session\",\n active: \"guidely:active\",\n authoring: \"guidely:authoring\",\n /** How-to capture session marker ({ token, … }) and its not-yet-uploaded steps. */\n capture: \"guidely:capture\",\n capturePending: \"guidely:capture:pending\",\n /** Messenger conversations this browser can read: [{ id, token }]. */\n conversations: (key: string) => `guidely:conversations:${key}`,\n /** Announcement ids opened in the messenger's News tab. */\n newsSeen: (key: string) => `guidely:news:${key}`,\n} as const;\n","import type { SdkEvent } from \"@guidelytech/shared\";\nimport { backoffMs } from \"./http\";\nimport { readJson, writeJson, type KeyValueStore } from \"./storage\";\n\nexport interface EventQueueOptions {\n store: KeyValueStore;\n storageKey: string;\n /** Sends one batch. Resolve `{ ok: true }` on success, `{ ok: false, retry }` otherwise. */\n send: (events: SdkEvent[]) => Promise<{ ok: boolean; retry?: boolean }>;\n /** Synchronous best-effort send used on page hide. */\n beacon?: (events: SdkEvent[]) => boolean;\n /** Should events be flushed right now? (analytics enabled + consent granted). */\n enabled: () => boolean;\n flushIntervalMs?: number;\n maxQueue?: number;\n batchSize?: number;\n maxAttempts?: number;\n onError?: (err: unknown) => void;\n}\n\n/**\n * Bounded, persisted event queue. Batches ≤50 events every 2s (or on demand), dedupes by\n * `eventId`, retries with exponential backoff (1s→30s, 5 attempts) and keeps failed events for\n * the next flush. Drops the oldest events once the queue exceeds 200.\n */\nexport class EventQueue {\n private events: SdkEvent[] = [];\n private timer: ReturnType<typeof setTimeout> | null = null;\n private retryTimer: ReturnType<typeof setTimeout> | null = null;\n private inflight = false;\n private inflightIds = new Set<string>();\n private attempt = 0;\n private destroyed = false;\n private readonly flushIntervalMs: number;\n private readonly maxQueue: number;\n private readonly batchSize: number;\n private readonly maxAttempts: number;\n\n constructor(private readonly opts: EventQueueOptions) {\n this.flushIntervalMs = opts.flushIntervalMs ?? 2000;\n this.maxQueue = opts.maxQueue ?? 200;\n this.batchSize = opts.batchSize ?? 50;\n this.maxAttempts = opts.maxAttempts ?? 5;\n const persisted = readJson<SdkEvent[]>(opts.store, opts.storageKey, []);\n if (Array.isArray(persisted)) this.events = persisted.filter((e) => e && typeof e.eventId === \"string\").slice(-this.maxQueue);\n // Events left over from a previous page load are flushed on the next tick.\n if (this.events.length) this.schedule();\n }\n\n get size(): number {\n return this.events.length;\n }\n\n /** Pending events (read-only copy, for debugging). */\n peek(): SdkEvent[] {\n return this.events.slice();\n }\n\n enqueue(event: SdkEvent): void {\n if (this.destroyed) return;\n if (this.events.some((e) => e.eventId === event.eventId)) return;\n this.events.push(event);\n if (this.events.length > this.maxQueue) this.events.splice(0, this.events.length - this.maxQueue);\n this.persist();\n this.schedule();\n }\n\n private persist(): void {\n writeJson(this.opts.store, this.opts.storageKey, this.events);\n }\n\n private schedule(): void {\n if (this.timer || this.retryTimer || this.destroyed) return;\n this.timer = setTimeout(() => {\n this.timer = null;\n void this.flush();\n }, this.flushIntervalMs);\n }\n\n /** Sends queued events now (one batch at a time). Safe to call at any time. */\n async flush(): Promise<void> {\n if (this.destroyed || this.inflight || !this.events.length || !this.opts.enabled()) return;\n if (this.timer) {\n clearTimeout(this.timer);\n this.timer = null;\n }\n this.inflight = true;\n const batch = this.events.slice(0, this.batchSize);\n this.inflightIds = new Set(batch.map((e) => e.eventId));\n let ok = false;\n let retry = true;\n try {\n const res = await this.opts.send(batch);\n ok = res.ok;\n retry = res.retry ?? !res.ok;\n } catch (err) {\n this.opts.onError?.(err);\n }\n this.inflight = false;\n this.inflightIds.clear();\n if (this.destroyed) return;\n if (ok || !retry) {\n const ids = new Set(batch.map((e) => e.eventId));\n this.events = this.events.filter((e) => !ids.has(e.eventId));\n this.attempt = 0;\n this.persist();\n if (this.events.length) this.schedule();\n return;\n }\n this.attempt += 1;\n if (this.attempt >= this.maxAttempts) {\n // Give up for now; events stay queued until the next flush trigger (navigation, timer, unload).\n this.attempt = 0;\n return;\n }\n this.retryTimer = setTimeout(() => {\n this.retryTimer = null;\n void this.flush();\n }, backoffMs(this.attempt - 1));\n }\n\n /** Best-effort synchronous flush on unload using `sendBeacon`. */\n flushSync(): void {\n if (this.destroyed || !this.opts.enabled() || !this.opts.beacon) return;\n // Events already in flight (fetch with keepalive) survive unload; only beacon the rest to avoid duplicate batches.\n const batch = this.events.filter((e) => !this.inflightIds.has(e.eventId)).slice(0, this.batchSize);\n if (!batch.length) return;\n if (this.opts.beacon(batch)) {\n const ids = new Set(batch.map((e) => e.eventId));\n this.events = this.events.filter((e) => !ids.has(e.eventId));\n this.persist();\n }\n }\n\n clear(): void {\n this.events = [];\n this.persist();\n }\n\n destroy(): void {\n this.destroyed = true;\n if (this.timer) clearTimeout(this.timer);\n if (this.retryTimer) clearTimeout(this.retryTimer);\n this.timer = null;\n this.retryTimer = null;\n this.persist();\n }\n}\n","type AnyFn = (...args: any[]) => void;\n\n/** Tiny typed event emitter. Listener errors are reported, never thrown into the caller. */\nexport class Emitter<Events extends { [K in keyof Events]: AnyFn }> {\n private listeners = new Map<keyof Events, Set<AnyFn>>();\n constructor(private readonly onError?: (err: unknown) => void) {}\n\n on<K extends keyof Events>(event: K, cb: Events[K]): () => void {\n let set = this.listeners.get(event);\n if (!set) {\n set = new Set();\n this.listeners.set(event, set);\n }\n set.add(cb);\n return () => this.off(event, cb);\n }\n\n off<K extends keyof Events>(event: K, cb: Events[K]): void {\n this.listeners.get(event)?.delete(cb);\n }\n\n emit<K extends keyof Events>(event: K, ...args: Parameters<Events[K]>): void {\n const set = this.listeners.get(event);\n if (!set) return;\n for (const cb of Array.from(set)) {\n try {\n cb(...args);\n } catch (err) {\n this.onError?.(err);\n }\n }\n }\n\n clear(): void {\n this.listeners.clear();\n }\n}\n","import type { FrequencyConfig, PublishedGuide } from \"@guidelytech/shared\";\n\n/** Per-guide state kept locally and merged with the server's `IdentifyResponse.guideState`. */\nexport interface GuideLocalState {\n impressions: number;\n completedAt?: string | null;\n dismissedAt?: string | null;\n lastShownAt?: string | null;\n /** Last step reached in a multi-step flow (local only), so the messenger can offer to resume it. */\n lastStepId?: string | null;\n}\n\nconst HOUR = 3600 * 1000;\n\n/**\n * Pure frequency evaluation. `shownThisSession` comes from session storage.\n * - `once`: a single impression ever (unless `reshowAfterDismiss` and it was only dismissed).\n * - `once_per_session`: at most once per browser session.\n * - `every_time`: on every trigger (still blocked by completion/dismissal rules below).\n * - `interval`: at most once every `intervalHours`.\n * `maxImpressions` caps total impressions; dismissal blocks re-showing unless `reshowAfterDismiss`\n * (then `cooldownHours` must have elapsed); completion blocks unless mode is `every_time`.\n */\nexport function isFrequencyEligible(freq: FrequencyConfig, state: GuideLocalState | undefined, shownThisSession: boolean, now: number = Date.now()): boolean {\n const s = state ?? { impressions: 0 };\n if (freq.maxImpressions != null && s.impressions >= freq.maxImpressions) return false;\n if (s.completedAt && freq.mode !== \"every_time\") return false;\n if (s.dismissedAt) {\n if (!freq.reshowAfterDismiss) return false;\n if (freq.cooldownHours > 0 && now - Date.parse(s.dismissedAt) < freq.cooldownHours * HOUR) return false;\n }\n switch (freq.mode) {\n case \"once\":\n return s.impressions === 0 || (!!s.dismissedAt && freq.reshowAfterDismiss);\n case \"once_per_session\":\n return !shownThisSession;\n case \"interval\": {\n if (!s.lastShownAt) return true;\n const hours = freq.intervalHours ?? 24;\n return now - Date.parse(s.lastShownAt) >= hours * HOUR;\n }\n case \"every_time\":\n default:\n return true;\n }\n}\n\n/**\n * Exclusive guides (flows, modals, tooltips, announcements, surveys) run one at a time.\n * Banners, checklists and hotspot-only tooltips can coexist with an active flow.\n */\nexport function isExclusiveGuide(guide: PublishedGuide): boolean {\n if (guide.kind === \"banner\" || guide.kind === \"checklist\") return false;\n const steps = guide.config.steps;\n if (steps.length > 0 && steps.every((s) => s.type === \"hotspot\" || s.type === \"banner\")) return false;\n return true;\n}\n\n/** Is `a` mutually exclusive with `b` (by id or key, in either direction)? */\nexport function areMutuallyExclusive(a: PublishedGuide, b: PublishedGuide): boolean {\n const la = a.config.frequency.mutuallyExclusiveWith;\n const lb = b.config.frequency.mutuallyExclusiveWith;\n return la.includes(b.id) || la.includes(b.key) || lb.includes(a.id) || lb.includes(a.key);\n}\n\n/** Survey scheduling window. */\nexport function isWithinSchedule(guide: PublishedGuide, now: number = Date.now()): boolean {\n const s = guide.config.survey;\n if (!s) return true;\n if (s.startAt && now < Date.parse(s.startAt)) return false;\n if (s.endAt && now > Date.parse(s.endAt)) return false;\n return true;\n}\n\n/**\n * A checklist's dismissal after merging server state. A local dismissal normally wins (it may not have\n * synced yet), but when the server reports a reset this device hasn't applied, the server's value wins:\n * null, or a dismissal made on another device after the reset. Compared as tokens, not times, so a\n * skewed device clock can't keep a reset dismissal.\n */\nexport function dismissedAfterMerge(local: { dismissedAt?: string | null; dismissResetAt?: string | null } | undefined, server: { dismissedAt?: string | null; dismissResetAt?: string | null }): string | null {\n if (server.dismissResetAt && server.dismissResetAt !== local?.dismissResetAt) return server.dismissedAt ?? null;\n return local?.dismissedAt ?? server.dismissedAt ?? null;\n}\n"],"mappings":"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","names":[]}
@@ -1,5 +1,5 @@
1
1
  export { A as Api, a as ApiResponse, F as FetchLike, K as KEYS, b as KeyValueStore, c as backoffMs, m as memoryStore, p as postWithRetry, r as readJson, s as sleep, w as writeJson } from './http-47tiAlOM.cjs';
2
- export { C as ChecklistProgressState, a as ConsentState, E as EventQueue, b as EventQueueOptions, G as GuideLocalState, c as GuidelyAccount, d as GuidelyEnvironment, e as GuidelyUser, P as PersistedState, S as SessionState, f as areMutuallyExclusive, i as isExclusiveGuide, g as isFrequencyEligible, h as isWithinSchedule } from './types-DbKQPvFt.cjs';
2
+ export { C as ChecklistProgressState, a as ConsentState, E as EventQueue, b as EventQueueOptions, G as GuideLocalState, c as GuidelyAccount, d as GuidelyEnvironment, e as GuidelyUser, P as PersistedState, S as SessionState, f as areMutuallyExclusive, g as dismissedAfterMerge, i as isExclusiveGuide, h as isFrequencyEligible, j as isWithinSchedule } from './types-zM5e3loY.cjs';
3
3
  import '@guidelytech/shared';
4
4
  import '@guidelytech/flags';
5
5
 
@@ -1,5 +1,5 @@
1
1
  export { A as Api, a as ApiResponse, F as FetchLike, K as KEYS, b as KeyValueStore, c as backoffMs, m as memoryStore, p as postWithRetry, r as readJson, s as sleep, w as writeJson } from './http-47tiAlOM.js';
2
- export { C as ChecklistProgressState, a as ConsentState, E as EventQueue, b as EventQueueOptions, G as GuideLocalState, c as GuidelyAccount, d as GuidelyEnvironment, e as GuidelyUser, P as PersistedState, S as SessionState, f as areMutuallyExclusive, i as isExclusiveGuide, g as isFrequencyEligible, h as isWithinSchedule } from './types-DqhNl5Am.js';
2
+ export { C as ChecklistProgressState, a as ConsentState, E as EventQueue, b as EventQueueOptions, G as GuideLocalState, c as GuidelyAccount, d as GuidelyEnvironment, e as GuidelyUser, P as PersistedState, S as SessionState, f as areMutuallyExclusive, g as dismissedAfterMerge, i as isExclusiveGuide, h as isFrequencyEligible, j as isWithinSchedule } from './types-DnJW_4ck.js';
3
3
  import '@guidelytech/shared';
4
4
  import '@guidelytech/flags';
5
5
 
package/dist/headless.js CHANGED
@@ -2,10 +2,11 @@ import {
2
2
  Emitter,
3
3
  EventQueue,
4
4
  areMutuallyExclusive,
5
+ dismissedAfterMerge,
5
6
  isExclusiveGuide,
6
7
  isFrequencyEligible,
7
8
  isWithinSchedule
8
- } from "./chunk-GDF6RCHQ.js";
9
+ } from "./chunk-544KSOC3.js";
9
10
  import {
10
11
  KEYS,
11
12
  memoryStore,
@@ -25,6 +26,7 @@ export {
25
26
  KEYS,
26
27
  areMutuallyExclusive,
27
28
  backoffMs,
29
+ dismissedAfterMerge,
28
30
  isExclusiveGuide,
29
31
  isFrequencyEligible,
30
32
  isWithinSchedule,
package/dist/index.cjs CHANGED
@@ -893,6 +893,10 @@ function isWithinSchedule(guide, now = Date.now()) {
893
893
  if (s.endAt && now > Date.parse(s.endAt)) return false;
894
894
  return true;
895
895
  }
896
+ function dismissedAfterMerge(local, server) {
897
+ if (server.dismissResetAt && server.dismissResetAt !== local?.dismissResetAt) return server.dismissedAt ?? null;
898
+ return local?.dismissedAt ?? server.dismissedAt ?? null;
899
+ }
896
900
 
897
901
  // src/http.ts
898
902
  var Api = class {
@@ -1061,11 +1065,15 @@ function forgetConversations(store, projectId) {
1061
1065
  store.remove(KEYS.conversations(projectId));
1062
1066
  }
1063
1067
  var HttpError = class extends Error {
1064
- constructor(status) {
1065
- super(`Request failed (${status})`);
1068
+ constructor(status, message) {
1069
+ super(message || `Request failed (${status})`);
1066
1070
  __publicField(this, "status", status);
1067
1071
  }
1068
1072
  };
1073
+ function serverMessage(data) {
1074
+ const m = data?.error?.message;
1075
+ return typeof m === "string" && m !== "Request validation failed" ? m : void 0;
1076
+ }
1069
1077
  function createMessengerTransport(opts) {
1070
1078
  const { api, store, projectId } = opts;
1071
1079
  const tokenFor = (id) => storedConversations(store, projectId).find((c) => c.id === id)?.token ?? "";
@@ -1147,6 +1155,16 @@ function createMessengerTransport(opts) {
1147
1155
  const res = await api.get(`/articles/${encodeURIComponent(id)}`);
1148
1156
  return res.ok && res.data ? res.data.article : null;
1149
1157
  },
1158
+ async fileComplaint(input) {
1159
+ const headers = input.conversationId ? { [TOKEN_HEADER]: tokenFor(input.conversationId) } : {};
1160
+ const res = await api.post("/complaints", { ...input, anonymousId: opts.anonymousId(), userId: opts.userId(), pageUrl: opts.pageUrl() }, { headers });
1161
+ if (!res.ok || !res.data || !("complaint" in res.data)) {
1162
+ if (res.status === 429) throw new HttpError(429, "You've filed several complaints just now. Please try again later.");
1163
+ throw new HttpError(res.status, serverMessage(res.data) ?? "Your complaint couldn't be sent. Please try again.");
1164
+ }
1165
+ if (res.data.token) remember(res.data.conversation.id, res.data.token);
1166
+ return res.data;
1167
+ },
1150
1168
  async pageContent(path) {
1151
1169
  const res = await api.post("/messenger/page", { anonymousId: opts.anonymousId(), userId: opts.userId(), path: path.split(/[?#]/)[0] || "/" });
1152
1170
  if (!res.ok || !res.data) throw new HttpError(res.status);
@@ -1457,7 +1475,7 @@ var loadRecorder = async () => (await import("./recorder.cjs")).recorder;
1457
1475
 
1458
1476
  // src/client.ts
1459
1477
  var SDK_NAME = "@guidelytech/sdk-core";
1460
- var SDK_VERSION = "0.7.0";
1478
+ var SDK_VERSION = "0.9.0";
1461
1479
  var DEFAULT_API_URL = "https://api.guidely.app";
1462
1480
  var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
1463
1481
  var DAY_MS = 24 * 60 * 60 * 1e3;
@@ -1865,7 +1883,8 @@ var GuidelyClient = class _GuidelyClient {
1865
1883
  checklists[id] = {
1866
1884
  completedTaskIds: Array.from(/* @__PURE__ */ new Set([...local?.completedTaskIds ?? [], ...p.completedTaskIds ?? []])),
1867
1885
  completedAt: local?.completedAt ?? p.completedAt ?? null,
1868
- dismissedAt: local?.dismissedAt ?? p.dismissedAt ?? null
1886
+ dismissedAt: dismissedAfterMerge(local, p),
1887
+ dismissResetAt: p.dismissResetAt ?? local?.dismissResetAt ?? null
1869
1888
  };
1870
1889
  }
1871
1890
  this.state = {
@@ -2504,6 +2523,12 @@ var GuidelyClient = class _GuidelyClient {
2504
2523
  ctx: () => this.getContext(),
2505
2524
  startAtStep: stepId,
2506
2525
  eventBus: this.eventBus,
2526
+ // Started from Help, Scout or startGuide(): steps on other pages offer to take the user there.
2527
+ promptForUrl: manual,
2528
+ // Also while a step waits for its page, so a link that loads that page resumes on it.
2529
+ onPosition: (id) => {
2530
+ if (exclusive) writeJson(this.sessionStore, KEYS.active, { guideId: guide.id, stepId: id, manual });
2531
+ },
2507
2532
  onError: (err) => this.reportError(err),
2508
2533
  onEvent: (e) => {
2509
2534
  const step = e.stepId ? guide.config.steps.find((s) => s.id === e.stepId) : void 0;
@@ -2727,7 +2752,7 @@ var GuidelyClient = class _GuidelyClient {
2727
2752
  const ctx = this.getContext();
2728
2753
  const url = location.href;
2729
2754
  const seen = readJson(this.store, KEYS.newsSeen(this.options.projectId), []);
2730
- return buildMessengerVisitor({
2755
+ const visitor = buildMessengerVisitor({
2731
2756
  guides: this.config?.guides ?? [],
2732
2757
  state: this.state,
2733
2758
  traits: this.user?.traits,
@@ -2740,6 +2765,7 @@ var GuidelyClient = class _GuidelyClient {
2740
2765
  },
2741
2766
  newsSeen: Array.isArray(seen) ? seen : []
2742
2767
  });
2768
+ return { ...visitor, identified: !!this.user?.id };
2743
2769
  }
2744
2770
  destroyMessenger() {
2745
2771
  this.messenger?.destroy();
@@ -2925,6 +2951,14 @@ var GuidelyClient = class _GuidelyClient {
2925
2951
  closeMessenger() {
2926
2952
  this.messenger?.close();
2927
2953
  }
2954
+ /**
2955
+ * Opens the complaint form in the messenger, optionally with a category picked (its id from
2956
+ * Complaints → Categories). Does nothing when the project doesn't take complaints.
2957
+ */
2958
+ openComplaint(categoryId) {
2959
+ if (!this.config?.messenger?.enabled || !this.config.messenger.complaints?.categories.length) return;
2960
+ void this.loadMessenger().then((m) => m?.openComplaint(categoryId));
2961
+ }
2928
2962
  /** Opens a new messenger conversation, optionally pre-filling the message. */
2929
2963
  showNewMessage(text) {
2930
2964
  if (!this.config?.messenger?.enabled) return;