@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
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @holdyapp/sdk (server) · Node/Edge (source: sdk_server).
3
+ *
4
+ * Stateless por default: user_id/accountKey vão por chamada em overrides.
5
+ * identify()/group() de sessão existem mas em server multi-tenant prefira
6
+ * overrides — estado compartilhado entre requests vaza identidade.
7
+ */
8
+ import { HoldyCore } from './core';
9
+ import type { HoldyOptions } from './types';
10
+ export declare class Holdy extends HoldyCore {
11
+ constructor(opts: HoldyOptions);
12
+ }
13
+ export { HoldyCore, DEFAULT_BASE_URL } from './core';
14
+ export type { HoldyOptions, Properties, TrackOverrides, CanonicalEvent } from './types';
15
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAA;AAClC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAE3C,qBAAa,KAAM,SAAQ,SAAS;gBACtB,IAAI,EAAE,YAAY;CAG/B;AAED,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAA;AACpD,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA"}
@@ -0,0 +1,148 @@
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
+ export {
144
+ DEFAULT_BASE_URL,
145
+ Holdy,
146
+ HoldyCore
147
+ };
148
+ //# sourceMappingURL=server.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/core.ts", "../src/server.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"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
package/dist/types.d.ts CHANGED
@@ -1,114 +1,46 @@
1
- export type CanonicalPrimitive = 'workflow.started' | 'workflow.progressed' | 'workflow.blocked' | 'workflow.completed' | 'workflow.abandoned' | 'outcome.progressed' | 'outcome.completed' | 'outcome.regressed' | 'friction.detected' | 'collaboration.invited' | 'collaboration.joined' | 'collaboration.deepened' | 'collaboration.weakened' | 'collaboration.champion_lost' | 'support.escalated' | 'support.sentiment_negative' | 'support.theme_detected' | 'expansion.intent_detected' | 'expansion.ceiling_hit' | 'memory.append' | 'session.activity';
2
- export type DecisionType = 'intervention' | 'briefing' | 'cs_task' | 'track_journey' | 'no_action';
3
- export type InterventionKind = 'tooltip' | 'banner' | 'modal' | 'checklist' | 'spotlight' | 'cta';
4
- export interface ChecklistItem {
5
- /** ID estável · persistência local + dedup. */
6
- id: string;
7
- label: string;
8
- /** Opcional · link aberto ao clicar. Sem href, item só marca completed. */
9
- href?: string;
10
- /** Workflow ou outcome primitive que marca esse item como completed quando emitido. */
11
- completed_event?: string;
12
- /** Pré-marcado · server pode indicar que cliente já fez essa etapa. */
13
- completed?: boolean;
14
- }
15
- export interface InterventionPayload {
16
- kind: InterventionKind;
17
- text: string;
18
- /** Título opcional · usado em modal/checklist headers. */
19
- title?: string;
20
- cta_text?: string;
21
- cta_href?: string;
22
- anchor_selector?: string;
23
- /** Items pro kind=checklist. */
24
- items?: ChecklistItem[];
25
- }
26
- export interface DecisionResult {
27
- decision_id: string;
28
- decision_type: DecisionType;
29
- decision_payload: InterventionPayload | Record<string, unknown>;
30
- rule_key: string;
31
- confidence: number;
32
- reason: string;
33
- decided_at: string;
34
- }
35
- export type OutcomeValue = 'executed' | 'dismissed' | 'converted' | 'no_show' | 'failed';
36
- export interface RuntimeConfig {
37
- api_version: string;
38
- taxonomy_version: string;
39
- organization_id: string;
40
- thresholds: Record<string, number | string>;
41
- features: {
42
- auto_attribution: boolean;
43
- interventions_enabled: boolean;
44
- consent_required: boolean;
45
- };
46
- theme?: ThemeConfig;
47
- }
48
- export interface ThemeConfig {
49
- primary_color: string;
50
- font_family: string;
51
- border_radius_px: number;
52
- position_offset_px: number;
53
- }
54
- export declare const DEFAULT_THEME: ThemeConfig;
55
- export interface HoldyConfig {
56
- apiKey: string;
57
- apiUrl?: string;
58
- accountId?: string;
59
- userId?: string;
60
- sessionId?: string;
61
- consumer?: 'ui' | 'csm' | 'agent';
62
- debug?: boolean;
63
- /** Disables render runtime · SDK só captura telemetry. */
64
- renderInterventions?: boolean;
65
- /**
66
- * Stage 28 · auto-capture de page views (initial_load + route_change +
67
- * visibility_change). Quando true, SDK monkeypatcha history.pushState/
68
- * replaceState + escuta popstate/visibilitychange e emite session.activity
69
- * sem código manual. Default false pra back-compat.
70
- */
71
- autoCapture?: boolean;
72
- }
73
1
  /**
74
- * Rich account metadata enviado pra /v1/accounts/upsert.
75
- * Backend usa COALESCE — não sobrescreve campos preenchidos no wizard manual.
2
+ * Tipos públicos do @holdyapp/sdk (entry-contract.md §2 + §6).
76
3
  *
77
- * `external_id` é o identificador do cliente (string opaca · ex "acct_abc123").
78
- * Backend resolve via customer_identities(source='sdk') → accounts.id.
4
+ * Envelope idêntico ao CanonicalEventInputSchema do servidor — o SDK nunca
5
+ * inventa campo. account_id (uuid Holdy) não existe aqui: a chave do cliente
6
+ * viaja em context.account_external_id e a Data layer resolve (regra 4).
79
7
  */
80
- export interface AccountIdentity {
81
- external_id: string;
82
- name?: string;
83
- logo_url?: string;
84
- domain?: string;
85
- plan?: string;
86
- mrr?: number;
87
- contract_start?: string;
88
- contract_end?: string;
89
- lifecycle_stage?: 'onboarding' | 'adoption' | 'expansion' | 'renewal' | 'churned';
90
- traits?: Record<string, string | number | boolean | null>;
91
- }
92
- export interface UserIdentity {
93
- external_id: string;
94
- email?: string;
95
- name?: string;
96
- job_title?: string;
97
- role?: 'decision_maker' | 'champion' | 'admin' | 'user' | 'unknown';
8
+ export type Properties = Record<string, unknown>;
9
+ export interface CanonicalEvent {
10
+ event: string;
11
+ occurred_at: string;
12
+ source: 'sdk_web' | 'sdk_server';
13
+ user_id?: string;
14
+ anonymous_id?: string;
15
+ properties: Properties;
16
+ context?: Properties;
17
+ }
18
+ export interface HoldyOptions {
19
+ /** API key `hol_{env}_...` com scope events:write. */
20
+ apiKey: string;
21
+ /** Override · default https://holdy.chat/api/v1 */
22
+ baseUrl?: string;
23
+ /** Flush automático quando a fila atinge este tamanho · default 20. */
24
+ flushAt?: number;
25
+ /** Flush automático a cada N ms · default 5000. 0 desliga o timer. */
26
+ flushIntervalMs?: number;
27
+ /**
28
+ * Chave da account do cliente (`context.account_external_id`). Define o
29
+ * vínculo sem emitir evento — `group()` continua sendo a chamada pra
30
+ * declarar a account COM traits.
31
+ */
32
+ accountKey?: string;
33
+ /** Logs de debug no console. */
34
+ debug?: boolean;
98
35
  }
99
- export interface IdentifyOptions {
100
- /** ID interno Holdy (UUID) · opcional · usado pra retro-compat. */
101
- accountId?: string;
36
+ export interface TrackOverrides {
37
+ /** Server-side: end-user do evento (stateless por chamada). */
102
38
  userId?: string;
103
- sessionId?: string;
104
- /** Metadata rica · gera best-effort upsert em /v1/accounts/upsert. */
105
- account?: AccountIdentity;
106
- user?: UserIdentity;
107
- }
108
- export interface TrackOptions {
109
- /** Pula chamada implícita ao decisions/evaluate. */
110
- skipEval?: boolean;
111
- /** Workflow corrente · override do estado interno. */
112
- workflow?: string;
113
- }
114
- export type DecisionListener = (decision: DecisionResult) => void;
39
+ /** Server-side: chave de account do cliente (context.account_external_id). */
40
+ accountKey?: string;
41
+ /** Timestamp do evento · default agora. */
42
+ occurredAt?: Date | string;
43
+ /** Metadata adicional pro context. */
44
+ context?: Properties;
45
+ }
46
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAEhD,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,SAAS,GAAG,YAAY,CAAA;IAChC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,UAAU,EAAE,UAAU,CAAA;IACtB,OAAO,CAAC,EAAE,UAAU,CAAA;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,sDAAsD;IACtD,MAAM,EAAE,MAAM,CAAA;IACd,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,sEAAsE;IACtE,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,gCAAgC;IAChC,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,+DAA+D;IAC/D,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,8EAA8E;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,2CAA2C;IAC3C,UAAU,CAAC,EAAE,IAAI,GAAG,MAAM,CAAA;IAC1B,sCAAsC;IACtC,OAAO,CAAC,EAAE,UAAU,CAAA;CACrB"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Contexto de ambiente do browser · @holdyapp/sdk/web.
3
+ *
4
+ * Vai em `context` de todo evento, nunca em `properties`: properties é o
5
+ * vocabulário do produto do cliente (`workflow_key`, `plan`), context é o que o
6
+ * SDK observou sozinho. A Data layer persiste os dois (facts.context, 00247) e
7
+ * a leitura de dimensão consulta os dois.
8
+ *
9
+ * Três decisões que definem se esse dado é útil ou lixo:
10
+ * · `url` sai sem query e sem hash. Href cru tem uma variação por sessão, e
11
+ * dimensão com cardinalidade quase-única é descartada na leitura — a
12
+ * página vira ruído exatamente por excesso de precisão.
13
+ * · utm é capturado na primeira página e carregado pelo resto da sessão.
14
+ * Atribuição que só existe no primeiro evento não responde nada.
15
+ * · referrer só quando é externo. Navegação interna preenchendo "Origem do
16
+ * tráfego" com o próprio domínio esconde a origem real.
17
+ *
18
+ * Tudo aqui é best-effort: browser em modo privativo, SSR e webview sem
19
+ * storage precisam funcionar. Nenhuma função lança.
20
+ */
21
+ export type WebContext = Record<string, string>;
22
+ /** `https://app.x.com/a/b?q=1#z` → `https://app.x.com/a/b`. */
23
+ export declare function stripUrl(href: string): string | null;
24
+ /** Parâmetros de campanha presentes na URL, já em lowercase. */
25
+ export declare function parseUtm(href: string): WebContext;
26
+ interface UaParts {
27
+ browser?: string;
28
+ os?: string;
29
+ device?: string;
30
+ }
31
+ /**
32
+ * Fatia mínima do UA: navegador, sistema e classe de dispositivo.
33
+ *
34
+ * Não é biblioteca de UA parsing e não deve virar uma — o valor está em
35
+ * separar mobile de desktop e Safari de Chrome, não em cravar a versão.
36
+ */
37
+ export declare function parseUserAgent(ua: string): UaParts;
38
+ /** ID da sessão de browser · vive enquanto a aba viver. */
39
+ export declare function resolveSessionId(): string;
40
+ /**
41
+ * Atribuição da sessão: utm da primeira página + referrer externo de entrada.
42
+ * Gravada uma vez e repetida em todo evento da sessão.
43
+ */
44
+ export declare function resolveAttribution(href: string, referrer: string, currentHost: string): WebContext;
45
+ /**
46
+ * Contexto completo do evento atual. Fora do browser devolve `{}` — o SDK
47
+ * server tem o próprio caminho e não deve inventar ambiente.
48
+ */
49
+ export declare function buildWebContext(): WebContext;
50
+ export {};
51
+ //# sourceMappingURL=web-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-context.d.ts","sourceRoot":"","sources":["../src/web-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAe/C,+DAA+D;AAC/D,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOpD;AAED,gEAAgE;AAChE,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAYjD;AAID,UAAU,OAAO;IACf,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CA6ClD;AA8BD,2DAA2D;AAC3D,wBAAgB,gBAAgB,IAAI,MAAM,CAMzC;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,UAAU,CAsBlG;AAYD;;;GAGG;AACH,wBAAgB,eAAe,IAAI,UAAU,CAyB5C"}
package/dist/web.d.ts ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @holdyapp/sdk/web · browser (source: sdk_web).
3
+ *
4
+ * anonymous_id gerado e persistido em localStorage (holdy_anon_id) — enviado em
5
+ * todo event até identify() promover pra user_id (identity graph faz o merge
6
+ * downstream, nunca o SDK). Flush no pagehide via fetch keepalive.
7
+ *
8
+ * Todo evento carrega o contexto de ambiente (url, path, referrer externo,
9
+ * utm da sessão, browser, os, device, locale, session_id) em `context` —
10
+ * separado das properties do produto. Pageview é automático por default:
11
+ * carregamento inicial e cada mudança de rota, inclusive em SPA.
12
+ */
13
+ import { HoldyCore } from './core';
14
+ import type { HoldyOptions, Properties } from './types';
15
+ export interface HoldyWebOptions extends HoldyOptions {
16
+ /**
17
+ * Emitir `$pageview` sozinho no load e a cada mudança de rota · default true.
18
+ * Desligue só se o produto já chama `page()` na mão em cada tela.
19
+ */
20
+ autoCapture?: boolean;
21
+ }
22
+ export declare class HoldyWeb extends HoldyCore {
23
+ private teardownAutoCapture;
24
+ constructor(opts: HoldyWebOptions);
25
+ /**
26
+ * Pageview. O `$` marca evento emitido pelo SDK, não pelo produto (mesma
27
+ * convenção de `$identify`/`$group`); a canonicalização downstream remove o
28
+ * sigilo, então `$pageview` e `pageview` são o mesmo evento na leitura.
29
+ * url/path/referrer/utm já vêm no context de todo evento — aqui só entra o
30
+ * que é da página como conteúdo (nome e título).
31
+ */
32
+ page(name?: string, properties?: Properties): void;
33
+ reset(): void;
34
+ /** Flush final + desinstala o auto-capture (devolve `history` ao original). */
35
+ shutdown(): Promise<void>;
36
+ protected environmentContext(): Properties;
37
+ }
38
+ /** Init idempotente — segunda chamada retorna a instância existente. */
39
+ export declare function initHoldy(opts: HoldyWebOptions): HoldyWeb;
40
+ export type { HoldyOptions, Properties };
41
+ //# sourceMappingURL=web.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web.d.ts","sourceRoot":"","sources":["../src/web.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAA;AAGlC,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAIvD,MAAM,WAAW,eAAgB,SAAQ,YAAY;IACnD;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB;AAED,qBAAa,QAAS,SAAQ,SAAS;IACrC,OAAO,CAAC,mBAAmB,CAA4B;gBAE3C,IAAI,EAAE,eAAe;IAkBjC;;;;;;OAMG;IACH,IAAI,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,UAAU,GAAG,IAAI;IAQlD,KAAK,IAAI,IAAI;IAWb,+EAA+E;IAChE,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;cAMrB,kBAAkB,IAAI,UAAU;CAGpD;AAgBD,wEAAwE;AACxE,wBAAgB,SAAS,CAAC,IAAI,EAAE,eAAe,GAAG,QAAQ,CAGzD;AAED,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,CAAA"}