@holdyapp/sdk 0.2.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/LICENSE +21 -0
- package/README.md +165 -0
- package/dist/anchor.d.ts +8 -0
- package/dist/auto-capture.d.ts +21 -0
- package/dist/holdy.cjs.js +255 -0
- package/dist/holdy.cjs.js.map +7 -0
- package/dist/holdy.esm.js +255 -0
- package/dist/holdy.esm.js.map +7 -0
- package/dist/holdy.iife.js +255 -0
- package/dist/holdy.iife.js.map +7 -0
- package/dist/index.d.ts +46 -0
- package/dist/outcome.d.ts +19 -0
- package/dist/queue.d.ts +54 -0
- package/dist/render/kinds/banner.d.ts +3 -0
- package/dist/render/kinds/checklist.d.ts +19 -0
- package/dist/render/kinds/cta.d.ts +8 -0
- package/dist/render/kinds/modal.d.ts +16 -0
- package/dist/render/kinds/shared.d.ts +16 -0
- package/dist/render/kinds/spotlight.d.ts +17 -0
- package/dist/render/kinds/tooltip.d.ts +3 -0
- package/dist/render/root.d.ts +3 -0
- package/dist/render/styles.d.ts +5 -0
- package/dist/store.d.ts +36 -0
- package/dist/transport.d.ts +59 -0
- package/dist/types.d.ts +114 -0
- package/package.json +60 -0
- package/src/anchor.ts +72 -0
- package/src/auto-capture.ts +129 -0
- package/src/index.ts +427 -0
- package/src/outcome.ts +51 -0
- package/src/queue.ts +296 -0
- package/src/render/kinds/banner.ts +52 -0
- package/src/render/kinds/checklist.ts +175 -0
- package/src/render/kinds/cta.ts +53 -0
- package/src/render/kinds/modal.ts +129 -0
- package/src/render/kinds/shared.ts +67 -0
- package/src/render/kinds/spotlight.ts +176 -0
- package/src/render/kinds/tooltip.ts +82 -0
- package/src/render/root.ts +69 -0
- package/src/render/styles.ts +280 -0
- package/src/store.ts +106 -0
- package/src/transport.ts +182 -0
- package/src/types.ts +173 -0
package/src/store.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { InterventionKind } from './types'
|
|
2
|
+
|
|
3
|
+
interface ActiveSlot {
|
|
4
|
+
decisionId: string
|
|
5
|
+
confidence: number
|
|
6
|
+
kind: InterventionKind
|
|
7
|
+
dismiss: () => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Active intervention slots · prioridade por confidence.
|
|
12
|
+
*
|
|
13
|
+
* Caps globais:
|
|
14
|
+
* - 1 banner global
|
|
15
|
+
* - 1 modal global (Wave 2 · UX bloqueia tudo, único)
|
|
16
|
+
* - 1 checklist global (Wave 2 · sticky bottom-right)
|
|
17
|
+
* - N tooltips, 1 por anchor
|
|
18
|
+
* - N spotlights, 1 por anchor (Wave 2)
|
|
19
|
+
* - 1 CTA global (sticky bottom-right)
|
|
20
|
+
*
|
|
21
|
+
* Modal e checklist + cta competem pelo mesmo "espaço mental" do usuário ·
|
|
22
|
+
* a próxima implementação pode consolidar (1 floating element no canto).
|
|
23
|
+
* Wave 2 mantém slots separados pra simplicidade.
|
|
24
|
+
*
|
|
25
|
+
* Quando 2 decisions reivindicam o mesmo slot, a de menor confidence perde.
|
|
26
|
+
*/
|
|
27
|
+
export class InterventionStore {
|
|
28
|
+
private readonly tooltipsByAnchor = new Map<string, ActiveSlot>()
|
|
29
|
+
private readonly spotlightsByAnchor = new Map<string, ActiveSlot>()
|
|
30
|
+
private banner: ActiveSlot | null = null
|
|
31
|
+
private modal: ActiveSlot | null = null
|
|
32
|
+
private checklist: ActiveSlot | null = null
|
|
33
|
+
private cta: ActiveSlot | null = null
|
|
34
|
+
|
|
35
|
+
/** Tenta reivindicar slot. Retorna previous slot que precisa ser dismissado (caller). */
|
|
36
|
+
claim(
|
|
37
|
+
kind: InterventionKind,
|
|
38
|
+
decisionId: string,
|
|
39
|
+
confidence: number,
|
|
40
|
+
anchorSelector: string | undefined,
|
|
41
|
+
dismiss: () => void,
|
|
42
|
+
): ActiveSlot | null {
|
|
43
|
+
const slot: ActiveSlot = { decisionId, confidence, kind, dismiss }
|
|
44
|
+
|
|
45
|
+
if (kind === 'tooltip') {
|
|
46
|
+
const key = anchorSelector ?? '__no_anchor__'
|
|
47
|
+
const prev = this.tooltipsByAnchor.get(key)
|
|
48
|
+
if (prev && prev.confidence >= confidence) return slot
|
|
49
|
+
this.tooltipsByAnchor.set(key, slot)
|
|
50
|
+
return prev ?? null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (kind === 'spotlight') {
|
|
54
|
+
const key = anchorSelector ?? '__no_anchor__'
|
|
55
|
+
const prev = this.spotlightsByAnchor.get(key)
|
|
56
|
+
if (prev && prev.confidence >= confidence) return slot
|
|
57
|
+
this.spotlightsByAnchor.set(key, slot)
|
|
58
|
+
return prev ?? null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (kind === 'banner') {
|
|
62
|
+
const prev = this.banner
|
|
63
|
+
if (prev && prev.confidence >= confidence) return slot
|
|
64
|
+
this.banner = slot
|
|
65
|
+
return prev ?? null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (kind === 'modal') {
|
|
69
|
+
const prev = this.modal
|
|
70
|
+
if (prev && prev.confidence >= confidence) return slot
|
|
71
|
+
this.modal = slot
|
|
72
|
+
return prev ?? null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (kind === 'checklist') {
|
|
76
|
+
const prev = this.checklist
|
|
77
|
+
if (prev && prev.confidence >= confidence) return slot
|
|
78
|
+
this.checklist = slot
|
|
79
|
+
return prev ?? null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (kind === 'cta') {
|
|
83
|
+
const prev = this.cta
|
|
84
|
+
if (prev && prev.confidence >= confidence) return slot
|
|
85
|
+
this.cta = slot
|
|
86
|
+
return prev ?? null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
release(decisionId: string): void {
|
|
93
|
+
if (this.banner?.decisionId === decisionId) this.banner = null
|
|
94
|
+
if (this.modal?.decisionId === decisionId) this.modal = null
|
|
95
|
+
if (this.checklist?.decisionId === decisionId) this.checklist = null
|
|
96
|
+
if (this.cta?.decisionId === decisionId) this.cta = null
|
|
97
|
+
for (const map of [this.tooltipsByAnchor, this.spotlightsByAnchor]) {
|
|
98
|
+
for (const [k, slot] of map) {
|
|
99
|
+
if (slot.decisionId === decisionId) {
|
|
100
|
+
map.delete(k)
|
|
101
|
+
break
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
package/src/transport.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AccountIdentity,
|
|
3
|
+
CanonicalPrimitive,
|
|
4
|
+
DecisionResult,
|
|
5
|
+
HoldyConfig,
|
|
6
|
+
RuntimeConfig,
|
|
7
|
+
UserIdentity,
|
|
8
|
+
} from './types'
|
|
9
|
+
import { EventQueue } from './queue'
|
|
10
|
+
|
|
11
|
+
const DEFAULT_API_URL = 'https://app.holdy.com.br/api/v1'
|
|
12
|
+
|
|
13
|
+
export class Transport {
|
|
14
|
+
private readonly apiKey: string
|
|
15
|
+
private readonly apiUrl: string
|
|
16
|
+
private readonly debug: boolean
|
|
17
|
+
private readonly queue: EventQueue
|
|
18
|
+
|
|
19
|
+
constructor(config: HoldyConfig) {
|
|
20
|
+
this.apiKey = config.apiKey
|
|
21
|
+
this.apiUrl = (config.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, '')
|
|
22
|
+
this.debug = config.debug ?? false
|
|
23
|
+
this.queue = new EventQueue(
|
|
24
|
+
(body) => this.sendBatch(body.events),
|
|
25
|
+
(...args) => this.log(...args),
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private headers(): Record<string, string> {
|
|
30
|
+
return {
|
|
31
|
+
'content-type': 'application/json',
|
|
32
|
+
authorization: `Bearer ${this.apiKey}`,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async fetchRuntimeConfig(): Promise<RuntimeConfig | null> {
|
|
37
|
+
try {
|
|
38
|
+
const res = await fetch(`${this.apiUrl}/runtime/config`, {
|
|
39
|
+
method: 'GET',
|
|
40
|
+
headers: this.headers(),
|
|
41
|
+
credentials: 'omit',
|
|
42
|
+
mode: 'cors',
|
|
43
|
+
})
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
this.log('runtime/config failed', res.status)
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
return (await res.json()) as RuntimeConfig
|
|
49
|
+
} catch (e) {
|
|
50
|
+
this.log('runtime/config error', e)
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async evaluate(input: {
|
|
56
|
+
account_id: string
|
|
57
|
+
current_workflow?: string
|
|
58
|
+
recent_primitives?: Array<{
|
|
59
|
+
primitive: CanonicalPrimitive
|
|
60
|
+
payload: Record<string, unknown>
|
|
61
|
+
occurred_at: string
|
|
62
|
+
}>
|
|
63
|
+
recent_sentiment?: 'positivo' | 'neutro' | 'negativo' | 'urgente'
|
|
64
|
+
consumer?: 'ui' | 'csm' | 'agent'
|
|
65
|
+
}): Promise<DecisionResult | null> {
|
|
66
|
+
try {
|
|
67
|
+
const res = await fetch(`${this.apiUrl}/decisions/evaluate`, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: this.headers(),
|
|
70
|
+
body: JSON.stringify({ consumer: 'ui', ...input }),
|
|
71
|
+
credentials: 'omit',
|
|
72
|
+
mode: 'cors',
|
|
73
|
+
})
|
|
74
|
+
if (!res.ok) {
|
|
75
|
+
this.log('decisions/evaluate failed', res.status)
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
return (await res.json()) as DecisionResult
|
|
79
|
+
} catch (e) {
|
|
80
|
+
this.log('decisions/evaluate error', e)
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Event ingest · delegado pra EventQueue (IndexedDB + exponential backoff).
|
|
87
|
+
* Tenta envio imediato; se falhar, persiste e retry em background.
|
|
88
|
+
*/
|
|
89
|
+
ingestBatch(events: Array<{
|
|
90
|
+
primitive: CanonicalPrimitive
|
|
91
|
+
occurred_at: string
|
|
92
|
+
account_id?: string
|
|
93
|
+
user_id?: string
|
|
94
|
+
session_id?: string
|
|
95
|
+
payload: Record<string, unknown>
|
|
96
|
+
idempotency_key?: string
|
|
97
|
+
}>): void {
|
|
98
|
+
if (events.length === 0) return
|
|
99
|
+
void this.queue.enqueue(events)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Send raw · usado pelo queue. Retorna true em sucesso (2xx), false em
|
|
104
|
+
* falha (rede ou 4xx/5xx).
|
|
105
|
+
*
|
|
106
|
+
* IMPORTANTE: 4xx é tratado como falha permanente — não vale retry. Mas
|
|
107
|
+
* a fila não distingue (mantém pra retry simples). MAX_ATTEMPTS = 5 limita
|
|
108
|
+
* o desperdício. Refinamento (4xx → drop imediato) fica pra Wave 3.
|
|
109
|
+
*/
|
|
110
|
+
private async sendBatch(events: unknown[]): Promise<boolean> {
|
|
111
|
+
try {
|
|
112
|
+
const res = await fetch(`${this.apiUrl}/events/batch`, {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
headers: this.headers(),
|
|
115
|
+
body: JSON.stringify({ events }),
|
|
116
|
+
credentials: 'omit',
|
|
117
|
+
mode: 'cors',
|
|
118
|
+
keepalive: true,
|
|
119
|
+
})
|
|
120
|
+
return res.ok
|
|
121
|
+
} catch (e) {
|
|
122
|
+
this.log('events/batch network error', e)
|
|
123
|
+
return false
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Upsert account + opcional user via /v1/accounts/upsert.
|
|
129
|
+
*
|
|
130
|
+
* Best-effort fire-and-forget. Backend COALESCE não sobrescreve campos não-nulos.
|
|
131
|
+
* Retorna account_id (UUID Holdy) pra hidratar config local; null em falha.
|
|
132
|
+
*/
|
|
133
|
+
async upsertAccount(payload: {
|
|
134
|
+
account: AccountIdentity
|
|
135
|
+
user?: UserIdentity
|
|
136
|
+
}): Promise<{ account_id: string; account_user_id: string | null } | null> {
|
|
137
|
+
try {
|
|
138
|
+
const res = await fetch(`${this.apiUrl}/accounts/upsert`, {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
headers: this.headers(),
|
|
141
|
+
body: JSON.stringify(payload),
|
|
142
|
+
credentials: 'omit',
|
|
143
|
+
mode: 'cors',
|
|
144
|
+
keepalive: true,
|
|
145
|
+
})
|
|
146
|
+
if (!res.ok) {
|
|
147
|
+
this.log('accounts/upsert failed', res.status)
|
|
148
|
+
return null
|
|
149
|
+
}
|
|
150
|
+
return (await res.json()) as { account_id: string; account_user_id: string | null }
|
|
151
|
+
} catch (e) {
|
|
152
|
+
this.log('accounts/upsert error', e)
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
reportOutcome(
|
|
158
|
+
decisionId: string,
|
|
159
|
+
outcome: 'executed' | 'dismissed' | 'converted' | 'no_show' | 'failed',
|
|
160
|
+
metadata?: Record<string, unknown>,
|
|
161
|
+
): void {
|
|
162
|
+
const url = `${this.apiUrl}/decisions/${encodeURIComponent(decisionId)}/outcome`
|
|
163
|
+
fetch(url, {
|
|
164
|
+
method: 'POST',
|
|
165
|
+
headers: this.headers(),
|
|
166
|
+
body: JSON.stringify({ outcome, metadata }),
|
|
167
|
+
credentials: 'omit',
|
|
168
|
+
mode: 'cors',
|
|
169
|
+
keepalive: true,
|
|
170
|
+
}).catch((e) => this.log('decisions/outcome error', e))
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private log(...args: unknown[]): void {
|
|
174
|
+
if (this.debug && typeof console !== 'undefined') {
|
|
175
|
+
console.warn('[holdy]', ...args)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
destroy(): void {
|
|
180
|
+
this.queue.destroy()
|
|
181
|
+
}
|
|
182
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
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'
|
|
81
|
+
|
|
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
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface ThemeConfig {
|
|
96
|
+
primary_color: string
|
|
97
|
+
font_family: string
|
|
98
|
+
border_radius_px: number
|
|
99
|
+
position_offset_px: number
|
|
100
|
+
}
|
|
101
|
+
|
|
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 {
|
|
110
|
+
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
|
|
119
|
+
/**
|
|
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.
|
|
124
|
+
*/
|
|
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'
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface IdentifyOptions {
|
|
157
|
+
/** ID interno Holdy (UUID) · opcional · usado pra retro-compat. */
|
|
158
|
+
accountId?: string
|
|
159
|
+
userId?: string
|
|
160
|
+
sessionId?: string
|
|
161
|
+
/** Metadata rica · gera best-effort upsert em /v1/accounts/upsert. */
|
|
162
|
+
account?: AccountIdentity
|
|
163
|
+
user?: UserIdentity
|
|
164
|
+
}
|
|
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
|