@holdyapp/sdk 0.2.1 → 1.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 (75) hide show
  1. package/README.md +20 -147
  2. package/dist/auto-capture.d.ts +12 -16
  3. package/dist/auto-capture.d.ts.map +1 -0
  4. package/dist/browser.d.ts +23 -0
  5. package/dist/browser.d.ts.map +1 -0
  6. package/dist/core.d.ts +42 -0
  7. package/dist/core.d.ts.map +1 -0
  8. package/dist/holdy.js +2 -0
  9. package/dist/holdy.js.map +7 -0
  10. package/dist/index.cjs +430 -0
  11. package/dist/index.cjs.map +7 -0
  12. package/dist/index.d.ts +5 -46
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.mjs +399 -0
  15. package/dist/index.mjs.map +7 -0
  16. package/dist/server.cjs +177 -0
  17. package/dist/server.cjs.map +7 -0
  18. package/dist/server.d.ts +15 -0
  19. package/dist/server.d.ts.map +1 -0
  20. package/dist/server.mjs +148 -0
  21. package/dist/server.mjs.map +7 -0
  22. package/dist/types.d.ts +41 -109
  23. package/dist/types.d.ts.map +1 -0
  24. package/dist/web-context.d.ts +51 -0
  25. package/dist/web-context.d.ts.map +1 -0
  26. package/dist/web.d.ts +41 -0
  27. package/dist/web.d.ts.map +1 -0
  28. package/dist/web.mjs +389 -0
  29. package/dist/web.mjs.map +7 -0
  30. package/package.json +24 -20
  31. package/src/auto-capture.ts +51 -78
  32. package/src/browser.ts +55 -0
  33. package/src/core.test.ts +147 -0
  34. package/src/core.ts +172 -0
  35. package/src/index.ts +4 -427
  36. package/src/server.ts +19 -0
  37. package/src/types.ts +38 -162
  38. package/src/web-context.test.ts +71 -0
  39. package/src/web-context.ts +227 -0
  40. package/src/web.ts +108 -0
  41. package/LICENSE +0 -21
  42. package/dist/anchor.d.ts +0 -8
  43. package/dist/holdy.cjs.js +0 -255
  44. package/dist/holdy.cjs.js.map +0 -7
  45. package/dist/holdy.esm.js +0 -255
  46. package/dist/holdy.esm.js.map +0 -7
  47. package/dist/holdy.iife.js +0 -255
  48. package/dist/holdy.iife.js.map +0 -7
  49. package/dist/outcome.d.ts +0 -19
  50. package/dist/queue.d.ts +0 -54
  51. package/dist/render/kinds/banner.d.ts +0 -3
  52. package/dist/render/kinds/checklist.d.ts +0 -19
  53. package/dist/render/kinds/cta.d.ts +0 -8
  54. package/dist/render/kinds/modal.d.ts +0 -16
  55. package/dist/render/kinds/shared.d.ts +0 -16
  56. package/dist/render/kinds/spotlight.d.ts +0 -17
  57. package/dist/render/kinds/tooltip.d.ts +0 -3
  58. package/dist/render/root.d.ts +0 -3
  59. package/dist/render/styles.d.ts +0 -5
  60. package/dist/store.d.ts +0 -36
  61. package/dist/transport.d.ts +0 -59
  62. package/src/anchor.ts +0 -72
  63. package/src/outcome.ts +0 -51
  64. package/src/queue.ts +0 -296
  65. package/src/render/kinds/banner.ts +0 -52
  66. package/src/render/kinds/checklist.ts +0 -175
  67. package/src/render/kinds/cta.ts +0 -53
  68. package/src/render/kinds/modal.ts +0 -129
  69. package/src/render/kinds/shared.ts +0 -67
  70. package/src/render/kinds/spotlight.ts +0 -176
  71. package/src/render/kinds/tooltip.ts +0 -82
  72. package/src/render/root.ts +0 -69
  73. package/src/render/styles.ts +0 -280
  74. package/src/store.ts +0 -106
  75. package/src/transport.ts +0 -182
package/dist/index.mjs ADDED
@@ -0,0 +1,399 @@
1
+ // src/core.ts
2
+ var DEFAULT_BASE_URL = "https://holdy.chat/api/v1";
3
+ var DEFAULT_FLUSH_AT = 20;
4
+ var DEFAULT_FLUSH_INTERVAL_MS = 5e3;
5
+ var MAX_QUEUE = 1e3;
6
+ var BULK_LIMIT = 500;
7
+ var HoldyCore = class {
8
+ apiKey;
9
+ baseUrl;
10
+ flushAt;
11
+ debug;
12
+ source;
13
+ queue = [];
14
+ timer = null;
15
+ flushing = null;
16
+ userId = null;
17
+ anonymousId = null;
18
+ accountKey = null;
19
+ constructor(source, opts) {
20
+ this.source = source;
21
+ this.apiKey = opts.apiKey;
22
+ this.accountKey = opts.accountKey ?? null;
23
+ this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
24
+ this.flushAt = Math.max(1, opts.flushAt ?? DEFAULT_FLUSH_AT);
25
+ this.debug = opts.debug ?? false;
26
+ const intervalMs = opts.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
27
+ if (intervalMs > 0) {
28
+ this.timer = setInterval(() => void this.flush(), intervalMs);
29
+ if (typeof this.timer.unref === "function") {
30
+ ;
31
+ this.timer.unref();
32
+ }
33
+ }
34
+ }
35
+ track(name, properties, overrides) {
36
+ if (!name || typeof name !== "string") return;
37
+ const userId = overrides?.userId ?? this.userId ?? void 0;
38
+ const accountKey = overrides?.accountKey ?? this.accountKey ?? void 0;
39
+ const occurredAt = overrides?.occurredAt ? new Date(overrides.occurredAt).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
40
+ const context = {
41
+ ...this.environmentContext(),
42
+ ...overrides?.context ?? {}
43
+ };
44
+ if (accountKey) context.account_external_id = accountKey;
45
+ const event = {
46
+ event: name,
47
+ occurred_at: occurredAt,
48
+ source: this.source,
49
+ ...userId ? { user_id: userId } : {},
50
+ ...this.anonymousId ? { anonymous_id: this.anonymousId } : {},
51
+ properties: properties ?? {},
52
+ ...Object.keys(context).length > 0 ? { context } : {}
53
+ };
54
+ this.queue.push(event);
55
+ if (this.queue.length > MAX_QUEUE) this.queue.splice(0, this.queue.length - MAX_QUEUE);
56
+ if (this.queue.length >= this.flushAt) void this.flush();
57
+ }
58
+ identify(userId, traits) {
59
+ if (!userId || typeof userId !== "string") return;
60
+ this.userId = userId;
61
+ this.track("$identify", traits ?? {});
62
+ }
63
+ group(accountKey, traits) {
64
+ if (!accountKey || typeof accountKey !== "string") return;
65
+ this.accountKey = accountKey;
66
+ this.track("$group", traits ?? {});
67
+ }
68
+ /** Drena a fila. Nunca lança — falha re-enfileira e loga em debug. */
69
+ async flush() {
70
+ if (this.flushing) return this.flushing;
71
+ if (this.queue.length === 0) return;
72
+ this.flushing = this.drain().finally(() => {
73
+ this.flushing = null;
74
+ });
75
+ return this.flushing;
76
+ }
77
+ /** Flush final + para o timer. Chamar em shutdown do server / unload do web. */
78
+ async shutdown() {
79
+ if (this.timer) {
80
+ clearInterval(this.timer);
81
+ this.timer = null;
82
+ }
83
+ await this.flush();
84
+ }
85
+ async drain() {
86
+ while (this.queue.length > 0) {
87
+ const batch = this.queue.splice(0, BULK_LIMIT);
88
+ const ok = await this.send(batch);
89
+ if (!ok) {
90
+ this.queue.unshift(...batch);
91
+ return;
92
+ }
93
+ }
94
+ }
95
+ async send(events) {
96
+ try {
97
+ const res = await fetch(`${this.baseUrl}/events/bulk`, {
98
+ method: "POST",
99
+ headers: {
100
+ Authorization: `Bearer ${this.apiKey}`,
101
+ "Content-Type": "application/json"
102
+ },
103
+ body: JSON.stringify({ events }),
104
+ ...this.source === "sdk_web" ? { keepalive: true } : {}
105
+ });
106
+ if (!res.ok) {
107
+ if (res.status >= 400 && res.status < 500) {
108
+ this.log(`bulk rejected ${res.status} \xB7 batch dropped`);
109
+ return true;
110
+ }
111
+ this.log(`bulk failed ${res.status} \xB7 will retry`);
112
+ return false;
113
+ }
114
+ return true;
115
+ } catch (err) {
116
+ this.log(`bulk network error: ${err.message} \xB7 will retry`);
117
+ return false;
118
+ }
119
+ }
120
+ log(msg) {
121
+ if (this.debug) console.warn(`[holdy] ${msg}`);
122
+ }
123
+ /**
124
+ * Ambiente que o SDK observa sozinho (url, referrer, browser, utm…).
125
+ * Vazio no core; `HoldyWeb` preenche. O server não infere ambiente — não
126
+ * existe "página atual" num worker.
127
+ */
128
+ environmentContext() {
129
+ return {};
130
+ }
131
+ /** Interno · exposto pra testes. */
132
+ get pending() {
133
+ return this.queue.length;
134
+ }
135
+ };
136
+
137
+ // src/server.ts
138
+ var Holdy = class extends HoldyCore {
139
+ constructor(opts) {
140
+ super("sdk_server", opts);
141
+ }
142
+ };
143
+
144
+ // src/auto-capture.ts
145
+ var INSTALLED_FLAG = "__holdy_autocapture__";
146
+ var ROUTE_DEBOUNCE_MS = 50;
147
+ function installAutoCapture(opts) {
148
+ if (typeof window === "undefined" || typeof history === "undefined") {
149
+ return () => void 0;
150
+ }
151
+ const w = window;
152
+ if (w[INSTALLED_FLAG]) return () => void 0;
153
+ w[INSTALLED_FLAG] = true;
154
+ const cleanups = [];
155
+ let lastPath = currentPath();
156
+ let pending = null;
157
+ function emit(kind) {
158
+ try {
159
+ opts.onNavigate(kind);
160
+ } catch {
161
+ }
162
+ }
163
+ function scheduleRouteEmit() {
164
+ if (pending) clearTimeout(pending);
165
+ pending = setTimeout(() => {
166
+ pending = null;
167
+ const path = currentPath();
168
+ if (path === lastPath) return;
169
+ lastPath = path;
170
+ emit("route_change");
171
+ }, ROUTE_DEBOUNCE_MS);
172
+ }
173
+ emit("initial_load");
174
+ const originalPush = history.pushState;
175
+ const originalReplace = history.replaceState;
176
+ history.pushState = function patchedPush(...args) {
177
+ const result = originalPush.apply(this, args);
178
+ scheduleRouteEmit();
179
+ return result;
180
+ };
181
+ history.replaceState = function patchedReplace(...args) {
182
+ const result = originalReplace.apply(this, args);
183
+ scheduleRouteEmit();
184
+ return result;
185
+ };
186
+ cleanups.push(() => {
187
+ history.pushState = originalPush;
188
+ history.replaceState = originalReplace;
189
+ });
190
+ const onPop = () => scheduleRouteEmit();
191
+ window.addEventListener("popstate", onPop);
192
+ cleanups.push(() => window.removeEventListener("popstate", onPop));
193
+ const onHash = () => scheduleRouteEmit();
194
+ window.addEventListener("hashchange", onHash);
195
+ cleanups.push(() => window.removeEventListener("hashchange", onHash));
196
+ return () => {
197
+ if (pending) clearTimeout(pending);
198
+ for (const cleanup of cleanups) cleanup();
199
+ w[INSTALLED_FLAG] = false;
200
+ };
201
+ }
202
+ function currentPath() {
203
+ if (typeof location === "undefined") return "/";
204
+ return `${location.pathname}${location.search}${location.hash}`;
205
+ }
206
+
207
+ // src/web-context.ts
208
+ var UTM_KEYS = [
209
+ "utm_source",
210
+ "utm_medium",
211
+ "utm_campaign",
212
+ "utm_term",
213
+ "utm_content"
214
+ ];
215
+ var SESSION_STORAGE_KEY = "holdy_session";
216
+ var ATTRIBUTION_STORAGE_KEY = "holdy_attribution";
217
+ function stripUrl(href) {
218
+ try {
219
+ const url = new URL(href);
220
+ return `${url.origin}${url.pathname}`;
221
+ } catch {
222
+ return null;
223
+ }
224
+ }
225
+ function parseUtm(href) {
226
+ const out = {};
227
+ try {
228
+ const params = new URL(href).searchParams;
229
+ for (const key of UTM_KEYS) {
230
+ const value = params.get(key)?.trim();
231
+ if (value) out[key] = value.toLowerCase().slice(0, 120);
232
+ }
233
+ } catch {
234
+ }
235
+ return out;
236
+ }
237
+ function parseUserAgent(ua) {
238
+ if (!ua) return {};
239
+ const s = ua.toLowerCase();
240
+ const browser = s.includes("edg/") ? "Edge" : s.includes("opr/") || s.includes("opera") ? "Opera" : s.includes("samsungbrowser") ? "Samsung Internet" : s.includes("firefox") || s.includes("fxios") ? "Firefox" : s.includes("chrome") || s.includes("crios") ? "Chrome" : s.includes("safari") ? "Safari" : void 0;
241
+ const os = s.includes("iphone") || s.includes("ipad") || s.includes("ipod") ? "iOS" : s.includes("android") ? "Android" : s.includes("windows") ? "Windows" : s.includes("mac os") || s.includes("macintosh") ? "macOS" : s.includes("cros") ? "ChromeOS" : s.includes("linux") ? "Linux" : void 0;
242
+ const device = s.includes("ipad") || s.includes("android") && !s.includes("mobile") ? "tablet" : s.includes("mobi") || s.includes("iphone") || s.includes("ipod") ? "mobile" : "desktop";
243
+ return {
244
+ ...browser ? { browser } : {},
245
+ ...os ? { os } : {},
246
+ device
247
+ };
248
+ }
249
+ function readStorage(storage, key) {
250
+ try {
251
+ const s = storage === "session" ? sessionStorage : localStorage;
252
+ return s.getItem(key);
253
+ } catch {
254
+ return null;
255
+ }
256
+ }
257
+ function writeStorage(storage, key, value) {
258
+ try {
259
+ const s = storage === "session" ? sessionStorage : localStorage;
260
+ s.setItem(key, value);
261
+ } catch {
262
+ }
263
+ }
264
+ function randomId() {
265
+ try {
266
+ return crypto.randomUUID();
267
+ } catch {
268
+ return `hol-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
269
+ }
270
+ }
271
+ function resolveSessionId() {
272
+ const existing = readStorage("session", SESSION_STORAGE_KEY);
273
+ if (existing) return existing;
274
+ const fresh = randomId();
275
+ writeStorage("session", SESSION_STORAGE_KEY, fresh);
276
+ return fresh;
277
+ }
278
+ function resolveAttribution(href, referrer, currentHost) {
279
+ const stored = readStorage("session", ATTRIBUTION_STORAGE_KEY);
280
+ if (stored) {
281
+ try {
282
+ return JSON.parse(stored);
283
+ } catch {
284
+ }
285
+ }
286
+ const attribution = { ...parseUtm(href) };
287
+ const referrerHost = hostOf(referrer);
288
+ if (referrerHost && referrerHost !== currentHost) {
289
+ const clean = stripUrl(referrer);
290
+ if (clean) attribution.referrer = clean;
291
+ attribution.referrer_host = referrerHost;
292
+ }
293
+ writeStorage("session", ATTRIBUTION_STORAGE_KEY, JSON.stringify(attribution));
294
+ return attribution;
295
+ }
296
+ function hostOf(href) {
297
+ try {
298
+ return new URL(href).hostname.toLowerCase() || null;
299
+ } catch {
300
+ return null;
301
+ }
302
+ }
303
+ function buildWebContext() {
304
+ if (typeof window === "undefined" || typeof location === "undefined") return {};
305
+ const href = location.href;
306
+ const referrer = typeof document !== "undefined" ? document.referrer : "";
307
+ const host = location.hostname.toLowerCase();
308
+ const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
309
+ const { browser, os, device } = parseUserAgent(ua);
310
+ const url = stripUrl(href);
311
+ return {
312
+ ...url ? { url } : {},
313
+ path: location.pathname || "/",
314
+ host,
315
+ ...browser ? { browser } : {},
316
+ ...os ? { os } : {},
317
+ ...device ? { device } : {},
318
+ ...typeof navigator !== "undefined" && navigator.language ? { locale: navigator.language } : {},
319
+ session_id: resolveSessionId(),
320
+ ...resolveAttribution(href, referrer, host)
321
+ };
322
+ }
323
+
324
+ // src/web.ts
325
+ var ANON_STORAGE_KEY = "holdy_anon_id";
326
+ var HoldyWeb = class extends HoldyCore {
327
+ teardownAutoCapture = null;
328
+ constructor(opts) {
329
+ super("sdk_web", opts);
330
+ this.anonymousId = resolveAnonymousId();
331
+ if (typeof window !== "undefined") {
332
+ window.addEventListener("pagehide", () => void this.flush());
333
+ document.addEventListener("visibilitychange", () => {
334
+ if (document.visibilityState === "hidden") void this.flush();
335
+ });
336
+ }
337
+ if (opts.autoCapture !== false) {
338
+ this.teardownAutoCapture = installAutoCapture({
339
+ onNavigate: (kind) => this.page(void 0, { navigation: kind })
340
+ });
341
+ }
342
+ }
343
+ /**
344
+ * Pageview. O `$` marca evento emitido pelo SDK, não pelo produto (mesma
345
+ * convenção de `$identify`/`$group`); a canonicalização downstream remove o
346
+ * sigilo, então `$pageview` e `pageview` são o mesmo evento na leitura.
347
+ * url/path/referrer/utm já vêm no context de todo evento — aqui só entra o
348
+ * que é da página como conteúdo (nome e título).
349
+ */
350
+ page(name, properties) {
351
+ this.track("$pageview", {
352
+ ...properties ?? {},
353
+ ...name ? { page_name: name } : {},
354
+ ...typeof document !== "undefined" && document.title ? { title: document.title } : {}
355
+ });
356
+ }
357
+ reset() {
358
+ this.userId = null;
359
+ this.accountKey = null;
360
+ try {
361
+ localStorage.removeItem(ANON_STORAGE_KEY);
362
+ } catch {
363
+ }
364
+ this.anonymousId = resolveAnonymousId();
365
+ }
366
+ /** Flush final + desinstala o auto-capture (devolve `history` ao original). */
367
+ async shutdown() {
368
+ this.teardownAutoCapture?.();
369
+ this.teardownAutoCapture = null;
370
+ await super.shutdown();
371
+ }
372
+ environmentContext() {
373
+ return buildWebContext();
374
+ }
375
+ };
376
+ function resolveAnonymousId() {
377
+ try {
378
+ const existing = localStorage.getItem(ANON_STORAGE_KEY);
379
+ if (existing) return existing;
380
+ const fresh = crypto.randomUUID();
381
+ localStorage.setItem(ANON_STORAGE_KEY, fresh);
382
+ return fresh;
383
+ } catch {
384
+ return crypto.randomUUID();
385
+ }
386
+ }
387
+ var singleton = null;
388
+ function initHoldy(opts) {
389
+ if (!singleton) singleton = new HoldyWeb(opts);
390
+ return singleton;
391
+ }
392
+ export {
393
+ DEFAULT_BASE_URL,
394
+ Holdy,
395
+ HoldyCore,
396
+ HoldyWeb,
397
+ initHoldy
398
+ };
399
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/core.ts", "../src/server.ts", "../src/auto-capture.ts", "../src/web-context.ts", "../src/web.ts"],
4
+ "sourcesContent": ["/**\n * Core do @holdyapp/sdk \u00B7 fila + flush pra POST /api/v1/events/bulk.\n *\n * Retry de batch inteiro \u00E9 seguro (partial accept + dedupe server-side).\n * Fila \u00E9 bounded: acima de MAX_QUEUE dropa os mais antigos (telemetria nunca\n * derruba o app do cliente \u2014 nenhum m\u00E9todo p\u00FAblico lan\u00E7a).\n */\n\nimport type { CanonicalEvent, HoldyOptions, Properties, TrackOverrides } from './types'\n\nexport const DEFAULT_BASE_URL = 'https://holdy.chat/api/v1'\nconst DEFAULT_FLUSH_AT = 20\nconst DEFAULT_FLUSH_INTERVAL_MS = 5000\nconst MAX_QUEUE = 1000\nconst BULK_LIMIT = 500\n\nexport class HoldyCore {\n private readonly apiKey: string\n private readonly baseUrl: string\n private readonly flushAt: number\n private readonly debug: boolean\n private readonly source: 'sdk_web' | 'sdk_server'\n\n private queue: CanonicalEvent[] = []\n private timer: ReturnType<typeof setInterval> | null = null\n private flushing: Promise<void> | null = null\n\n protected userId: string | null = null\n protected anonymousId: string | null = null\n protected accountKey: string | null = null\n\n constructor(source: 'sdk_web' | 'sdk_server', opts: HoldyOptions) {\n this.source = source\n this.apiKey = opts.apiKey\n this.accountKey = opts.accountKey ?? null\n this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, '')\n this.flushAt = Math.max(1, opts.flushAt ?? DEFAULT_FLUSH_AT)\n this.debug = opts.debug ?? false\n\n const intervalMs = opts.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS\n if (intervalMs > 0) {\n this.timer = setInterval(() => void this.flush(), intervalMs)\n // Node: n\u00E3o segura o processo vivo s\u00F3 pelo timer do SDK.\n if (typeof (this.timer as { unref?: () => void }).unref === 'function') {\n ;(this.timer as unknown as { unref: () => void }).unref()\n }\n }\n }\n\n track(name: string, properties?: Properties, overrides?: TrackOverrides): void {\n if (!name || typeof name !== 'string') return\n\n const userId = overrides?.userId ?? this.userId ?? undefined\n const accountKey = overrides?.accountKey ?? this.accountKey ?? undefined\n const occurredAt = overrides?.occurredAt\n ? new Date(overrides.occurredAt).toISOString()\n : new Date().toISOString()\n\n // Ordem: ambiente observado pelo SDK \u2192 context da chamada \u2192 carrier de\n // account. O que o caller passou vence o inferido; o carrier vence tudo,\n // porque \u00E9 o que a Data layer usa pra resolver a conta.\n const context: Properties = {\n ...this.environmentContext(),\n ...(overrides?.context ?? {}),\n }\n if (accountKey) context.account_external_id = accountKey\n\n const event: CanonicalEvent = {\n event: name,\n occurred_at: occurredAt,\n source: this.source,\n ...(userId ? { user_id: userId } : {}),\n ...(this.anonymousId ? { anonymous_id: this.anonymousId } : {}),\n properties: properties ?? {},\n ...(Object.keys(context).length > 0 ? { context } : {}),\n }\n\n this.queue.push(event)\n if (this.queue.length > MAX_QUEUE) this.queue.splice(0, this.queue.length - MAX_QUEUE)\n if (this.queue.length >= this.flushAt) void this.flush()\n }\n\n identify(userId: string, traits?: Properties): void {\n if (!userId || typeof userId !== 'string') return\n this.userId = userId\n this.track('$identify', traits ?? {})\n }\n\n group(accountKey: string, traits?: Properties): void {\n if (!accountKey || typeof accountKey !== 'string') return\n this.accountKey = accountKey\n this.track('$group', traits ?? {})\n }\n\n /** Drena a fila. Nunca lan\u00E7a \u2014 falha re-enfileira e loga em debug. */\n async flush(): Promise<void> {\n // Serializa flushes concorrentes (timer + flushAt + manual).\n if (this.flushing) return this.flushing\n if (this.queue.length === 0) return\n\n this.flushing = this.drain().finally(() => {\n this.flushing = null\n })\n return this.flushing\n }\n\n /** Flush final + para o timer. Chamar em shutdown do server / unload do web. */\n async shutdown(): Promise<void> {\n if (this.timer) {\n clearInterval(this.timer)\n this.timer = null\n }\n await this.flush()\n }\n\n private async drain(): Promise<void> {\n while (this.queue.length > 0) {\n const batch = this.queue.splice(0, BULK_LIMIT)\n const ok = await this.send(batch)\n if (!ok) {\n // Devolve pra frente da fila e para \u2014 pr\u00F3ximo flush re-tenta (dedupe-safe).\n this.queue.unshift(...batch)\n return\n }\n }\n }\n\n private async send(events: CanonicalEvent[]): Promise<boolean> {\n try {\n const res = await fetch(`${this.baseUrl}/events/bulk`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ events }),\n ...(this.source === 'sdk_web' ? { keepalive: true } : {}),\n })\n if (!res.ok) {\n // 4xx: batch estruturalmente inv\u00E1lido n\u00E3o re-tenta (evita loop infinito).\n if (res.status >= 400 && res.status < 500) {\n this.log(`bulk rejected ${res.status} \u00B7 batch dropped`)\n return true\n }\n this.log(`bulk failed ${res.status} \u00B7 will retry`)\n return false\n }\n return true\n } catch (err) {\n this.log(`bulk network error: ${(err as Error).message} \u00B7 will retry`)\n return false\n }\n }\n\n private log(msg: string): void {\n if (this.debug) console.warn(`[holdy] ${msg}`)\n }\n\n /**\n * Ambiente que o SDK observa sozinho (url, referrer, browser, utm\u2026).\n * Vazio no core; `HoldyWeb` preenche. O server n\u00E3o infere ambiente \u2014 n\u00E3o\n * existe \"p\u00E1gina atual\" num worker.\n */\n protected environmentContext(): Properties {\n return {}\n }\n\n /** Interno \u00B7 exposto pra testes. */\n get pending(): number {\n return this.queue.length\n }\n}\n", "/**\n * @holdyapp/sdk (server) \u00B7 Node/Edge (source: sdk_server).\n *\n * Stateless por default: user_id/accountKey v\u00E3o por chamada em overrides.\n * identify()/group() de sess\u00E3o existem mas em server multi-tenant prefira\n * overrides \u2014 estado compartilhado entre requests vaza identidade.\n */\n\nimport { HoldyCore } from './core'\nimport type { HoldyOptions } from './types'\n\nexport class Holdy extends HoldyCore {\n constructor(opts: HoldyOptions) {\n super('sdk_server', opts)\n }\n}\n\nexport { HoldyCore, DEFAULT_BASE_URL } from './core'\nexport type { HoldyOptions, Properties, TrackOverrides, CanonicalEvent } from './types'\n", "/**\n * Auto-capture de navega\u00E7\u00E3o \u00B7 @holdyapp/sdk/web.\n *\n * Emite pageview no carregamento e a cada mudan\u00E7a de rota, inclusive nas que\n * n\u00E3o recarregam a p\u00E1gina (SPA). Sem isto, instrumentar exige que o cliente\n * chame `page()` em cada tela \u2014 e o primeiro dado s\u00F3 aparece depois de um\n * trabalho que ele ainda n\u00E3o fez.\n *\n * Idempotente: reinstalar \u00E9 no-op (guard global). O retorno desfaz o patch de\n * `history`, ent\u00E3o `destroy()` devolve o browser como estava.\n */\n\nconst INSTALLED_FLAG = '__holdy_autocapture__'\n\n/** pushState seguido de replaceState \u00E9 uma mudan\u00E7a l\u00F3gica s\u00F3. */\nconst ROUTE_DEBOUNCE_MS = 50\n\nexport type NavigationKind = 'initial_load' | 'route_change'\n\nexport interface AutoCaptureOptions {\n onNavigate: (kind: NavigationKind) => void\n}\n\ninterface GlobalWithFlag {\n [INSTALLED_FLAG]?: boolean\n}\n\nexport function installAutoCapture(opts: AutoCaptureOptions): () => void {\n if (typeof window === 'undefined' || typeof history === 'undefined') {\n return () => undefined\n }\n\n const w = window as unknown as GlobalWithFlag\n if (w[INSTALLED_FLAG]) return () => undefined\n w[INSTALLED_FLAG] = true\n\n const cleanups: Array<() => void> = []\n let lastPath = currentPath()\n let pending: ReturnType<typeof setTimeout> | null = null\n\n function emit(kind: NavigationKind): void {\n try {\n opts.onNavigate(kind)\n } catch {\n // Telemetria nunca derruba a app do cliente.\n }\n }\n\n function scheduleRouteEmit(): void {\n if (pending) clearTimeout(pending)\n pending = setTimeout(() => {\n pending = null\n const path = currentPath()\n // S\u00F3 conta como pageview se a rota mudou de fato \u2014 replaceState de\n // estado interno (filtro, scroll) n\u00E3o \u00E9 navega\u00E7\u00E3o.\n if (path === lastPath) return\n lastPath = path\n emit('route_change')\n }, ROUTE_DEBOUNCE_MS)\n }\n\n emit('initial_load')\n\n const originalPush = history.pushState\n const originalReplace = history.replaceState\n\n history.pushState = function patchedPush(...args: Parameters<History['pushState']>) {\n const result = originalPush.apply(this, args)\n scheduleRouteEmit()\n return result\n }\n history.replaceState = function patchedReplace(...args: Parameters<History['replaceState']>) {\n const result = originalReplace.apply(this, args)\n scheduleRouteEmit()\n return result\n }\n cleanups.push(() => {\n history.pushState = originalPush\n history.replaceState = originalReplace\n })\n\n // Voltar/avan\u00E7ar do browser e routers baseados em hash.\n const onPop = (): void => scheduleRouteEmit()\n window.addEventListener('popstate', onPop)\n cleanups.push(() => window.removeEventListener('popstate', onPop))\n\n const onHash = (): void => scheduleRouteEmit()\n window.addEventListener('hashchange', onHash)\n cleanups.push(() => window.removeEventListener('hashchange', onHash))\n\n return () => {\n if (pending) clearTimeout(pending)\n for (const cleanup of cleanups) cleanup()\n w[INSTALLED_FLAG] = false\n }\n}\n\n/** Rota compar\u00E1vel \u00B7 path + query, sem hash de \u00E2ncora. */\nfunction currentPath(): string {\n if (typeof location === 'undefined') return '/'\n return `${location.pathname}${location.search}${location.hash}`\n}\n", "/**\n * Contexto de ambiente do browser \u00B7 @holdyapp/sdk/web.\n *\n * Vai em `context` de todo evento, nunca em `properties`: properties \u00E9 o\n * vocabul\u00E1rio do produto do cliente (`workflow_key`, `plan`), context \u00E9 o que o\n * SDK observou sozinho. A Data layer persiste os dois (facts.context, 00247) e\n * a leitura de dimens\u00E3o consulta os dois.\n *\n * Tr\u00EAs decis\u00F5es que definem se esse dado \u00E9 \u00FAtil ou lixo:\n * \u00B7 `url` sai sem query e sem hash. Href cru tem uma varia\u00E7\u00E3o por sess\u00E3o, e\n * dimens\u00E3o com cardinalidade quase-\u00FAnica \u00E9 descartada na leitura \u2014 a\n * p\u00E1gina vira ru\u00EDdo exatamente por excesso de precis\u00E3o.\n * \u00B7 utm \u00E9 capturado na primeira p\u00E1gina e carregado pelo resto da sess\u00E3o.\n * Atribui\u00E7\u00E3o que s\u00F3 existe no primeiro evento n\u00E3o responde nada.\n * \u00B7 referrer s\u00F3 quando \u00E9 externo. Navega\u00E7\u00E3o interna preenchendo \"Origem do\n * tr\u00E1fego\" com o pr\u00F3prio dom\u00EDnio esconde a origem real.\n *\n * Tudo aqui \u00E9 best-effort: browser em modo privativo, SSR e webview sem\n * storage precisam funcionar. Nenhuma fun\u00E7\u00E3o lan\u00E7a.\n */\n\nexport type WebContext = Record<string, string>\n\nconst UTM_KEYS = [\n 'utm_source',\n 'utm_medium',\n 'utm_campaign',\n 'utm_term',\n 'utm_content',\n] as const\n\nconst SESSION_STORAGE_KEY = 'holdy_session'\nconst ATTRIBUTION_STORAGE_KEY = 'holdy_attribution'\n\n/* \u2500\u2500 URL \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/** `https://app.x.com/a/b?q=1#z` \u2192 `https://app.x.com/a/b`. */\nexport function stripUrl(href: string): string | null {\n try {\n const url = new URL(href)\n return `${url.origin}${url.pathname}`\n } catch {\n return null\n }\n}\n\n/** Par\u00E2metros de campanha presentes na URL, j\u00E1 em lowercase. */\nexport function parseUtm(href: string): WebContext {\n const out: WebContext = {}\n try {\n const params = new URL(href).searchParams\n for (const key of UTM_KEYS) {\n const value = params.get(key)?.trim()\n if (value) out[key] = value.toLowerCase().slice(0, 120)\n }\n } catch {\n // href inv\u00E1lido \u2014 sem utm, sem drama.\n }\n return out\n}\n\n/* \u2500\u2500 User agent \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\ninterface UaParts {\n browser?: string\n os?: string\n device?: string\n}\n\n/**\n * Fatia m\u00EDnima do UA: navegador, sistema e classe de dispositivo.\n *\n * N\u00E3o \u00E9 biblioteca de UA parsing e n\u00E3o deve virar uma \u2014 o valor est\u00E1 em\n * separar mobile de desktop e Safari de Chrome, n\u00E3o em cravar a vers\u00E3o.\n */\nexport function parseUserAgent(ua: string): UaParts {\n if (!ua) return {}\n const s = ua.toLowerCase()\n\n const browser = s.includes('edg/')\n ? 'Edge'\n : s.includes('opr/') || s.includes('opera')\n ? 'Opera'\n : s.includes('samsungbrowser')\n ? 'Samsung Internet'\n : s.includes('firefox') || s.includes('fxios')\n ? 'Firefox'\n : s.includes('chrome') || s.includes('crios')\n ? 'Chrome'\n : s.includes('safari')\n ? 'Safari'\n : undefined\n\n const os = s.includes('iphone') || s.includes('ipad') || s.includes('ipod')\n ? 'iOS'\n : s.includes('android')\n ? 'Android'\n : s.includes('windows')\n ? 'Windows'\n : s.includes('mac os') || s.includes('macintosh')\n ? 'macOS'\n : s.includes('cros')\n ? 'ChromeOS'\n : s.includes('linux')\n ? 'Linux'\n : undefined\n\n // iPad moderno se declara macOS; a pista que sobra \u00E9 o suporte a toque, que\n // o parser puro n\u00E3o v\u00EA. Tablet aqui cobre o caso declarado no UA.\n const device = s.includes('ipad') || (s.includes('android') && !s.includes('mobile'))\n ? 'tablet'\n : s.includes('mobi') || s.includes('iphone') || s.includes('ipod')\n ? 'mobile'\n : 'desktop'\n\n return {\n ...(browser ? { browser } : {}),\n ...(os ? { os } : {}),\n device,\n }\n}\n\n/* \u2500\u2500 Storage \u00B7 nunca lan\u00E7a \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\nfunction readStorage(storage: 'session' | 'local', key: string): string | null {\n try {\n const s = storage === 'session' ? sessionStorage : localStorage\n return s.getItem(key)\n } catch {\n return null\n }\n}\n\nfunction writeStorage(storage: 'session' | 'local', key: string, value: string): void {\n try {\n const s = storage === 'session' ? sessionStorage : localStorage\n s.setItem(key, value)\n } catch {\n // Modo privativo / storage bloqueado \u2014 o contexto degrada, o evento segue.\n }\n}\n\nfunction randomId(): string {\n try {\n return crypto.randomUUID()\n } catch {\n return `hol-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`\n }\n}\n\n/** ID da sess\u00E3o de browser \u00B7 vive enquanto a aba viver. */\nexport function resolveSessionId(): string {\n const existing = readStorage('session', SESSION_STORAGE_KEY)\n if (existing) return existing\n const fresh = randomId()\n writeStorage('session', SESSION_STORAGE_KEY, fresh)\n return fresh\n}\n\n/**\n * Atribui\u00E7\u00E3o da sess\u00E3o: utm da primeira p\u00E1gina + referrer externo de entrada.\n * Gravada uma vez e repetida em todo evento da sess\u00E3o.\n */\nexport function resolveAttribution(href: string, referrer: string, currentHost: string): WebContext {\n const stored = readStorage('session', ATTRIBUTION_STORAGE_KEY)\n if (stored) {\n try {\n return JSON.parse(stored) as WebContext\n } catch {\n // Valor corrompido \u2014 recalcula por cima.\n }\n }\n\n const attribution: WebContext = { ...parseUtm(href) }\n\n const referrerHost = hostOf(referrer)\n // Referrer interno n\u00E3o \u00E9 origem de tr\u00E1fego, \u00E9 navega\u00E7\u00E3o.\n if (referrerHost && referrerHost !== currentHost) {\n const clean = stripUrl(referrer)\n if (clean) attribution.referrer = clean\n attribution.referrer_host = referrerHost\n }\n\n writeStorage('session', ATTRIBUTION_STORAGE_KEY, JSON.stringify(attribution))\n return attribution\n}\n\nfunction hostOf(href: string): string | null {\n try {\n return new URL(href).hostname.toLowerCase() || null\n } catch {\n return null\n }\n}\n\n/* \u2500\u2500 Montagem \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/**\n * Contexto completo do evento atual. Fora do browser devolve `{}` \u2014 o SDK\n * server tem o pr\u00F3prio caminho e n\u00E3o deve inventar ambiente.\n */\nexport function buildWebContext(): WebContext {\n if (typeof window === 'undefined' || typeof location === 'undefined') return {}\n\n const href = location.href\n const referrer = typeof document !== 'undefined' ? document.referrer : ''\n const host = location.hostname.toLowerCase()\n\n const ua = typeof navigator !== 'undefined' ? navigator.userAgent : ''\n const { browser, os, device } = parseUserAgent(ua)\n\n const url = stripUrl(href)\n\n return {\n ...(url ? { url } : {}),\n path: location.pathname || '/',\n host,\n ...(browser ? { browser } : {}),\n ...(os ? { os } : {}),\n ...(device ? { device } : {}),\n ...(typeof navigator !== 'undefined' && navigator.language\n ? { locale: navigator.language }\n : {}),\n session_id: resolveSessionId(),\n ...resolveAttribution(href, referrer, host),\n }\n}\n", "/**\n * @holdyapp/sdk/web \u00B7 browser (source: sdk_web).\n *\n * anonymous_id gerado e persistido em localStorage (holdy_anon_id) \u2014 enviado em\n * todo event at\u00E9 identify() promover pra user_id (identity graph faz o merge\n * downstream, nunca o SDK). Flush no pagehide via fetch keepalive.\n *\n * Todo evento carrega o contexto de ambiente (url, path, referrer externo,\n * utm da sess\u00E3o, browser, os, device, locale, session_id) em `context` \u2014\n * separado das properties do produto. Pageview \u00E9 autom\u00E1tico por default:\n * carregamento inicial e cada mudan\u00E7a de rota, inclusive em SPA.\n */\n\nimport { HoldyCore } from './core'\nimport { installAutoCapture, type NavigationKind } from './auto-capture'\nimport { buildWebContext } from './web-context'\nimport type { HoldyOptions, Properties } from './types'\n\nconst ANON_STORAGE_KEY = 'holdy_anon_id'\n\nexport interface HoldyWebOptions extends HoldyOptions {\n /**\n * Emitir `$pageview` sozinho no load e a cada mudan\u00E7a de rota \u00B7 default true.\n * Desligue s\u00F3 se o produto j\u00E1 chama `page()` na m\u00E3o em cada tela.\n */\n autoCapture?: boolean\n}\n\nexport class HoldyWeb extends HoldyCore {\n private teardownAutoCapture: (() => void) | null = null\n\n constructor(opts: HoldyWebOptions) {\n super('sdk_web', opts)\n this.anonymousId = resolveAnonymousId()\n\n if (typeof window !== 'undefined') {\n window.addEventListener('pagehide', () => void this.flush())\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'hidden') void this.flush()\n })\n }\n\n if (opts.autoCapture !== false) {\n this.teardownAutoCapture = installAutoCapture({\n onNavigate: (kind: NavigationKind) => this.page(undefined, { navigation: kind }),\n })\n }\n }\n\n /**\n * Pageview. O `$` marca evento emitido pelo SDK, n\u00E3o pelo produto (mesma\n * conven\u00E7\u00E3o de `$identify`/`$group`); a canonicaliza\u00E7\u00E3o downstream remove o\n * sigilo, ent\u00E3o `$pageview` e `pageview` s\u00E3o o mesmo evento na leitura.\n * url/path/referrer/utm j\u00E1 v\u00EAm no context de todo evento \u2014 aqui s\u00F3 entra o\n * que \u00E9 da p\u00E1gina como conte\u00FAdo (nome e t\u00EDtulo).\n */\n page(name?: string, properties?: Properties): void {\n this.track('$pageview', {\n ...(properties ?? {}),\n ...(name ? { page_name: name } : {}),\n ...(typeof document !== 'undefined' && document.title ? { title: document.title } : {}),\n })\n }\n\n reset(): void {\n this.userId = null\n this.accountKey = null\n try {\n localStorage.removeItem(ANON_STORAGE_KEY)\n } catch {\n // storage indispon\u00EDvel (SSR/privacy mode) \u2014 ok\n }\n this.anonymousId = resolveAnonymousId()\n }\n\n /** Flush final + desinstala o auto-capture (devolve `history` ao original). */\n override async shutdown(): Promise<void> {\n this.teardownAutoCapture?.()\n this.teardownAutoCapture = null\n await super.shutdown()\n }\n\n protected override environmentContext(): Properties {\n return buildWebContext()\n }\n}\n\nfunction resolveAnonymousId(): string {\n try {\n const existing = localStorage.getItem(ANON_STORAGE_KEY)\n if (existing) return existing\n const fresh = crypto.randomUUID()\n localStorage.setItem(ANON_STORAGE_KEY, fresh)\n return fresh\n } catch {\n return crypto.randomUUID()\n }\n}\n\nlet singleton: HoldyWeb | null = null\n\n/** Init idempotente \u2014 segunda chamada retorna a inst\u00E2ncia existente. */\nexport function initHoldy(opts: HoldyWebOptions): HoldyWeb {\n if (!singleton) singleton = new HoldyWeb(opts)\n return singleton\n}\n\nexport type { HoldyOptions, Properties }\n"],
5
+ "mappings": ";AAUO,IAAM,mBAAmB;AAChC,IAAM,mBAAmB;AACzB,IAAM,4BAA4B;AAClC,IAAM,YAAY;AAClB,IAAM,aAAa;AAEZ,IAAM,YAAN,MAAgB;AAAA,EACJ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAET,QAA0B,CAAC;AAAA,EAC3B,QAA+C;AAAA,EAC/C,WAAiC;AAAA,EAE/B,SAAwB;AAAA,EACxB,cAA6B;AAAA,EAC7B,aAA4B;AAAA,EAEtC,YAAY,QAAkC,MAAoB;AAChE,SAAK,SAAS;AACd,SAAK,SAAS,KAAK;AACnB,SAAK,aAAa,KAAK,cAAc;AACrC,SAAK,WAAW,KAAK,WAAW,kBAAkB,QAAQ,OAAO,EAAE;AACnE,SAAK,UAAU,KAAK,IAAI,GAAG,KAAK,WAAW,gBAAgB;AAC3D,SAAK,QAAQ,KAAK,SAAS;AAE3B,UAAM,aAAa,KAAK,mBAAmB;AAC3C,QAAI,aAAa,GAAG;AAClB,WAAK,QAAQ,YAAY,MAAM,KAAK,KAAK,MAAM,GAAG,UAAU;AAE5D,UAAI,OAAQ,KAAK,MAAiC,UAAU,YAAY;AACtE;AAAC,QAAC,KAAK,MAA2C,MAAM;AAAA,MAC1D;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,MAAc,YAAyB,WAAkC;AAC7E,QAAI,CAAC,QAAQ,OAAO,SAAS,SAAU;AAEvC,UAAM,SAAS,WAAW,UAAU,KAAK,UAAU;AACnD,UAAM,aAAa,WAAW,cAAc,KAAK,cAAc;AAC/D,UAAM,aAAa,WAAW,aAC1B,IAAI,KAAK,UAAU,UAAU,EAAE,YAAY,KAC3C,oBAAI,KAAK,GAAE,YAAY;AAK3B,UAAM,UAAsB;AAAA,MAC1B,GAAG,KAAK,mBAAmB;AAAA,MAC3B,GAAI,WAAW,WAAW,CAAC;AAAA,IAC7B;AACA,QAAI,WAAY,SAAQ,sBAAsB;AAE9C,UAAM,QAAwB;AAAA,MAC5B,OAAO;AAAA,MACP,aAAa;AAAA,MACb,QAAQ,KAAK;AAAA,MACb,GAAI,SAAS,EAAE,SAAS,OAAO,IAAI,CAAC;AAAA,MACpC,GAAI,KAAK,cAAc,EAAE,cAAc,KAAK,YAAY,IAAI,CAAC;AAAA,MAC7D,YAAY,cAAc,CAAC;AAAA,MAC3B,GAAI,OAAO,KAAK,OAAO,EAAE,SAAS,IAAI,EAAE,QAAQ,IAAI,CAAC;AAAA,IACvD;AAEA,SAAK,MAAM,KAAK,KAAK;AACrB,QAAI,KAAK,MAAM,SAAS,UAAW,MAAK,MAAM,OAAO,GAAG,KAAK,MAAM,SAAS,SAAS;AACrF,QAAI,KAAK,MAAM,UAAU,KAAK,QAAS,MAAK,KAAK,MAAM;AAAA,EACzD;AAAA,EAEA,SAAS,QAAgB,QAA2B;AAClD,QAAI,CAAC,UAAU,OAAO,WAAW,SAAU;AAC3C,SAAK,SAAS;AACd,SAAK,MAAM,aAAa,UAAU,CAAC,CAAC;AAAA,EACtC;AAAA,EAEA,MAAM,YAAoB,QAA2B;AACnD,QAAI,CAAC,cAAc,OAAO,eAAe,SAAU;AACnD,SAAK,aAAa;AAClB,SAAK,MAAM,UAAU,UAAU,CAAC,CAAC;AAAA,EACnC;AAAA;AAAA,EAGA,MAAM,QAAuB;AAE3B,QAAI,KAAK,SAAU,QAAO,KAAK;AAC/B,QAAI,KAAK,MAAM,WAAW,EAAG;AAE7B,SAAK,WAAW,KAAK,MAAM,EAAE,QAAQ,MAAM;AACzC,WAAK,WAAW;AAAA,IAClB,CAAC;AACD,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,MAAM,WAA0B;AAC9B,QAAI,KAAK,OAAO;AACd,oBAAc,KAAK,KAAK;AACxB,WAAK,QAAQ;AAAA,IACf;AACA,UAAM,KAAK,MAAM;AAAA,EACnB;AAAA,EAEA,MAAc,QAAuB;AACnC,WAAO,KAAK,MAAM,SAAS,GAAG;AAC5B,YAAM,QAAQ,KAAK,MAAM,OAAO,GAAG,UAAU;AAC7C,YAAM,KAAK,MAAM,KAAK,KAAK,KAAK;AAChC,UAAI,CAAC,IAAI;AAEP,aAAK,MAAM,QAAQ,GAAG,KAAK;AAC3B;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,KAAK,QAA4C;AAC7D,QAAI;AACF,YAAM,MAAM,MAAM,MAAM,GAAG,KAAK,OAAO,gBAAgB;AAAA,QACrD,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,eAAe,UAAU,KAAK,MAAM;AAAA,UACpC,gBAAgB;AAAA,QAClB;AAAA,QACA,MAAM,KAAK,UAAU,EAAE,OAAO,CAAC;AAAA,QAC/B,GAAI,KAAK,WAAW,YAAY,EAAE,WAAW,KAAK,IAAI,CAAC;AAAA,MACzD,CAAC;AACD,UAAI,CAAC,IAAI,IAAI;AAEX,YAAI,IAAI,UAAU,OAAO,IAAI,SAAS,KAAK;AACzC,eAAK,IAAI,iBAAiB,IAAI,MAAM,qBAAkB;AACtD,iBAAO;AAAA,QACT;AACA,aAAK,IAAI,eAAe,IAAI,MAAM,kBAAe;AACjD,eAAO;AAAA,MACT;AACA,aAAO;AAAA,IACT,SAAS,KAAK;AACZ,WAAK,IAAI,uBAAwB,IAAc,OAAO,kBAAe;AACrE,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEQ,IAAI,KAAmB;AAC7B,QAAI,KAAK,MAAO,SAAQ,KAAK,WAAW,GAAG,EAAE;AAAA,EAC/C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOU,qBAAiC;AACzC,WAAO,CAAC;AAAA,EACV;AAAA;AAAA,EAGA,IAAI,UAAkB;AACpB,WAAO,KAAK,MAAM;AAAA,EACpB;AACF;;;AChKO,IAAM,QAAN,cAAoB,UAAU;AAAA,EACnC,YAAY,MAAoB;AAC9B,UAAM,cAAc,IAAI;AAAA,EAC1B;AACF;;;ACHA,IAAM,iBAAiB;AAGvB,IAAM,oBAAoB;AAYnB,SAAS,mBAAmB,MAAsC;AACvE,MAAI,OAAO,WAAW,eAAe,OAAO,YAAY,aAAa;AACnE,WAAO,MAAM;AAAA,EACf;AAEA,QAAM,IAAI;AACV,MAAI,EAAE,cAAc,EAAG,QAAO,MAAM;AACpC,IAAE,cAAc,IAAI;AAEpB,QAAM,WAA8B,CAAC;AACrC,MAAI,WAAW,YAAY;AAC3B,MAAI,UAAgD;AAEpD,WAAS,KAAK,MAA4B;AACxC,QAAI;AACF,WAAK,WAAW,IAAI;AAAA,IACtB,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,WAAS,oBAA0B;AACjC,QAAI,QAAS,cAAa,OAAO;AACjC,cAAU,WAAW,MAAM;AACzB,gBAAU;AACV,YAAM,OAAO,YAAY;AAGzB,UAAI,SAAS,SAAU;AACvB,iBAAW;AACX,WAAK,cAAc;AAAA,IACrB,GAAG,iBAAiB;AAAA,EACtB;AAEA,OAAK,cAAc;AAEnB,QAAM,eAAe,QAAQ;AAC7B,QAAM,kBAAkB,QAAQ;AAEhC,UAAQ,YAAY,SAAS,eAAe,MAAwC;AAClF,UAAM,SAAS,aAAa,MAAM,MAAM,IAAI;AAC5C,sBAAkB;AAClB,WAAO;AAAA,EACT;AACA,UAAQ,eAAe,SAAS,kBAAkB,MAA2C;AAC3F,UAAM,SAAS,gBAAgB,MAAM,MAAM,IAAI;AAC/C,sBAAkB;AAClB,WAAO;AAAA,EACT;AACA,WAAS,KAAK,MAAM;AAClB,YAAQ,YAAY;AACpB,YAAQ,eAAe;AAAA,EACzB,CAAC;AAGD,QAAM,QAAQ,MAAY,kBAAkB;AAC5C,SAAO,iBAAiB,YAAY,KAAK;AACzC,WAAS,KAAK,MAAM,OAAO,oBAAoB,YAAY,KAAK,CAAC;AAEjE,QAAM,SAAS,MAAY,kBAAkB;AAC7C,SAAO,iBAAiB,cAAc,MAAM;AAC5C,WAAS,KAAK,MAAM,OAAO,oBAAoB,cAAc,MAAM,CAAC;AAEpE,SAAO,MAAM;AACX,QAAI,QAAS,cAAa,OAAO;AACjC,eAAW,WAAW,SAAU,SAAQ;AACxC,MAAE,cAAc,IAAI;AAAA,EACtB;AACF;AAGA,SAAS,cAAsB;AAC7B,MAAI,OAAO,aAAa,YAAa,QAAO;AAC5C,SAAO,GAAG,SAAS,QAAQ,GAAG,SAAS,MAAM,GAAG,SAAS,IAAI;AAC/D;;;AC9EA,IAAM,WAAW;AAAA,EACf;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,IAAM,sBAAsB;AAC5B,IAAM,0BAA0B;AAKzB,SAAS,SAAS,MAA6B;AACpD,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,IAAI;AACxB,WAAO,GAAG,IAAI,MAAM,GAAG,IAAI,QAAQ;AAAA,EACrC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,SAAS,SAAS,MAA0B;AACjD,QAAM,MAAkB,CAAC;AACzB,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,IAAI,EAAE;AAC7B,eAAW,OAAO,UAAU;AAC1B,YAAM,QAAQ,OAAO,IAAI,GAAG,GAAG,KAAK;AACpC,UAAI,MAAO,KAAI,GAAG,IAAI,MAAM,YAAY,EAAE,MAAM,GAAG,GAAG;AAAA,IACxD;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAgBO,SAAS,eAAe,IAAqB;AAClD,MAAI,CAAC,GAAI,QAAO,CAAC;AACjB,QAAM,IAAI,GAAG,YAAY;AAEzB,QAAM,UAAU,EAAE,SAAS,MAAM,IAC7B,SACA,EAAE,SAAS,MAAM,KAAK,EAAE,SAAS,OAAO,IACtC,UACA,EAAE,SAAS,gBAAgB,IACzB,qBACA,EAAE,SAAS,SAAS,KAAK,EAAE,SAAS,OAAO,IACzC,YACA,EAAE,SAAS,QAAQ,KAAK,EAAE,SAAS,OAAO,IACxC,WACA,EAAE,SAAS,QAAQ,IACjB,WACA;AAEd,QAAM,KAAK,EAAE,SAAS,QAAQ,KAAK,EAAE,SAAS,MAAM,KAAK,EAAE,SAAS,MAAM,IACtE,QACA,EAAE,SAAS,SAAS,IAClB,YACA,EAAE,SAAS,SAAS,IAClB,YACA,EAAE,SAAS,QAAQ,KAAK,EAAE,SAAS,WAAW,IAC5C,UACA,EAAE,SAAS,MAAM,IACf,aACA,EAAE,SAAS,OAAO,IAChB,UACA;AAId,QAAM,SAAS,EAAE,SAAS,MAAM,KAAM,EAAE,SAAS,SAAS,KAAK,CAAC,EAAE,SAAS,QAAQ,IAC/E,WACA,EAAE,SAAS,MAAM,KAAK,EAAE,SAAS,QAAQ,KAAK,EAAE,SAAS,MAAM,IAC7D,WACA;AAEN,SAAO;AAAA,IACL,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC7B,GAAI,KAAK,EAAE,GAAG,IAAI,CAAC;AAAA,IACnB;AAAA,EACF;AACF;AAIA,SAAS,YAAY,SAA8B,KAA4B;AAC7E,MAAI;AACF,UAAM,IAAI,YAAY,YAAY,iBAAiB;AACnD,WAAO,EAAE,QAAQ,GAAG;AAAA,EACtB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,aAAa,SAA8B,KAAa,OAAqB;AACpF,MAAI;AACF,UAAM,IAAI,YAAY,YAAY,iBAAiB;AACnD,MAAE,QAAQ,KAAK,KAAK;AAAA,EACtB,QAAQ;AAAA,EAER;AACF;AAEA,SAAS,WAAmB;AAC1B,MAAI;AACF,WAAO,OAAO,WAAW;AAAA,EAC3B,QAAQ;AACN,WAAO,OAAO,KAAK,IAAI,EAAE,SAAS,EAAE,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC;AAAA,EAClF;AACF;AAGO,SAAS,mBAA2B;AACzC,QAAM,WAAW,YAAY,WAAW,mBAAmB;AAC3D,MAAI,SAAU,QAAO;AACrB,QAAM,QAAQ,SAAS;AACvB,eAAa,WAAW,qBAAqB,KAAK;AAClD,SAAO;AACT;AAMO,SAAS,mBAAmB,MAAc,UAAkB,aAAiC;AAClG,QAAM,SAAS,YAAY,WAAW,uBAAuB;AAC7D,MAAI,QAAQ;AACV,QAAI;AACF,aAAO,KAAK,MAAM,MAAM;AAAA,IAC1B,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,QAAM,cAA0B,EAAE,GAAG,SAAS,IAAI,EAAE;AAEpD,QAAM,eAAe,OAAO,QAAQ;AAEpC,MAAI,gBAAgB,iBAAiB,aAAa;AAChD,UAAM,QAAQ,SAAS,QAAQ;AAC/B,QAAI,MAAO,aAAY,WAAW;AAClC,gBAAY,gBAAgB;AAAA,EAC9B;AAEA,eAAa,WAAW,yBAAyB,KAAK,UAAU,WAAW,CAAC;AAC5E,SAAO;AACT;AAEA,SAAS,OAAO,MAA6B;AAC3C,MAAI;AACF,WAAO,IAAI,IAAI,IAAI,EAAE,SAAS,YAAY,KAAK;AAAA,EACjD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAQO,SAAS,kBAA8B;AAC5C,MAAI,OAAO,WAAW,eAAe,OAAO,aAAa,YAAa,QAAO,CAAC;AAE9E,QAAM,OAAO,SAAS;AACtB,QAAM,WAAW,OAAO,aAAa,cAAc,SAAS,WAAW;AACvE,QAAM,OAAO,SAAS,SAAS,YAAY;AAE3C,QAAM,KAAK,OAAO,cAAc,cAAc,UAAU,YAAY;AACpE,QAAM,EAAE,SAAS,IAAI,OAAO,IAAI,eAAe,EAAE;AAEjD,QAAM,MAAM,SAAS,IAAI;AAEzB,SAAO;AAAA,IACL,GAAI,MAAM,EAAE,IAAI,IAAI,CAAC;AAAA,IACrB,MAAM,SAAS,YAAY;AAAA,IAC3B;AAAA,IACA,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC7B,GAAI,KAAK,EAAE,GAAG,IAAI,CAAC;AAAA,IACnB,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,IAC3B,GAAI,OAAO,cAAc,eAAe,UAAU,WAC9C,EAAE,QAAQ,UAAU,SAAS,IAC7B,CAAC;AAAA,IACL,YAAY,iBAAiB;AAAA,IAC7B,GAAG,mBAAmB,MAAM,UAAU,IAAI;AAAA,EAC5C;AACF;;;AChNA,IAAM,mBAAmB;AAUlB,IAAM,WAAN,cAAuB,UAAU;AAAA,EAC9B,sBAA2C;AAAA,EAEnD,YAAY,MAAuB;AACjC,UAAM,WAAW,IAAI;AACrB,SAAK,cAAc,mBAAmB;AAEtC,QAAI,OAAO,WAAW,aAAa;AACjC,aAAO,iBAAiB,YAAY,MAAM,KAAK,KAAK,MAAM,CAAC;AAC3D,eAAS,iBAAiB,oBAAoB,MAAM;AAClD,YAAI,SAAS,oBAAoB,SAAU,MAAK,KAAK,MAAM;AAAA,MAC7D,CAAC;AAAA,IACH;AAEA,QAAI,KAAK,gBAAgB,OAAO;AAC9B,WAAK,sBAAsB,mBAAmB;AAAA,QAC5C,YAAY,CAAC,SAAyB,KAAK,KAAK,QAAW,EAAE,YAAY,KAAK,CAAC;AAAA,MACjF,CAAC;AAAA,IACH;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,KAAK,MAAe,YAA+B;AACjD,SAAK,MAAM,aAAa;AAAA,MACtB,GAAI,cAAc,CAAC;AAAA,MACnB,GAAI,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;AAAA,MAClC,GAAI,OAAO,aAAa,eAAe,SAAS,QAAQ,EAAE,OAAO,SAAS,MAAM,IAAI,CAAC;AAAA,IACvF,CAAC;AAAA,EACH;AAAA,EAEA,QAAc;AACZ,SAAK,SAAS;AACd,SAAK,aAAa;AAClB,QAAI;AACF,mBAAa,WAAW,gBAAgB;AAAA,IAC1C,QAAQ;AAAA,IAER;AACA,SAAK,cAAc,mBAAmB;AAAA,EACxC;AAAA;AAAA,EAGA,MAAe,WAA0B;AACvC,SAAK,sBAAsB;AAC3B,SAAK,sBAAsB;AAC3B,UAAM,MAAM,SAAS;AAAA,EACvB;AAAA,EAEmB,qBAAiC;AAClD,WAAO,gBAAgB;AAAA,EACzB;AACF;AAEA,SAAS,qBAA6B;AACpC,MAAI;AACF,UAAM,WAAW,aAAa,QAAQ,gBAAgB;AACtD,QAAI,SAAU,QAAO;AACrB,UAAM,QAAQ,OAAO,WAAW;AAChC,iBAAa,QAAQ,kBAAkB,KAAK;AAC5C,WAAO;AAAA,EACT,QAAQ;AACN,WAAO,OAAO,WAAW;AAAA,EAC3B;AACF;AAEA,IAAI,YAA6B;AAG1B,SAAS,UAAU,MAAiC;AACzD,MAAI,CAAC,UAAW,aAAY,IAAI,SAAS,IAAI;AAC7C,SAAO;AACT;",
6
+ "names": []
7
+ }
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/server.ts
21
+ var server_exports = {};
22
+ __export(server_exports, {
23
+ DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
24
+ Holdy: () => Holdy,
25
+ HoldyCore: () => HoldyCore
26
+ });
27
+ module.exports = __toCommonJS(server_exports);
28
+
29
+ // src/core.ts
30
+ var DEFAULT_BASE_URL = "https://holdy.chat/api/v1";
31
+ var DEFAULT_FLUSH_AT = 20;
32
+ var DEFAULT_FLUSH_INTERVAL_MS = 5e3;
33
+ var MAX_QUEUE = 1e3;
34
+ var BULK_LIMIT = 500;
35
+ var HoldyCore = class {
36
+ apiKey;
37
+ baseUrl;
38
+ flushAt;
39
+ debug;
40
+ source;
41
+ queue = [];
42
+ timer = null;
43
+ flushing = null;
44
+ userId = null;
45
+ anonymousId = null;
46
+ accountKey = null;
47
+ constructor(source, opts) {
48
+ this.source = source;
49
+ this.apiKey = opts.apiKey;
50
+ this.accountKey = opts.accountKey ?? null;
51
+ this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
52
+ this.flushAt = Math.max(1, opts.flushAt ?? DEFAULT_FLUSH_AT);
53
+ this.debug = opts.debug ?? false;
54
+ const intervalMs = opts.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
55
+ if (intervalMs > 0) {
56
+ this.timer = setInterval(() => void this.flush(), intervalMs);
57
+ if (typeof this.timer.unref === "function") {
58
+ ;
59
+ this.timer.unref();
60
+ }
61
+ }
62
+ }
63
+ track(name, properties, overrides) {
64
+ if (!name || typeof name !== "string") return;
65
+ const userId = overrides?.userId ?? this.userId ?? void 0;
66
+ const accountKey = overrides?.accountKey ?? this.accountKey ?? void 0;
67
+ const occurredAt = overrides?.occurredAt ? new Date(overrides.occurredAt).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
68
+ const context = {
69
+ ...this.environmentContext(),
70
+ ...overrides?.context ?? {}
71
+ };
72
+ if (accountKey) context.account_external_id = accountKey;
73
+ const event = {
74
+ event: name,
75
+ occurred_at: occurredAt,
76
+ source: this.source,
77
+ ...userId ? { user_id: userId } : {},
78
+ ...this.anonymousId ? { anonymous_id: this.anonymousId } : {},
79
+ properties: properties ?? {},
80
+ ...Object.keys(context).length > 0 ? { context } : {}
81
+ };
82
+ this.queue.push(event);
83
+ if (this.queue.length > MAX_QUEUE) this.queue.splice(0, this.queue.length - MAX_QUEUE);
84
+ if (this.queue.length >= this.flushAt) void this.flush();
85
+ }
86
+ identify(userId, traits) {
87
+ if (!userId || typeof userId !== "string") return;
88
+ this.userId = userId;
89
+ this.track("$identify", traits ?? {});
90
+ }
91
+ group(accountKey, traits) {
92
+ if (!accountKey || typeof accountKey !== "string") return;
93
+ this.accountKey = accountKey;
94
+ this.track("$group", traits ?? {});
95
+ }
96
+ /** Drena a fila. Nunca lança — falha re-enfileira e loga em debug. */
97
+ async flush() {
98
+ if (this.flushing) return this.flushing;
99
+ if (this.queue.length === 0) return;
100
+ this.flushing = this.drain().finally(() => {
101
+ this.flushing = null;
102
+ });
103
+ return this.flushing;
104
+ }
105
+ /** Flush final + para o timer. Chamar em shutdown do server / unload do web. */
106
+ async shutdown() {
107
+ if (this.timer) {
108
+ clearInterval(this.timer);
109
+ this.timer = null;
110
+ }
111
+ await this.flush();
112
+ }
113
+ async drain() {
114
+ while (this.queue.length > 0) {
115
+ const batch = this.queue.splice(0, BULK_LIMIT);
116
+ const ok = await this.send(batch);
117
+ if (!ok) {
118
+ this.queue.unshift(...batch);
119
+ return;
120
+ }
121
+ }
122
+ }
123
+ async send(events) {
124
+ try {
125
+ const res = await fetch(`${this.baseUrl}/events/bulk`, {
126
+ method: "POST",
127
+ headers: {
128
+ Authorization: `Bearer ${this.apiKey}`,
129
+ "Content-Type": "application/json"
130
+ },
131
+ body: JSON.stringify({ events }),
132
+ ...this.source === "sdk_web" ? { keepalive: true } : {}
133
+ });
134
+ if (!res.ok) {
135
+ if (res.status >= 400 && res.status < 500) {
136
+ this.log(`bulk rejected ${res.status} \xB7 batch dropped`);
137
+ return true;
138
+ }
139
+ this.log(`bulk failed ${res.status} \xB7 will retry`);
140
+ return false;
141
+ }
142
+ return true;
143
+ } catch (err) {
144
+ this.log(`bulk network error: ${err.message} \xB7 will retry`);
145
+ return false;
146
+ }
147
+ }
148
+ log(msg) {
149
+ if (this.debug) console.warn(`[holdy] ${msg}`);
150
+ }
151
+ /**
152
+ * Ambiente que o SDK observa sozinho (url, referrer, browser, utm…).
153
+ * Vazio no core; `HoldyWeb` preenche. O server não infere ambiente — não
154
+ * existe "página atual" num worker.
155
+ */
156
+ environmentContext() {
157
+ return {};
158
+ }
159
+ /** Interno · exposto pra testes. */
160
+ get pending() {
161
+ return this.queue.length;
162
+ }
163
+ };
164
+
165
+ // src/server.ts
166
+ var Holdy = class extends HoldyCore {
167
+ constructor(opts) {
168
+ super("sdk_server", opts);
169
+ }
170
+ };
171
+ // Annotate the CommonJS export names for ESM import in node:
172
+ 0 && (module.exports = {
173
+ DEFAULT_BASE_URL,
174
+ Holdy,
175
+ HoldyCore
176
+ });
177
+ //# sourceMappingURL=server.cjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/server.ts", "../src/core.ts"],
4
+ "sourcesContent": ["/**\n * @holdyapp/sdk (server) \u00B7 Node/Edge (source: sdk_server).\n *\n * Stateless por default: user_id/accountKey v\u00E3o por chamada em overrides.\n * identify()/group() de sess\u00E3o existem mas em server multi-tenant prefira\n * overrides \u2014 estado compartilhado entre requests vaza identidade.\n */\n\nimport { HoldyCore } from './core'\nimport type { HoldyOptions } from './types'\n\nexport class Holdy extends HoldyCore {\n constructor(opts: HoldyOptions) {\n super('sdk_server', opts)\n }\n}\n\nexport { HoldyCore, DEFAULT_BASE_URL } from './core'\nexport type { HoldyOptions, Properties, TrackOverrides, CanonicalEvent } from './types'\n", "/**\n * Core do @holdyapp/sdk \u00B7 fila + flush pra POST /api/v1/events/bulk.\n *\n * Retry de batch inteiro \u00E9 seguro (partial accept + dedupe server-side).\n * Fila \u00E9 bounded: acima de MAX_QUEUE dropa os mais antigos (telemetria nunca\n * derruba o app do cliente \u2014 nenhum m\u00E9todo p\u00FAblico lan\u00E7a).\n */\n\nimport type { CanonicalEvent, HoldyOptions, Properties, TrackOverrides } from './types'\n\nexport const DEFAULT_BASE_URL = 'https://holdy.chat/api/v1'\nconst DEFAULT_FLUSH_AT = 20\nconst DEFAULT_FLUSH_INTERVAL_MS = 5000\nconst MAX_QUEUE = 1000\nconst BULK_LIMIT = 500\n\nexport class HoldyCore {\n private readonly apiKey: string\n private readonly baseUrl: string\n private readonly flushAt: number\n private readonly debug: boolean\n private readonly source: 'sdk_web' | 'sdk_server'\n\n private queue: CanonicalEvent[] = []\n private timer: ReturnType<typeof setInterval> | null = null\n private flushing: Promise<void> | null = null\n\n protected userId: string | null = null\n protected anonymousId: string | null = null\n protected accountKey: string | null = null\n\n constructor(source: 'sdk_web' | 'sdk_server', opts: HoldyOptions) {\n this.source = source\n this.apiKey = opts.apiKey\n this.accountKey = opts.accountKey ?? null\n this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, '')\n this.flushAt = Math.max(1, opts.flushAt ?? DEFAULT_FLUSH_AT)\n this.debug = opts.debug ?? false\n\n const intervalMs = opts.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS\n if (intervalMs > 0) {\n this.timer = setInterval(() => void this.flush(), intervalMs)\n // Node: n\u00E3o segura o processo vivo s\u00F3 pelo timer do SDK.\n if (typeof (this.timer as { unref?: () => void }).unref === 'function') {\n ;(this.timer as unknown as { unref: () => void }).unref()\n }\n }\n }\n\n track(name: string, properties?: Properties, overrides?: TrackOverrides): void {\n if (!name || typeof name !== 'string') return\n\n const userId = overrides?.userId ?? this.userId ?? undefined\n const accountKey = overrides?.accountKey ?? this.accountKey ?? undefined\n const occurredAt = overrides?.occurredAt\n ? new Date(overrides.occurredAt).toISOString()\n : new Date().toISOString()\n\n // Ordem: ambiente observado pelo SDK \u2192 context da chamada \u2192 carrier de\n // account. O que o caller passou vence o inferido; o carrier vence tudo,\n // porque \u00E9 o que a Data layer usa pra resolver a conta.\n const context: Properties = {\n ...this.environmentContext(),\n ...(overrides?.context ?? {}),\n }\n if (accountKey) context.account_external_id = accountKey\n\n const event: CanonicalEvent = {\n event: name,\n occurred_at: occurredAt,\n source: this.source,\n ...(userId ? { user_id: userId } : {}),\n ...(this.anonymousId ? { anonymous_id: this.anonymousId } : {}),\n properties: properties ?? {},\n ...(Object.keys(context).length > 0 ? { context } : {}),\n }\n\n this.queue.push(event)\n if (this.queue.length > MAX_QUEUE) this.queue.splice(0, this.queue.length - MAX_QUEUE)\n if (this.queue.length >= this.flushAt) void this.flush()\n }\n\n identify(userId: string, traits?: Properties): void {\n if (!userId || typeof userId !== 'string') return\n this.userId = userId\n this.track('$identify', traits ?? {})\n }\n\n group(accountKey: string, traits?: Properties): void {\n if (!accountKey || typeof accountKey !== 'string') return\n this.accountKey = accountKey\n this.track('$group', traits ?? {})\n }\n\n /** Drena a fila. Nunca lan\u00E7a \u2014 falha re-enfileira e loga em debug. */\n async flush(): Promise<void> {\n // Serializa flushes concorrentes (timer + flushAt + manual).\n if (this.flushing) return this.flushing\n if (this.queue.length === 0) return\n\n this.flushing = this.drain().finally(() => {\n this.flushing = null\n })\n return this.flushing\n }\n\n /** Flush final + para o timer. Chamar em shutdown do server / unload do web. */\n async shutdown(): Promise<void> {\n if (this.timer) {\n clearInterval(this.timer)\n this.timer = null\n }\n await this.flush()\n }\n\n private async drain(): Promise<void> {\n while (this.queue.length > 0) {\n const batch = this.queue.splice(0, BULK_LIMIT)\n const ok = await this.send(batch)\n if (!ok) {\n // Devolve pra frente da fila e para \u2014 pr\u00F3ximo flush re-tenta (dedupe-safe).\n this.queue.unshift(...batch)\n return\n }\n }\n }\n\n private async send(events: CanonicalEvent[]): Promise<boolean> {\n try {\n const res = await fetch(`${this.baseUrl}/events/bulk`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ events }),\n ...(this.source === 'sdk_web' ? { keepalive: true } : {}),\n })\n if (!res.ok) {\n // 4xx: batch estruturalmente inv\u00E1lido n\u00E3o re-tenta (evita loop infinito).\n if (res.status >= 400 && res.status < 500) {\n this.log(`bulk rejected ${res.status} \u00B7 batch dropped`)\n return true\n }\n this.log(`bulk failed ${res.status} \u00B7 will retry`)\n return false\n }\n return true\n } catch (err) {\n this.log(`bulk network error: ${(err as Error).message} \u00B7 will retry`)\n return false\n }\n }\n\n private log(msg: string): void {\n if (this.debug) console.warn(`[holdy] ${msg}`)\n }\n\n /**\n * Ambiente que o SDK observa sozinho (url, referrer, browser, utm\u2026).\n * Vazio no core; `HoldyWeb` preenche. O server n\u00E3o infere ambiente \u2014 n\u00E3o\n * existe \"p\u00E1gina atual\" num worker.\n */\n protected environmentContext(): Properties {\n return {}\n }\n\n /** Interno \u00B7 exposto pra testes. */\n get pending(): number {\n return this.queue.length\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }