@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.
- package/README.md +20 -147
- package/dist/auto-capture.d.ts +12 -16
- package/dist/auto-capture.d.ts.map +1 -0
- package/dist/browser.d.ts +23 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/core.d.ts +42 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/holdy.js +2 -0
- package/dist/holdy.js.map +7 -0
- package/dist/index.cjs.js +422 -0
- package/dist/index.cjs.js.map +7 -0
- package/dist/index.d.ts +5 -46
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +399 -0
- package/dist/index.mjs.map +7 -0
- package/dist/server.cjs.js +171 -0
- package/dist/server.cjs.js.map +7 -0
- package/dist/server.d.ts +15 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.mjs +148 -0
- package/dist/server.mjs.map +7 -0
- package/dist/types.d.ts +41 -109
- package/dist/types.d.ts.map +1 -0
- package/dist/web-context.d.ts +51 -0
- package/dist/web-context.d.ts.map +1 -0
- package/dist/web.d.ts +41 -0
- package/dist/web.d.ts.map +1 -0
- package/dist/web.mjs +389 -0
- package/dist/web.mjs.map +7 -0
- package/package.json +25 -21
- package/src/auto-capture.ts +51 -78
- package/src/browser.ts +55 -0
- package/src/core.test.ts +147 -0
- package/src/core.ts +172 -0
- package/src/index.ts +4 -427
- package/src/server.ts +19 -0
- package/src/types.ts +38 -162
- package/src/web-context.test.ts +71 -0
- package/src/web-context.ts +227 -0
- package/src/web.ts +108 -0
- package/LICENSE +0 -21
- package/dist/anchor.d.ts +0 -8
- package/dist/holdy.cjs.js +0 -255
- package/dist/holdy.cjs.js.map +0 -7
- package/dist/holdy.esm.js +0 -255
- package/dist/holdy.esm.js.map +0 -7
- package/dist/holdy.iife.js +0 -255
- package/dist/holdy.iife.js.map +0 -7
- package/dist/outcome.d.ts +0 -19
- package/dist/queue.d.ts +0 -54
- package/dist/render/kinds/banner.d.ts +0 -3
- package/dist/render/kinds/checklist.d.ts +0 -19
- package/dist/render/kinds/cta.d.ts +0 -8
- package/dist/render/kinds/modal.d.ts +0 -16
- package/dist/render/kinds/shared.d.ts +0 -16
- package/dist/render/kinds/spotlight.d.ts +0 -17
- package/dist/render/kinds/tooltip.d.ts +0 -3
- package/dist/render/root.d.ts +0 -3
- package/dist/render/styles.d.ts +0 -5
- package/dist/store.d.ts +0 -36
- package/dist/transport.d.ts +0 -59
- package/src/anchor.ts +0 -72
- package/src/outcome.ts +0 -51
- package/src/queue.ts +0 -296
- package/src/render/kinds/banner.ts +0 -52
- package/src/render/kinds/checklist.ts +0 -175
- package/src/render/kinds/cta.ts +0 -53
- package/src/render/kinds/modal.ts +0 -129
- package/src/render/kinds/shared.ts +0 -67
- package/src/render/kinds/spotlight.ts +0 -176
- package/src/render/kinds/tooltip.ts +0 -82
- package/src/render/root.ts +0 -69
- package/src/render/styles.ts +0 -280
- package/src/store.ts +0 -106
- package/src/transport.ts +0 -182
package/src/index.ts
CHANGED
|
@@ -1,427 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
103
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
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
|
-
|
|
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
|
|
157
|
-
/**
|
|
158
|
-
accountId?: string
|
|
40
|
+
export interface TrackOverrides {
|
|
41
|
+
/** Server-side: end-user do evento (stateless por chamada). */
|
|
159
42
|
userId?: string
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|