@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.
- 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 +430 -0
- package/dist/index.cjs.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 +177 -0
- package/dist/server.cjs.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 +24 -20
- 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
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { parseUserAgent, parseUtm, stripUrl } from './web-context'
|
|
3
|
+
|
|
4
|
+
describe('stripUrl', () => {
|
|
5
|
+
it('tira query e hash — href cru vira dimensão de cardinalidade quase-única', () => {
|
|
6
|
+
expect(stripUrl('https://app.x.com/rel/42?utm_source=x&t=9#top')).toBe(
|
|
7
|
+
'https://app.x.com/rel/42',
|
|
8
|
+
)
|
|
9
|
+
expect(stripUrl('https://app.x.com/')).toBe('https://app.x.com/')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('devolve null pro que não é URL', () => {
|
|
13
|
+
expect(stripUrl('/só-o-path')).toBeNull()
|
|
14
|
+
expect(stripUrl('')).toBeNull()
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
describe('parseUtm', () => {
|
|
19
|
+
it('captura os cinco parâmetros de campanha em minúsculas', () => {
|
|
20
|
+
const utm = parseUtm(
|
|
21
|
+
'https://x.com/?utm_source=Google&utm_medium=CPC&utm_campaign=Lançamento&utm_term=abc&utm_content=v2',
|
|
22
|
+
)
|
|
23
|
+
expect(utm).toEqual({
|
|
24
|
+
utm_source: 'google',
|
|
25
|
+
utm_medium: 'cpc',
|
|
26
|
+
utm_campaign: 'lançamento',
|
|
27
|
+
utm_term: 'abc',
|
|
28
|
+
utm_content: 'v2',
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('ignora ausentes e vazios em vez de gravar string vazia', () => {
|
|
33
|
+
expect(parseUtm('https://x.com/?utm_source=&foo=1')).toEqual({})
|
|
34
|
+
expect(parseUtm('https://x.com/')).toEqual({})
|
|
35
|
+
expect(parseUtm('nem-url')).toEqual({})
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe('parseUserAgent', () => {
|
|
40
|
+
const CHROME_MAC =
|
|
41
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36'
|
|
42
|
+
const SAFARI_IPHONE =
|
|
43
|
+
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1'
|
|
44
|
+
const EDGE_WINDOWS =
|
|
45
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0'
|
|
46
|
+
const FIREFOX_LINUX = 'Mozilla/5.0 (X11; Linux x86_64; rv:127.0) Gecko/20100101 Firefox/127.0'
|
|
47
|
+
const CHROME_ANDROID_TABLET =
|
|
48
|
+
'Mozilla/5.0 (Linux; Android 13; SM-X700) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36'
|
|
49
|
+
|
|
50
|
+
it('separa navegador, sistema e classe de dispositivo', () => {
|
|
51
|
+
expect(parseUserAgent(CHROME_MAC)).toEqual({ browser: 'Chrome', os: 'macOS', device: 'desktop' })
|
|
52
|
+
expect(parseUserAgent(SAFARI_IPHONE)).toEqual({ browser: 'Safari', os: 'iOS', device: 'mobile' })
|
|
53
|
+
expect(parseUserAgent(FIREFOX_LINUX)).toEqual({
|
|
54
|
+
browser: 'Firefox',
|
|
55
|
+
os: 'Linux',
|
|
56
|
+
device: 'desktop',
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('não confunde Edge com Chrome (o UA do Edge contém "chrome")', () => {
|
|
61
|
+
expect(parseUserAgent(EDGE_WINDOWS).browser).toBe('Edge')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('android sem "mobile" é tablet', () => {
|
|
65
|
+
expect(parseUserAgent(CHROME_ANDROID_TABLET).device).toBe('tablet')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('UA vazio não inventa campo', () => {
|
|
69
|
+
expect(parseUserAgent('')).toEqual({})
|
|
70
|
+
})
|
|
71
|
+
})
|
|
@@ -0,0 +1,227 @@
|
|
|
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
|
+
|
|
22
|
+
export type WebContext = Record<string, string>
|
|
23
|
+
|
|
24
|
+
const UTM_KEYS = [
|
|
25
|
+
'utm_source',
|
|
26
|
+
'utm_medium',
|
|
27
|
+
'utm_campaign',
|
|
28
|
+
'utm_term',
|
|
29
|
+
'utm_content',
|
|
30
|
+
] as const
|
|
31
|
+
|
|
32
|
+
const SESSION_STORAGE_KEY = 'holdy_session'
|
|
33
|
+
const ATTRIBUTION_STORAGE_KEY = 'holdy_attribution'
|
|
34
|
+
|
|
35
|
+
/* ── URL ──────────────────────────────────────────────────────────────────── */
|
|
36
|
+
|
|
37
|
+
/** `https://app.x.com/a/b?q=1#z` → `https://app.x.com/a/b`. */
|
|
38
|
+
export function stripUrl(href: string): string | null {
|
|
39
|
+
try {
|
|
40
|
+
const url = new URL(href)
|
|
41
|
+
return `${url.origin}${url.pathname}`
|
|
42
|
+
} catch {
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Parâmetros de campanha presentes na URL, já em lowercase. */
|
|
48
|
+
export function parseUtm(href: string): WebContext {
|
|
49
|
+
const out: WebContext = {}
|
|
50
|
+
try {
|
|
51
|
+
const params = new URL(href).searchParams
|
|
52
|
+
for (const key of UTM_KEYS) {
|
|
53
|
+
const value = params.get(key)?.trim()
|
|
54
|
+
if (value) out[key] = value.toLowerCase().slice(0, 120)
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// href inválido — sem utm, sem drama.
|
|
58
|
+
}
|
|
59
|
+
return out
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* ── User agent ───────────────────────────────────────────────────────────── */
|
|
63
|
+
|
|
64
|
+
interface UaParts {
|
|
65
|
+
browser?: string
|
|
66
|
+
os?: string
|
|
67
|
+
device?: string
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Fatia mínima do UA: navegador, sistema e classe de dispositivo.
|
|
72
|
+
*
|
|
73
|
+
* Não é biblioteca de UA parsing e não deve virar uma — o valor está em
|
|
74
|
+
* separar mobile de desktop e Safari de Chrome, não em cravar a versão.
|
|
75
|
+
*/
|
|
76
|
+
export function parseUserAgent(ua: string): UaParts {
|
|
77
|
+
if (!ua) return {}
|
|
78
|
+
const s = ua.toLowerCase()
|
|
79
|
+
|
|
80
|
+
const browser = s.includes('edg/')
|
|
81
|
+
? 'Edge'
|
|
82
|
+
: s.includes('opr/') || s.includes('opera')
|
|
83
|
+
? 'Opera'
|
|
84
|
+
: s.includes('samsungbrowser')
|
|
85
|
+
? 'Samsung Internet'
|
|
86
|
+
: s.includes('firefox') || s.includes('fxios')
|
|
87
|
+
? 'Firefox'
|
|
88
|
+
: s.includes('chrome') || s.includes('crios')
|
|
89
|
+
? 'Chrome'
|
|
90
|
+
: s.includes('safari')
|
|
91
|
+
? 'Safari'
|
|
92
|
+
: undefined
|
|
93
|
+
|
|
94
|
+
const os = s.includes('iphone') || s.includes('ipad') || s.includes('ipod')
|
|
95
|
+
? 'iOS'
|
|
96
|
+
: s.includes('android')
|
|
97
|
+
? 'Android'
|
|
98
|
+
: s.includes('windows')
|
|
99
|
+
? 'Windows'
|
|
100
|
+
: s.includes('mac os') || s.includes('macintosh')
|
|
101
|
+
? 'macOS'
|
|
102
|
+
: s.includes('cros')
|
|
103
|
+
? 'ChromeOS'
|
|
104
|
+
: s.includes('linux')
|
|
105
|
+
? 'Linux'
|
|
106
|
+
: undefined
|
|
107
|
+
|
|
108
|
+
// iPad moderno se declara macOS; a pista que sobra é o suporte a toque, que
|
|
109
|
+
// o parser puro não vê. Tablet aqui cobre o caso declarado no UA.
|
|
110
|
+
const device = s.includes('ipad') || (s.includes('android') && !s.includes('mobile'))
|
|
111
|
+
? 'tablet'
|
|
112
|
+
: s.includes('mobi') || s.includes('iphone') || s.includes('ipod')
|
|
113
|
+
? 'mobile'
|
|
114
|
+
: 'desktop'
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
...(browser ? { browser } : {}),
|
|
118
|
+
...(os ? { os } : {}),
|
|
119
|
+
device,
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* ── Storage · nunca lança ────────────────────────────────────────────────── */
|
|
124
|
+
|
|
125
|
+
function readStorage(storage: 'session' | 'local', key: string): string | null {
|
|
126
|
+
try {
|
|
127
|
+
const s = storage === 'session' ? sessionStorage : localStorage
|
|
128
|
+
return s.getItem(key)
|
|
129
|
+
} catch {
|
|
130
|
+
return null
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function writeStorage(storage: 'session' | 'local', key: string, value: string): void {
|
|
135
|
+
try {
|
|
136
|
+
const s = storage === 'session' ? sessionStorage : localStorage
|
|
137
|
+
s.setItem(key, value)
|
|
138
|
+
} catch {
|
|
139
|
+
// Modo privativo / storage bloqueado — o contexto degrada, o evento segue.
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function randomId(): string {
|
|
144
|
+
try {
|
|
145
|
+
return crypto.randomUUID()
|
|
146
|
+
} catch {
|
|
147
|
+
return `hol-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** ID da sessão de browser · vive enquanto a aba viver. */
|
|
152
|
+
export function resolveSessionId(): string {
|
|
153
|
+
const existing = readStorage('session', SESSION_STORAGE_KEY)
|
|
154
|
+
if (existing) return existing
|
|
155
|
+
const fresh = randomId()
|
|
156
|
+
writeStorage('session', SESSION_STORAGE_KEY, fresh)
|
|
157
|
+
return fresh
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Atribuição da sessão: utm da primeira página + referrer externo de entrada.
|
|
162
|
+
* Gravada uma vez e repetida em todo evento da sessão.
|
|
163
|
+
*/
|
|
164
|
+
export function resolveAttribution(href: string, referrer: string, currentHost: string): WebContext {
|
|
165
|
+
const stored = readStorage('session', ATTRIBUTION_STORAGE_KEY)
|
|
166
|
+
if (stored) {
|
|
167
|
+
try {
|
|
168
|
+
return JSON.parse(stored) as WebContext
|
|
169
|
+
} catch {
|
|
170
|
+
// Valor corrompido — recalcula por cima.
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const attribution: WebContext = { ...parseUtm(href) }
|
|
175
|
+
|
|
176
|
+
const referrerHost = hostOf(referrer)
|
|
177
|
+
// Referrer interno não é origem de tráfego, é navegação.
|
|
178
|
+
if (referrerHost && referrerHost !== currentHost) {
|
|
179
|
+
const clean = stripUrl(referrer)
|
|
180
|
+
if (clean) attribution.referrer = clean
|
|
181
|
+
attribution.referrer_host = referrerHost
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
writeStorage('session', ATTRIBUTION_STORAGE_KEY, JSON.stringify(attribution))
|
|
185
|
+
return attribution
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function hostOf(href: string): string | null {
|
|
189
|
+
try {
|
|
190
|
+
return new URL(href).hostname.toLowerCase() || null
|
|
191
|
+
} catch {
|
|
192
|
+
return null
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/* ── Montagem ─────────────────────────────────────────────────────────────── */
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Contexto completo do evento atual. Fora do browser devolve `{}` — o SDK
|
|
200
|
+
* server tem o próprio caminho e não deve inventar ambiente.
|
|
201
|
+
*/
|
|
202
|
+
export function buildWebContext(): WebContext {
|
|
203
|
+
if (typeof window === 'undefined' || typeof location === 'undefined') return {}
|
|
204
|
+
|
|
205
|
+
const href = location.href
|
|
206
|
+
const referrer = typeof document !== 'undefined' ? document.referrer : ''
|
|
207
|
+
const host = location.hostname.toLowerCase()
|
|
208
|
+
|
|
209
|
+
const ua = typeof navigator !== 'undefined' ? navigator.userAgent : ''
|
|
210
|
+
const { browser, os, device } = parseUserAgent(ua)
|
|
211
|
+
|
|
212
|
+
const url = stripUrl(href)
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
...(url ? { url } : {}),
|
|
216
|
+
path: location.pathname || '/',
|
|
217
|
+
host,
|
|
218
|
+
...(browser ? { browser } : {}),
|
|
219
|
+
...(os ? { os } : {}),
|
|
220
|
+
...(device ? { device } : {}),
|
|
221
|
+
...(typeof navigator !== 'undefined' && navigator.language
|
|
222
|
+
? { locale: navigator.language }
|
|
223
|
+
: {}),
|
|
224
|
+
session_id: resolveSessionId(),
|
|
225
|
+
...resolveAttribution(href, referrer, host),
|
|
226
|
+
}
|
|
227
|
+
}
|
package/src/web.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
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
|
+
|
|
14
|
+
import { HoldyCore } from './core'
|
|
15
|
+
import { installAutoCapture, type NavigationKind } from './auto-capture'
|
|
16
|
+
import { buildWebContext } from './web-context'
|
|
17
|
+
import type { HoldyOptions, Properties } from './types'
|
|
18
|
+
|
|
19
|
+
const ANON_STORAGE_KEY = 'holdy_anon_id'
|
|
20
|
+
|
|
21
|
+
export interface HoldyWebOptions extends HoldyOptions {
|
|
22
|
+
/**
|
|
23
|
+
* Emitir `$pageview` sozinho no load e a cada mudança de rota · default true.
|
|
24
|
+
* Desligue só se o produto já chama `page()` na mão em cada tela.
|
|
25
|
+
*/
|
|
26
|
+
autoCapture?: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class HoldyWeb extends HoldyCore {
|
|
30
|
+
private teardownAutoCapture: (() => void) | null = null
|
|
31
|
+
|
|
32
|
+
constructor(opts: HoldyWebOptions) {
|
|
33
|
+
super('sdk_web', opts)
|
|
34
|
+
this.anonymousId = resolveAnonymousId()
|
|
35
|
+
|
|
36
|
+
if (typeof window !== 'undefined') {
|
|
37
|
+
window.addEventListener('pagehide', () => void this.flush())
|
|
38
|
+
document.addEventListener('visibilitychange', () => {
|
|
39
|
+
if (document.visibilityState === 'hidden') void this.flush()
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (opts.autoCapture !== false) {
|
|
44
|
+
this.teardownAutoCapture = installAutoCapture({
|
|
45
|
+
onNavigate: (kind: NavigationKind) => this.page(undefined, { navigation: kind }),
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Pageview. O `$` marca evento emitido pelo SDK, não pelo produto (mesma
|
|
52
|
+
* convenção de `$identify`/`$group`); a canonicalização downstream remove o
|
|
53
|
+
* sigilo, então `$pageview` e `pageview` são o mesmo evento na leitura.
|
|
54
|
+
* url/path/referrer/utm já vêm no context de todo evento — aqui só entra o
|
|
55
|
+
* que é da página como conteúdo (nome e título).
|
|
56
|
+
*/
|
|
57
|
+
page(name?: string, properties?: Properties): void {
|
|
58
|
+
this.track('$pageview', {
|
|
59
|
+
...(properties ?? {}),
|
|
60
|
+
...(name ? { page_name: name } : {}),
|
|
61
|
+
...(typeof document !== 'undefined' && document.title ? { title: document.title } : {}),
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
reset(): void {
|
|
66
|
+
this.userId = null
|
|
67
|
+
this.accountKey = null
|
|
68
|
+
try {
|
|
69
|
+
localStorage.removeItem(ANON_STORAGE_KEY)
|
|
70
|
+
} catch {
|
|
71
|
+
// storage indisponível (SSR/privacy mode) — ok
|
|
72
|
+
}
|
|
73
|
+
this.anonymousId = resolveAnonymousId()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Flush final + desinstala o auto-capture (devolve `history` ao original). */
|
|
77
|
+
override async shutdown(): Promise<void> {
|
|
78
|
+
this.teardownAutoCapture?.()
|
|
79
|
+
this.teardownAutoCapture = null
|
|
80
|
+
await super.shutdown()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
protected override environmentContext(): Properties {
|
|
84
|
+
return buildWebContext()
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function resolveAnonymousId(): string {
|
|
89
|
+
try {
|
|
90
|
+
const existing = localStorage.getItem(ANON_STORAGE_KEY)
|
|
91
|
+
if (existing) return existing
|
|
92
|
+
const fresh = crypto.randomUUID()
|
|
93
|
+
localStorage.setItem(ANON_STORAGE_KEY, fresh)
|
|
94
|
+
return fresh
|
|
95
|
+
} catch {
|
|
96
|
+
return crypto.randomUUID()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let singleton: HoldyWeb | null = null
|
|
101
|
+
|
|
102
|
+
/** Init idempotente — segunda chamada retorna a instância existente. */
|
|
103
|
+
export function initHoldy(opts: HoldyWebOptions): HoldyWeb {
|
|
104
|
+
if (!singleton) singleton = new HoldyWeb(opts)
|
|
105
|
+
return singleton
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export type { HoldyOptions, Properties }
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Holdy
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
package/dist/anchor.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Anchor resolver · MutationObserver-based.
|
|
3
|
-
*
|
|
4
|
-
* Cliente do produto pode renderizar o anchor depois do decision chegar
|
|
5
|
-
* (SPA route, lazy mount). Resolver espera até 5s antes de desistir.
|
|
6
|
-
* Single observer global · cap retries por selector.
|
|
7
|
-
*/
|
|
8
|
-
export declare function resolveAnchor(selector: string | undefined): Promise<Element | null>;
|