@holdyapp/sdk 1.1.0 → 1.5.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 +52 -1
- package/dist/arm.d.ts +5 -0
- package/dist/arm.d.ts.map +1 -0
- package/dist/changes.d.ts +42 -0
- package/dist/changes.d.ts.map +1 -0
- package/dist/core.d.ts +17 -6
- package/dist/core.d.ts.map +1 -1
- package/dist/friction.d.ts +91 -0
- package/dist/friction.d.ts.map +1 -0
- package/dist/holdy.js +6 -1
- package/dist/holdy.js.map +4 -4
- package/dist/index.cjs +748 -26
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +748 -26
- package/dist/index.mjs.map +4 -4
- package/dist/run.d.ts +37 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/server.cjs +127 -23
- package/dist/server.cjs.map +3 -3
- package/dist/server.d.ts +14 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.mjs +124 -22
- package/dist/server.mjs.map +3 -3
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/web.d.ts +18 -0
- package/dist/web.d.ts.map +1 -1
- package/dist/web.mjs +701 -26
- package/dist/web.mjs.map +4 -4
- package/package.json +1 -1
- package/src/arm.test.ts +47 -0
- package/src/arm.ts +25 -0
- package/src/changes.test.ts +54 -0
- package/src/changes.ts +357 -0
- package/src/core.test.ts +78 -4
- package/src/core.ts +95 -31
- package/src/friction.test.ts +223 -0
- package/src/friction.ts +419 -0
- package/src/index.ts +1 -0
- package/src/run.test.ts +47 -0
- package/src/run.ts +80 -0
- package/src/server.ts +17 -0
- package/src/types.ts +2 -0
- package/src/web.ts +52 -1
package/src/run.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execução como unidade de trabalho (source: sdk_server).
|
|
3
|
+
*
|
|
4
|
+
* Produto de agente não tem sessão de browser: o trabalho roda no servidor,
|
|
5
|
+
* por API, em background. Pra Holdy dizer "começou e concluiu na mesma
|
|
6
|
+
* execução" e dividir recurso por resultado entregue, todos os eventos de uma
|
|
7
|
+
* tarefa carregam a mesma `ai_run_id` — a chave que o composer usa por padrão
|
|
8
|
+
* no grão de execução (lib/knowledge/compose-definition.ts).
|
|
9
|
+
*
|
|
10
|
+
* Não inventa vocabulário: início e fim saem com o nome que o cliente deu à
|
|
11
|
+
* execução (`support_agent.started`), e todo passo no meio é nome dele. O
|
|
12
|
+
* retorno passa direto e o erro propaga; só o NOME do erro vira propriedade,
|
|
13
|
+
* porque a mensagem carrega dado do cliente com frequência demais.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { Properties, TrackOverrides } from './types'
|
|
17
|
+
|
|
18
|
+
export interface RunOptions {
|
|
19
|
+
/** Nome da execução no vocabulário do produto, ex.: 'support_agent'. */
|
|
20
|
+
name: string
|
|
21
|
+
/** Conta (id externo) dona da execução. */
|
|
22
|
+
accountKey?: string
|
|
23
|
+
userId?: string
|
|
24
|
+
/** Chave própria (trace id do agente); ausente = gerada aqui. */
|
|
25
|
+
runId?: string
|
|
26
|
+
/** Propriedades repetidas em todos os eventos da execução. */
|
|
27
|
+
properties?: Properties
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface RunHandle {
|
|
31
|
+
/** A chave que agrupa tudo desta execução. */
|
|
32
|
+
id: string
|
|
33
|
+
/** Um passo dentro da execução, já carimbado com ai_run_id. */
|
|
34
|
+
track(event: string, properties?: Properties): void
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type TrackFn = (name: string, properties?: Properties, overrides?: TrackOverrides) => void
|
|
38
|
+
|
|
39
|
+
export function newRunId(): string {
|
|
40
|
+
const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto
|
|
41
|
+
if (typeof c?.randomUUID === 'function') return c.randomUUID()
|
|
42
|
+
return `run_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function runEventName(name: string, suffix: 'started' | 'completed' | 'failed'): string {
|
|
46
|
+
const base = name.trim().replace(/\.$/, '')
|
|
47
|
+
return `${base || 'run'}.${suffix}`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function withRun<T>(
|
|
51
|
+
track: TrackFn,
|
|
52
|
+
opts: RunOptions,
|
|
53
|
+
fn: (run: RunHandle) => Promise<T> | T,
|
|
54
|
+
): Promise<T> {
|
|
55
|
+
if (!opts || !opts.name) throw new Error('[holdy] run: name is required')
|
|
56
|
+
|
|
57
|
+
const id = opts.runId && opts.runId.length > 0 ? opts.runId : newRunId()
|
|
58
|
+
const base: Properties = { ...(opts.properties ?? {}), ai_run_id: id }
|
|
59
|
+
const overrides: TrackOverrides = {
|
|
60
|
+
...(opts.accountKey ? { accountKey: opts.accountKey } : {}),
|
|
61
|
+
...(opts.userId ? { userId: opts.userId } : {}),
|
|
62
|
+
}
|
|
63
|
+
const emit = (event: string, properties?: Properties) =>
|
|
64
|
+
track(event, { ...base, ...(properties ?? {}), ai_run_id: id }, overrides)
|
|
65
|
+
|
|
66
|
+
const startedAt = Date.now()
|
|
67
|
+
emit(runEventName(opts.name, 'started'))
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const result = await fn({ id, track: emit })
|
|
71
|
+
emit(runEventName(opts.name, 'completed'), { duration_ms: Date.now() - startedAt })
|
|
72
|
+
return result
|
|
73
|
+
} catch (err) {
|
|
74
|
+
emit(runEventName(opts.name, 'failed'), {
|
|
75
|
+
duration_ms: Date.now() - startedAt,
|
|
76
|
+
error: err instanceof Error ? err.name : 'Error',
|
|
77
|
+
})
|
|
78
|
+
throw err
|
|
79
|
+
}
|
|
80
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -7,13 +7,30 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { HoldyCore } from './core'
|
|
10
|
+
import { withRun, type RunHandle, type RunOptions } from './run'
|
|
10
11
|
import type { HoldyOptions } from './types'
|
|
11
12
|
|
|
12
13
|
export class Holdy extends HoldyCore {
|
|
13
14
|
constructor(opts: HoldyOptions) {
|
|
14
15
|
super('sdk_server', opts)
|
|
15
16
|
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Executa `fn` como UMA execução de agente: todo evento lá dentro carrega a
|
|
20
|
+
* mesma `ai_run_id`, e `<name>.started` / `.completed` / `.failed` saem
|
|
21
|
+
* sozinhos.
|
|
22
|
+
*
|
|
23
|
+
* await holdy.run({ name: 'support_agent', accountKey: 'acme' }, async (run) => {
|
|
24
|
+
* run.track('ticket_drafted', { ai_tokens: 1200, ai_cost_usd: 0.02 })
|
|
25
|
+
* return resolveTicket(ticket)
|
|
26
|
+
* })
|
|
27
|
+
*/
|
|
28
|
+
run<T>(opts: RunOptions, fn: (run: RunHandle) => Promise<T> | T): Promise<T> {
|
|
29
|
+
return withRun((name, properties, overrides) => this.track(name, properties, overrides), opts, fn)
|
|
30
|
+
}
|
|
16
31
|
}
|
|
17
32
|
|
|
18
33
|
export { HoldyCore, DEFAULT_BASE_URL } from './core'
|
|
19
34
|
export type { HoldyOptions, Properties, TrackOverrides, CanonicalEvent } from './types'
|
|
35
|
+
export { newRunId, runEventName } from './run'
|
|
36
|
+
export type { RunHandle, RunOptions } from './run'
|
package/src/types.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface CanonicalEvent {
|
|
|
12
12
|
event: string
|
|
13
13
|
occurred_at: string
|
|
14
14
|
source: 'sdk_web' | 'sdk_server'
|
|
15
|
+
/** Id do evento gerado no SDK · torna o reenvio de um lote idempotente. */
|
|
16
|
+
external_id?: string
|
|
15
17
|
user_id?: string
|
|
16
18
|
anonymous_id?: string
|
|
17
19
|
properties: Properties
|
package/src/web.ts
CHANGED
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
|
|
14
14
|
import { HoldyCore } from './core'
|
|
15
15
|
import { installAutoCapture, type NavigationKind } from './auto-capture'
|
|
16
|
+
import { installFrictionCapture, type FrictionCapture, type FrictionCaptureOptions } from './friction'
|
|
16
17
|
import { buildWebContext } from './web-context'
|
|
18
|
+
import { installProductChanges, type ProductChangesHandle } from './changes'
|
|
17
19
|
import type { HoldyOptions, Properties } from './types'
|
|
18
20
|
|
|
19
21
|
const ANON_STORAGE_KEY = 'holdy_anon_id'
|
|
@@ -24,10 +26,26 @@ export interface HoldyWebOptions extends HoldyOptions {
|
|
|
24
26
|
* Desligue só se o produto já chama `page()` na mão em cada tela.
|
|
25
27
|
*/
|
|
26
28
|
autoCapture?: boolean
|
|
29
|
+
/**
|
|
30
|
+
* Capturar fricção sozinho · default true. Emite `$dead_click`, `$rage_click`,
|
|
31
|
+
* `$form_abandon`, `$js_error`, `$slow_request`, `$nav_loop` e `$http_error`
|
|
32
|
+
* (requisição com status ≥ 400 ou sem resposta, e a própria página que abre
|
|
33
|
+
* com erro) — eventos determinísticos, sem DOM nem replay. Objeto ajusta os
|
|
34
|
+
* limiares; a URL da Holdy nunca conta como erro.
|
|
35
|
+
*/
|
|
36
|
+
friction?: boolean | Omit<FrictionCaptureOptions, 'onFriction'>
|
|
37
|
+
/**
|
|
38
|
+
* Aplicar as mudanças de texto e dica que a org pôs no ar na Holdy · default
|
|
39
|
+
* true. Parte das contas fica de fora por sorteio pra medir; sem conta
|
|
40
|
+
* identificada (`group()` ou `accountKey`) nada é aplicado.
|
|
41
|
+
*/
|
|
42
|
+
productChanges?: boolean
|
|
27
43
|
}
|
|
28
44
|
|
|
29
45
|
export class HoldyWeb extends HoldyCore {
|
|
30
46
|
private teardownAutoCapture: (() => void) | null = null
|
|
47
|
+
private friction: FrictionCapture | null = null
|
|
48
|
+
private changes: ProductChangesHandle | null = null
|
|
31
49
|
|
|
32
50
|
constructor(opts: HoldyWebOptions) {
|
|
33
51
|
super('sdk_web', opts)
|
|
@@ -40,9 +58,32 @@ export class HoldyWeb extends HoldyCore {
|
|
|
40
58
|
})
|
|
41
59
|
}
|
|
42
60
|
|
|
61
|
+
if (opts.friction !== false) {
|
|
62
|
+
const frictionOpts = typeof opts.friction === 'object' ? opts.friction : {}
|
|
63
|
+
this.friction = installFrictionCapture({
|
|
64
|
+
...frictionOpts,
|
|
65
|
+
ignoreUrls: [...(frictionOpts.ignoreUrls ?? []), this.baseUrl],
|
|
66
|
+
onFriction: (signal) => this.track(`$${signal.kind}`, signal.properties),
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
43
70
|
if (opts.autoCapture !== false) {
|
|
44
71
|
this.teardownAutoCapture = installAutoCapture({
|
|
45
|
-
onNavigate: (kind: NavigationKind) =>
|
|
72
|
+
onNavigate: (kind: NavigationKind) => {
|
|
73
|
+
this.page(undefined, { navigation: kind })
|
|
74
|
+
this.friction?.onNavigate(typeof location !== 'undefined' ? location.pathname : '/')
|
|
75
|
+
this.changes?.onNavigate()
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (opts.productChanges !== false && typeof window !== 'undefined') {
|
|
81
|
+
this.changes = installProductChanges({
|
|
82
|
+
baseUrl: this.baseUrl,
|
|
83
|
+
apiKey: this.apiKey,
|
|
84
|
+
getAccountKey: () => this.accountKey,
|
|
85
|
+
track: (name, properties) => this.track(name, properties),
|
|
86
|
+
debug: opts.debug,
|
|
46
87
|
})
|
|
47
88
|
}
|
|
48
89
|
}
|
|
@@ -62,6 +103,11 @@ export class HoldyWeb extends HoldyCore {
|
|
|
62
103
|
})
|
|
63
104
|
}
|
|
64
105
|
|
|
106
|
+
override group(accountKey: string, traits?: Properties): void {
|
|
107
|
+
super.group(accountKey, traits)
|
|
108
|
+
this.changes?.onNavigate()
|
|
109
|
+
}
|
|
110
|
+
|
|
65
111
|
reset(): void {
|
|
66
112
|
this.userId = null
|
|
67
113
|
this.accountKey = null
|
|
@@ -71,12 +117,17 @@ export class HoldyWeb extends HoldyCore {
|
|
|
71
117
|
// storage indisponível (SSR/privacy mode) — ok
|
|
72
118
|
}
|
|
73
119
|
this.anonymousId = resolveAnonymousId()
|
|
120
|
+
this.changes?.onNavigate()
|
|
74
121
|
}
|
|
75
122
|
|
|
76
123
|
/** Flush final + desinstala o auto-capture (devolve `history` ao original). */
|
|
77
124
|
override async shutdown(): Promise<void> {
|
|
78
125
|
this.teardownAutoCapture?.()
|
|
79
126
|
this.teardownAutoCapture = null
|
|
127
|
+
this.friction?.teardown()
|
|
128
|
+
this.friction = null
|
|
129
|
+
this.changes?.teardown()
|
|
130
|
+
this.changes = null
|
|
80
131
|
await super.shutdown()
|
|
81
132
|
}
|
|
82
133
|
|