@holdyapp/sdk 0.2.0 → 1.0.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.
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.js +422 -0
  11. package/dist/index.cjs.js.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.js +171 -0
  17. package/dist/server.cjs.js.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 +25 -21
  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/src/index.ts CHANGED
@@ -1,427 +1,4 @@
1
- import type {
2
- AccountIdentity,
3
- CanonicalPrimitive,
4
- DecisionListener,
5
- DecisionResult,
6
- HoldyConfig,
7
- IdentifyOptions,
8
- InterventionPayload,
9
- OutcomeValue,
10
- RuntimeConfig,
11
- ThemeConfig,
12
- TrackOptions,
13
- UserIdentity,
14
- } from './types'
15
- import { Transport } from './transport'
16
- import { OutcomeTracker } from './outcome'
17
- import { InterventionStore } from './store'
18
- import { resolveAnchor } from './anchor'
19
- import { installAutoCapture } from './auto-capture'
20
- import { appendToRoot, ensureRoot } from './render/root'
21
- import { renderTooltip } from './render/kinds/tooltip'
22
- import { renderBanner } from './render/kinds/banner'
23
- import { renderCta } from './render/kinds/cta'
24
- import { renderModal } from './render/kinds/modal'
25
- import { renderChecklist } from './render/kinds/checklist'
26
- import { renderSpotlight } from './render/kinds/spotlight'
27
- import type { RenderControl, RenderHandlers } from './render/kinds/shared'
28
-
29
- const RING_BUFFER_LIMIT = 20
30
- const CONFIG_CACHE_KEY = '__holdy_runtime_config_v1'
31
- const CONFIG_CACHE_TTL_MS = 60 * 60 * 1000
32
- const EVAL_THROTTLE_MS = 1500
33
-
34
- interface RingEntry {
35
- primitive: CanonicalPrimitive
36
- payload: Record<string, unknown>
37
- occurred_at: string
38
- }
39
-
40
- class HoldySdk {
41
- private config: HoldyConfig | null = null
42
- private transport: Transport | null = null
43
- private outcomeTracker: OutcomeTracker | null = null
44
- private store: InterventionStore = new InterventionStore()
45
- private runtime: RuntimeConfig | null = null
46
- private activeWorkflow: string | null = null
47
- private sentiment: 'positivo' | 'neutro' | 'negativo' | 'urgente' | null = null
48
- private readonly ring: RingEntry[] = []
49
- private listeners: DecisionListener[] = []
50
- private lastEvalAt = 0
51
- private initialized = false
52
- private autoCaptureCleanup: (() => void) | null = null
53
-
54
- async init(config: HoldyConfig): Promise<void> {
55
- if (this.initialized) {
56
- this.log('init called twice · ignoring')
57
- return
58
- }
59
- if (!config.apiKey) {
60
- throw new Error('[holdy] apiKey is required')
61
- }
62
- this.config = config
63
- this.transport = new Transport(config)
64
- this.outcomeTracker = new OutcomeTracker(this.transport)
65
- this.initialized = true
66
-
67
- const cached = this.readCachedConfig()
68
- if (cached) this.runtime = cached
69
-
70
- const fresh = await this.transport.fetchRuntimeConfig()
71
- if (fresh) {
72
- this.runtime = fresh
73
- this.writeCachedConfig(fresh)
74
- }
75
-
76
- if (this.shouldRender()) {
77
- ensureRoot(this.themeFromRuntime())
78
- }
79
-
80
- // Stage 28 G_auto · auto-capture de page views.
81
- // Habilita só quando customer pediu explicitamente · default off pra
82
- // back-compat. Ring buffer + transport recebem session.activity igual
83
- // se viesse de track() manual · decision engine consome no contexto.
84
- if (config.autoCapture) {
85
- this.autoCaptureCleanup = installAutoCapture((path, title, referrer, kind) => {
86
- void this.track('session.activity', {
87
- kind,
88
- path,
89
- ...(title ? { title } : {}),
90
- ...(referrer ? { referrer } : {}),
91
- }, { skipEval: kind !== 'route_change' })
92
- })
93
- }
94
- }
95
-
96
- /**
97
- * Identifica conta + usuário corrente.
98
- *
99
- * - `accountId/userId/sessionId` hidratam config local pra evaluate/track.
100
- * - `account/user` (metadata rica) disparam upsert best-effort em
101
- * /v1/accounts/upsert. Backend COALESCE → nunca sobrescreve campos do
102
- * wizard manual. Em sucesso, account_id Holdy retornado hidrata config
103
- * local automaticamente (cliente não precisa saber UUID).
104
- */
105
- identify(opts: IdentifyOptions): void {
106
- if (!this.config) return
107
- if (opts.accountId !== undefined) this.config.accountId = opts.accountId
108
- if (opts.userId !== undefined) this.config.userId = opts.userId
109
- if (opts.sessionId !== undefined) this.config.sessionId = opts.sessionId
110
-
111
- if (opts.account || opts.user) {
112
- void this.upsertIdentityRich(opts.account, opts.user)
113
- }
114
- }
115
-
116
- private async upsertIdentityRich(
117
- account: AccountIdentity | undefined,
118
- user: UserIdentity | undefined,
119
- ): Promise<void> {
120
- if (!this.transport || !account) return
121
- const result = await this.transport.upsertAccount({ account, user })
122
- if (!result || !this.config) return
123
- if (!this.config.accountId) this.config.accountId = result.account_id
124
- if (!this.config.userId && result.account_user_id) {
125
- this.config.userId = result.account_user_id
126
- }
127
- }
128
-
129
- setWorkflow(workflowKey: string | null | undefined): void {
130
- this.activeWorkflow = workflowKey ?? null
131
- }
132
-
133
- setSentiment(sentiment: 'positivo' | 'neutro' | 'negativo' | 'urgente' | null): void {
134
- this.sentiment = sentiment
135
- }
136
-
137
- async track(
138
- primitive: CanonicalPrimitive,
139
- payload: Record<string, unknown> = {},
140
- opts: TrackOptions = {},
141
- ): Promise<void> {
142
- if (!this.config || !this.transport) {
143
- this.log('track called before init')
144
- return
145
- }
146
- const occurred_at = new Date().toISOString()
147
- const entry: RingEntry = { primitive, payload, occurred_at }
148
- this.pushRing(entry)
149
-
150
- // workflow tracking auto-sync
151
- if (primitive === 'workflow.started' && typeof payload.workflow === 'string') {
152
- this.activeWorkflow = payload.workflow
153
- }
154
- if (
155
- (primitive === 'workflow.completed' || primitive === 'workflow.abandoned') &&
156
- typeof payload.workflow === 'string' &&
157
- this.activeWorkflow === payload.workflow
158
- ) {
159
- this.activeWorkflow = null
160
- }
161
- if (opts.workflow !== undefined) this.activeWorkflow = opts.workflow
162
-
163
- // ingest fire-and-forget
164
- this.transport.ingestBatch([
165
- {
166
- primitive,
167
- occurred_at,
168
- account_id: this.config.accountId,
169
- user_id: this.config.userId,
170
- session_id: this.config.sessionId,
171
- payload,
172
- },
173
- ])
174
-
175
- if (opts.skipEval) return
176
- if (!this.shouldTriggerEval(primitive)) return
177
-
178
- await this.evaluateInternal()
179
- }
180
-
181
- async evaluate(): Promise<DecisionResult | null> {
182
- return this.evaluateInternal(true)
183
- }
184
-
185
- onDecision(listener: DecisionListener): () => void {
186
- this.listeners.push(listener)
187
- return () => {
188
- this.listeners = this.listeners.filter((l) => l !== listener)
189
- }
190
- }
191
-
192
- reportOutcome(
193
- decisionId: string,
194
- outcome: OutcomeValue,
195
- metadata?: Record<string, unknown>,
196
- ): void {
197
- this.outcomeTracker?.dispatch(decisionId, outcome, metadata)
198
- }
199
-
200
- destroy(): void {
201
- this.transport?.destroy()
202
- if (this.autoCaptureCleanup) {
203
- try { this.autoCaptureCleanup() } catch { /* ignore */ }
204
- this.autoCaptureCleanup = null
205
- }
206
- this.config = null
207
- this.transport = null
208
- this.outcomeTracker = null
209
- this.runtime = null
210
- this.activeWorkflow = null
211
- this.listeners = []
212
- this.ring.length = 0
213
- this.initialized = false
214
- }
215
-
216
- /* ───────── internals ───────── */
217
-
218
- private async evaluateInternal(force = false): Promise<DecisionResult | null> {
219
- if (!this.transport || !this.config?.accountId) {
220
- this.log('evaluate skipped · no accountId set · call identify first')
221
- return null
222
- }
223
- const now = Date.now()
224
- if (!force && now - this.lastEvalAt < EVAL_THROTTLE_MS) {
225
- return null
226
- }
227
- this.lastEvalAt = now
228
-
229
- const evalInput: {
230
- account_id: string
231
- current_workflow?: string
232
- recent_primitives: Array<{
233
- primitive: CanonicalPrimitive
234
- payload: Record<string, unknown>
235
- occurred_at: string
236
- }>
237
- consumer: 'ui' | 'csm' | 'agent'
238
- recent_sentiment?: 'positivo' | 'neutro' | 'negativo' | 'urgente'
239
- } = {
240
- account_id: this.config.accountId,
241
- recent_primitives: [...this.ring],
242
- consumer: this.config.consumer ?? 'ui',
243
- }
244
- if (this.activeWorkflow) evalInput.current_workflow = this.activeWorkflow
245
- if (this.sentiment) evalInput.recent_sentiment = this.sentiment
246
-
247
- const result = await this.transport.evaluate(evalInput)
248
- if (!result) return null
249
-
250
- this.listeners.forEach((l) => {
251
- try {
252
- l(result)
253
- } catch (e) {
254
- this.log('listener error', e)
255
- }
256
- })
257
-
258
- if (result.decision_type === 'intervention' && this.shouldRender()) {
259
- void this.renderDecision(result)
260
- }
261
-
262
- return result
263
- }
264
-
265
- private async renderDecision(decision: DecisionResult): Promise<void> {
266
- const payload = decision.decision_payload as InterventionPayload
267
- if (!payload || typeof payload.kind !== 'string' || typeof payload.text !== 'string') {
268
- this.outcomeTracker?.dispatch(decision.decision_id, 'failed', { reason: 'invalid_payload' })
269
- return
270
- }
271
-
272
- const handlers: RenderHandlers = {
273
- onExecuted: (meta) => this.outcomeTracker?.dispatch(decision.decision_id, 'executed', meta),
274
- onDismissed: (meta) => this.outcomeTracker?.dispatch(decision.decision_id, 'dismissed', meta),
275
- onFailed: (meta) => this.outcomeTracker?.dispatch(decision.decision_id, 'failed', meta),
276
- }
277
-
278
- let control: RenderControl | null = null
279
- try {
280
- switch (payload.kind) {
281
- case 'tooltip': {
282
- const anchor = await resolveAnchor(payload.anchor_selector)
283
- if (!anchor) {
284
- this.outcomeTracker?.dispatch(decision.decision_id, 'no_show', {
285
- reason: 'anchor_unresolved',
286
- selector: payload.anchor_selector ?? null,
287
- })
288
- return
289
- }
290
- control = renderTooltip(payload, anchor, handlers)
291
- break
292
- }
293
- case 'banner':
294
- control = renderBanner(payload, handlers)
295
- break
296
- case 'cta':
297
- control = renderCta(payload, handlers)
298
- break
299
- case 'modal':
300
- control = renderModal(payload, handlers)
301
- break
302
- case 'checklist':
303
- control = renderChecklist(payload, decision.decision_id, handlers)
304
- break
305
- case 'spotlight': {
306
- const anchor = await resolveAnchor(payload.anchor_selector)
307
- if (!anchor) {
308
- this.outcomeTracker?.dispatch(decision.decision_id, 'no_show', {
309
- reason: 'anchor_unresolved',
310
- selector: payload.anchor_selector ?? null,
311
- })
312
- return
313
- }
314
- control = renderSpotlight(payload, anchor, handlers)
315
- break
316
- }
317
- default:
318
- this.outcomeTracker?.dispatch(decision.decision_id, 'no_show', {
319
- reason: 'kind_not_supported',
320
- kind: payload.kind,
321
- })
322
- return
323
- }
324
- } catch (e) {
325
- this.log('render error', e)
326
- handlers.onFailed({ error: String(e) })
327
- return
328
- }
329
-
330
- if (!control) return
331
-
332
- // Slot collision: store decide quem fica · sup ersedido é dismissado.
333
- const superseded = this.store.claim(
334
- payload.kind,
335
- decision.decision_id,
336
- decision.confidence,
337
- payload.anchor_selector,
338
- () => control!.dismiss('dismissed'),
339
- )
340
- if (superseded && superseded.decisionId !== decision.decision_id) {
341
- superseded.dismiss()
342
- this.outcomeTracker?.dispatch(superseded.decisionId, 'dismissed', {
343
- reason: 'superseded',
344
- by: decision.decision_id,
345
- })
346
- }
347
-
348
- this.outcomeTracker?.registerPending(decision.decision_id, this.activeWorkflow ?? undefined)
349
- appendToRoot(control.el)
350
- }
351
-
352
- private shouldRender(): boolean {
353
- if (this.config?.renderInterventions === false) return false
354
- if (this.runtime?.features && this.runtime.features.interventions_enabled === false) return false
355
- return typeof document !== 'undefined'
356
- }
357
-
358
- private shouldTriggerEval(primitive: CanonicalPrimitive): boolean {
359
- // Eventos que tipicamente disparam decisão (state-change ou friction).
360
- return (
361
- primitive === 'workflow.started' ||
362
- primitive === 'workflow.blocked' ||
363
- primitive === 'workflow.completed' ||
364
- primitive === 'workflow.abandoned' ||
365
- primitive === 'friction.detected' ||
366
- primitive === 'support.sentiment_negative' ||
367
- primitive === 'expansion.intent_detected' ||
368
- primitive === 'expansion.ceiling_hit'
369
- )
370
- }
371
-
372
- private pushRing(entry: RingEntry): void {
373
- this.ring.push(entry)
374
- while (this.ring.length > RING_BUFFER_LIMIT) this.ring.shift()
375
- }
376
-
377
- private themeFromRuntime(): ThemeConfig | undefined {
378
- return this.runtime?.theme
379
- }
380
-
381
- private readCachedConfig(): RuntimeConfig | null {
382
- if (typeof localStorage === 'undefined') return null
383
- try {
384
- const raw = localStorage.getItem(CONFIG_CACHE_KEY)
385
- if (!raw) return null
386
- const parsed = JSON.parse(raw) as { at: number; data: RuntimeConfig }
387
- if (Date.now() - parsed.at > CONFIG_CACHE_TTL_MS) return null
388
- return parsed.data
389
- } catch {
390
- return null
391
- }
392
- }
393
-
394
- private writeCachedConfig(config: RuntimeConfig): void {
395
- if (typeof localStorage === 'undefined') return
396
- try {
397
- localStorage.setItem(CONFIG_CACHE_KEY, JSON.stringify({ at: Date.now(), data: config }))
398
- } catch {
399
- // storage quota · ignore
400
- }
401
- }
402
-
403
- private log(...args: unknown[]): void {
404
- if (this.config?.debug && typeof console !== 'undefined') {
405
- console.warn('[holdy]', ...args)
406
- }
407
- }
408
- }
409
-
410
- export const Holdy = new HoldySdk()
411
-
412
- export type {
413
- AccountIdentity,
414
- CanonicalPrimitive,
415
- DecisionResult,
416
- DecisionListener,
417
- HoldyConfig,
418
- IdentifyOptions,
419
- InterventionPayload,
420
- OutcomeValue,
421
- RuntimeConfig,
422
- ThemeConfig,
423
- TrackOptions,
424
- UserIdentity,
425
- } from './types'
426
-
427
- export default Holdy
1
+ export { Holdy, HoldyCore, DEFAULT_BASE_URL } from './server'
2
+ export { HoldyWeb, initHoldy } from './web'
3
+ export type { HoldyWebOptions } from './web'
4
+ export type { HoldyOptions, Properties, TrackOverrides, CanonicalEvent } from './types'
package/src/server.ts ADDED
@@ -0,0 +1,19 @@
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
+
9
+ import { HoldyCore } from './core'
10
+ import type { HoldyOptions } from './types'
11
+
12
+ export class Holdy extends HoldyCore {
13
+ constructor(opts: HoldyOptions) {
14
+ super('sdk_server', opts)
15
+ }
16
+ }
17
+
18
+ export { HoldyCore, DEFAULT_BASE_URL } from './core'
19
+ export type { HoldyOptions, Properties, TrackOverrides, CanonicalEvent } from './types'
package/src/types.ts CHANGED
@@ -1,173 +1,49 @@
1
- // Public types — keep in sync with backend canonical-v1.ts + decisions/engine.ts.
2
-
3
- export type CanonicalPrimitive =
4
- | 'workflow.started'
5
- | 'workflow.progressed'
6
- | 'workflow.blocked'
7
- | 'workflow.completed'
8
- | 'workflow.abandoned'
9
- | 'outcome.progressed'
10
- | 'outcome.completed'
11
- | 'outcome.regressed'
12
- | 'friction.detected'
13
- | 'collaboration.invited'
14
- | 'collaboration.joined'
15
- | 'collaboration.deepened'
16
- | 'collaboration.weakened'
17
- | 'collaboration.champion_lost'
18
- | 'support.escalated'
19
- | 'support.sentiment_negative'
20
- | 'support.theme_detected'
21
- | 'expansion.intent_detected'
22
- | 'expansion.ceiling_hit'
23
- | 'memory.append'
24
- | 'session.activity'
25
-
26
- export type DecisionType =
27
- | 'intervention'
28
- | 'briefing'
29
- | 'cs_task'
30
- | 'track_journey'
31
- | 'no_action'
32
-
33
- export type InterventionKind =
34
- | 'tooltip'
35
- | 'banner'
36
- | 'modal'
37
- | 'checklist'
38
- | 'spotlight'
39
- | 'cta'
40
-
41
- export interface ChecklistItem {
42
- /** ID estável · persistência local + dedup. */
43
- id: string
44
- label: string
45
- /** Opcional · link aberto ao clicar. Sem href, item só marca completed. */
46
- href?: string
47
- /** Workflow ou outcome primitive que marca esse item como completed quando emitido. */
48
- completed_event?: string
49
- /** Pré-marcado · server pode indicar que cliente já fez essa etapa. */
50
- completed?: boolean
51
- }
52
-
53
- export interface InterventionPayload {
54
- kind: InterventionKind
55
- text: string
56
- /** Título opcional · usado em modal/checklist headers. */
57
- title?: string
58
- cta_text?: string
59
- cta_href?: string
60
- anchor_selector?: string
61
- /** Items pro kind=checklist. */
62
- items?: ChecklistItem[]
63
- }
64
-
65
- export interface DecisionResult {
66
- decision_id: string
67
- decision_type: DecisionType
68
- decision_payload: InterventionPayload | Record<string, unknown>
69
- rule_key: string
70
- confidence: number
71
- reason: string
72
- decided_at: string
73
- }
74
-
75
- export type OutcomeValue =
76
- | 'executed'
77
- | 'dismissed'
78
- | 'converted'
79
- | 'no_show'
80
- | 'failed'
1
+ /**
2
+ * Tipos públicos do @holdyapp/sdk (entry-contract.md §2 + §6).
3
+ *
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).
7
+ */
81
8
 
82
- export interface RuntimeConfig {
83
- api_version: string
84
- taxonomy_version: string
85
- organization_id: string
86
- thresholds: Record<string, number | string>
87
- features: {
88
- auto_attribution: boolean
89
- interventions_enabled: boolean
90
- consent_required: boolean
91
- }
92
- theme?: ThemeConfig
93
- }
9
+ export type Properties = Record<string, unknown>
94
10
 
95
- export interface ThemeConfig {
96
- primary_color: string
97
- font_family: string
98
- border_radius_px: number
99
- position_offset_px: number
11
+ export interface CanonicalEvent {
12
+ event: string
13
+ occurred_at: string
14
+ source: 'sdk_web' | 'sdk_server'
15
+ user_id?: string
16
+ anonymous_id?: string
17
+ properties: Properties
18
+ context?: Properties
100
19
  }
101
20
 
102
- export const DEFAULT_THEME: ThemeConfig = {
103
- primary_color: '#ca2121',
104
- font_family: 'inherit',
105
- border_radius_px: 8,
106
- position_offset_px: 8,
107
- }
108
-
109
- export interface HoldyConfig {
21
+ export interface HoldyOptions {
22
+ /** API key `hol_{env}_...` com scope events:write. */
110
23
  apiKey: string
111
- apiUrl?: string
112
- accountId?: string
113
- userId?: string
114
- sessionId?: string
115
- consumer?: 'ui' | 'csm' | 'agent'
116
- debug?: boolean
117
- /** Disables render runtime · SDK só captura telemetry. */
118
- renderInterventions?: boolean
24
+ /** Override · default https://holdy.chat/api/v1 */
25
+ baseUrl?: string
26
+ /** Flush automático quando a fila atinge este tamanho · default 20. */
27
+ flushAt?: number
28
+ /** Flush automático a cada N ms · default 5000. 0 desliga o timer. */
29
+ flushIntervalMs?: number
119
30
  /**
120
- * Stage 28 · auto-capture de page views (initial_load + route_change +
121
- * visibility_change). Quando true, SDK monkeypatcha history.pushState/
122
- * replaceState + escuta popstate/visibilitychange e emite session.activity
123
- * sem código manual. Default false pra back-compat.
31
+ * Chave da account do cliente (`context.account_external_id`). Define o
32
+ * vínculo sem emitir evento — `group()` continua sendo a chamada pra
33
+ * declarar a account COM traits.
124
34
  */
125
- autoCapture?: boolean
126
- }
127
-
128
- /**
129
- * Rich account metadata enviado pra /v1/accounts/upsert.
130
- * Backend usa COALESCE — não sobrescreve campos preenchidos no wizard manual.
131
- *
132
- * `external_id` é o identificador do cliente (string opaca · ex "acct_abc123").
133
- * Backend resolve via customer_identities(source='sdk') → accounts.id.
134
- */
135
- export interface AccountIdentity {
136
- external_id: string
137
- name?: string
138
- logo_url?: string
139
- domain?: string
140
- plan?: string
141
- mrr?: number
142
- contract_start?: string
143
- contract_end?: string
144
- lifecycle_stage?: 'onboarding' | 'adoption' | 'expansion' | 'renewal' | 'churned'
145
- traits?: Record<string, string | number | boolean | null>
146
- }
147
-
148
- export interface UserIdentity {
149
- external_id: string
150
- email?: string
151
- name?: string
152
- job_title?: string
153
- role?: 'decision_maker' | 'champion' | 'admin' | 'user' | 'unknown'
35
+ accountKey?: string
36
+ /** Logs de debug no console. */
37
+ debug?: boolean
154
38
  }
155
39
 
156
- export interface IdentifyOptions {
157
- /** ID interno Holdy (UUID) · opcional · usado pra retro-compat. */
158
- accountId?: string
40
+ export interface TrackOverrides {
41
+ /** Server-side: end-user do evento (stateless por chamada). */
159
42
  userId?: string
160
- sessionId?: string
161
- /** Metadata rica · gera best-effort upsert em /v1/accounts/upsert. */
162
- account?: AccountIdentity
163
- user?: UserIdentity
43
+ /** Server-side: chave de account do cliente (context.account_external_id). */
44
+ accountKey?: string
45
+ /** Timestamp do evento · default agora. */
46
+ occurredAt?: Date | string
47
+ /** Metadata adicional pro context. */
48
+ context?: Properties
164
49
  }
165
-
166
- export interface TrackOptions {
167
- /** Pula chamada implícita ao decisions/evaluate. */
168
- skipEval?: boolean
169
- /** Workflow corrente · override do estado interno. */
170
- workflow?: string
171
- }
172
-
173
- export type DecisionListener = (decision: DecisionResult) => void