@holdyapp/sdk 1.0.1 → 1.1.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/dist/holdy.js +1 -1
- package/dist/holdy.js.map +1 -1
- package/dist/index.cjs +7 -1
- package/dist/index.cjs.map +2 -2
- package/dist/index.mjs +7 -1
- package/dist/index.mjs.map +2 -2
- package/dist/web.d.ts.map +1 -1
- package/dist/web.mjs +7 -1
- package/dist/web.mjs.map +2 -2
- package/package.json +2 -2
- package/src/web.ts +10 -1
package/dist/holdy.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var Holdy=(()=>{var g=Object.defineProperty;var O=Object.getOwnPropertyDescriptor;var C=Object.getOwnPropertyNames;var P=Object.prototype.hasOwnProperty;var T=(n,t)=>{for(var e in t)g(n,e,{get:t[e],enumerable:!0})},k=(n,t,e,
|
|
1
|
+
"use strict";var Holdy=(()=>{var g=Object.defineProperty;var O=Object.getOwnPropertyDescriptor;var C=Object.getOwnPropertyNames;var P=Object.prototype.hasOwnProperty;var T=(n,t)=>{for(var e in t)g(n,e,{get:t[e],enumerable:!0})},k=(n,t,e,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of C(t))!P.call(n,o)&&o!==e&&g(n,o,{get:()=>t[o],enumerable:!(i=O(t,o))||i.enumerable});return n};var H=n=>k(g({},"__esModule",{value:!0}),n);var z={};T(z,{flush:()=>Q,getInstance:()=>V,group:()=>Y,identify:()=>G,init:()=>q,page:()=>j,reset:()=>J,shutdown:()=>X,track:()=>B});var W="https://holdy.chat/api/v1";var d=class{apiKey;baseUrl;flushAt;debug;source;queue=[];timer=null;flushing=null;userId=null;anonymousId=null;accountKey=null;constructor(t,e){this.source=t,this.apiKey=e.apiKey,this.accountKey=e.accountKey??null,this.baseUrl=(e.baseUrl??W).replace(/\/$/,""),this.flushAt=Math.max(1,e.flushAt??20),this.debug=e.debug??!1;let i=e.flushIntervalMs??5e3;i>0&&(this.timer=setInterval(()=>void this.flush(),i),typeof this.timer.unref=="function"&&this.timer.unref())}track(t,e,i){if(!t||typeof t!="string")return;let o=i?.userId??this.userId??void 0,r=i?.accountKey??this.accountKey??void 0,s=i?.occurredAt?new Date(i.occurredAt).toISOString():new Date().toISOString(),u={...this.environmentContext(),...i?.context??{}};r&&(u.account_external_id=r);let c={event:t,occurred_at:s,source:this.source,...o?{user_id:o}:{},...this.anonymousId?{anonymous_id:this.anonymousId}:{},properties:e??{},...Object.keys(u).length>0?{context:u}:{}};this.queue.push(c),this.queue.length>1e3&&this.queue.splice(0,this.queue.length-1e3),this.queue.length>=this.flushAt&&this.flush()}identify(t,e){!t||typeof t!="string"||(this.userId=t,this.track("$identify",e??{}))}group(t,e){!t||typeof t!="string"||(this.accountKey=t,this.track("$group",e??{}))}async flush(){if(this.flushing)return this.flushing;if(this.queue.length!==0)return this.flushing=this.drain().finally(()=>{this.flushing=null}),this.flushing}async shutdown(){this.timer&&(clearInterval(this.timer),this.timer=null),await this.flush()}async drain(){for(;this.queue.length>0;){let t=this.queue.splice(0,500);if(!await this.send(t)){this.queue.unshift(...t);return}}}async send(t){try{let e=await fetch(`${this.baseUrl}/events/bulk`,{method:"POST",headers:{Authorization:`Bearer ${this.apiKey}`,"Content-Type":"application/json"},body:JSON.stringify({events:t}),...this.source==="sdk_web"?{keepalive:!0}:{}});return e.ok?!0:e.status>=400&&e.status<500?(this.log(`bulk rejected ${e.status} \xB7 batch dropped`),!0):(this.log(`bulk failed ${e.status} \xB7 will retry`),!1)}catch(e){return this.log(`bulk network error: ${e.message} \xB7 will retry`),!1}}log(t){this.debug&&console.warn(`[holdy] ${t}`)}environmentContext(){return{}}get pending(){return this.queue.length}};var y="__holdy_autocapture__";function _(n){if(typeof window>"u"||typeof history>"u")return()=>{};let t=window;if(t[y])return()=>{};t[y]=!0;let e=[],i=b(),o=null;function r(l){try{n.onNavigate(l)}catch{}}function s(){o&&clearTimeout(o),o=setTimeout(()=>{o=null;let l=b();l!==i&&(i=l,r("route_change"))},50)}r("initial_load");let u=history.pushState,c=history.replaceState;history.pushState=function(...h){let f=u.apply(this,h);return s(),f},history.replaceState=function(...h){let f=c.apply(this,h);return s(),f},e.push(()=>{history.pushState=u,history.replaceState=c});let v=()=>s();window.addEventListener("popstate",v),e.push(()=>window.removeEventListener("popstate",v));let w=()=>s();return window.addEventListener("hashchange",w),e.push(()=>window.removeEventListener("hashchange",w)),()=>{o&&clearTimeout(o);for(let l of e)l();t[y]=!1}}function b(){return typeof location>"u"?"/":`${location.pathname}${location.search}${location.hash}`}var K=["utm_source","utm_medium","utm_campaign","utm_term","utm_content"],S="holdy_session",x="holdy_attribution";function A(n){try{let t=new URL(n);return`${t.origin}${t.pathname}`}catch{return null}}function N(n){let t={};try{let e=new URL(n).searchParams;for(let i of K){let o=e.get(i)?.trim();o&&(t[i]=o.toLowerCase().slice(0,120))}}catch{}return t}function R(n){if(!n)return{};let t=n.toLowerCase(),e=t.includes("edg/")?"Edge":t.includes("opr/")||t.includes("opera")?"Opera":t.includes("samsungbrowser")?"Samsung Internet":t.includes("firefox")||t.includes("fxios")?"Firefox":t.includes("chrome")||t.includes("crios")?"Chrome":t.includes("safari")?"Safari":void 0,i=t.includes("iphone")||t.includes("ipad")||t.includes("ipod")?"iOS":t.includes("android")?"Android":t.includes("windows")?"Windows":t.includes("mac os")||t.includes("macintosh")?"macOS":t.includes("cros")?"ChromeOS":t.includes("linux")?"Linux":void 0,o=t.includes("ipad")||t.includes("android")&&!t.includes("mobile")?"tablet":t.includes("mobi")||t.includes("iphone")||t.includes("ipod")?"mobile":"desktop";return{...e?{browser:e}:{},...i?{os:i}:{},device:o}}function E(n,t){try{return(n==="session"?sessionStorage:localStorage).getItem(t)}catch{return null}}function U(n,t,e){try{(n==="session"?sessionStorage:localStorage).setItem(t,e)}catch{}}function $(){try{return crypto.randomUUID()}catch{return`hol-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,10)}`}}function D(){let n=E("session",S);if(n)return n;let t=$();return U("session",S,t),t}function F(n,t,e){let i=E("session",x);if(i)try{return JSON.parse(i)}catch{}let o={...N(n)},r=M(t);if(r&&r!==e){let s=A(t);s&&(o.referrer=s),o.referrer_host=r}return U("session",x,JSON.stringify(o)),o}function M(n){try{return new URL(n).hostname.toLowerCase()||null}catch{return null}}function I(){if(typeof window>"u"||typeof location>"u")return{};let n=location.href,t=typeof document<"u"?document.referrer:"",e=location.hostname.toLowerCase(),i=typeof navigator<"u"?navigator.userAgent:"",{browser:o,os:r,device:s}=R(i),u=A(n);return{...u?{url:u}:{},path:location.pathname||"/",host:e,...o?{browser:o}:{},...r?{os:r}:{},...s?{device:s}:{},...typeof navigator<"u"&&navigator.language?{locale:navigator.language}:{},session_id:D(),...F(n,t,e)}}var m="holdy_anon_id",p=class extends d{teardownAutoCapture=null;constructor(t){super("sdk_web",t),this.anonymousId=L(),typeof window<"u"&&(window.addEventListener("pagehide",()=>void this.flush()),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&this.flush()})),t.autoCapture!==!1&&(this.teardownAutoCapture=_({onNavigate:e=>this.page(void 0,{navigation:e})}))}page(t,e){this.track("$pageview",{...e??{},...t?{page_name:t}:{},...typeof document<"u"&&document.title?{title:document.title}:{}})}reset(){this.userId=null,this.accountKey=null;try{localStorage.removeItem(m)}catch{}this.anonymousId=L()}async shutdown(){this.teardownAutoCapture?.(),this.teardownAutoCapture=null,await super.shutdown()}environmentContext(){return I()}};function L(){try{let n=localStorage.getItem(m);if(n)return n;let t=crypto.randomUUID();return localStorage.setItem(m,t),t}catch{return crypto.randomUUID()}}var a=null;function q(n){return a||(a=new p(n)),a}function B(n,t){a?.track(n,t)}function G(n,t){a?.identify(n,t)}function Y(n,t){a?.group(n,t)}function j(n,t){a?.page(n,t)}function J(){a?.reset()}async function Q(){await a?.flush()}async function X(){await a?.shutdown(),a=null}function V(){return a}return H(z);})();
|
|
2
2
|
//# sourceMappingURL=holdy.js.map
|
package/dist/holdy.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/browser.ts", "../src/core.ts", "../src/auto-capture.ts", "../src/web-context.ts", "../src/web.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Entrada do bundle IIFE \u00B7 global `Holdy` pra instala\u00E7\u00E3o por `<script>`.\n *\n * Existe pro cliente que n\u00E3o tem bundler (ou n\u00E3o quer mexer no build) poder\n * instrumentar colando duas linhas no HTML. A API \u00E9 a mesma de `@holdyapp/sdk/web`\n * em forma de fun\u00E7\u00F5es soltas, porque `Holdy.track(...)` \u00E9 o que se espera de um\n * global.\n *\n * Chamar antes de `init` n\u00E3o lan\u00E7a: telemetria n\u00E3o derruba a p\u00E1gina do cliente.\n */\n\nimport { HoldyWeb, type HoldyWebOptions } from './web'\nimport type { Properties } from './types'\n\nlet instance: HoldyWeb | null = null\n\nexport function init(opts: HoldyWebOptions): HoldyWeb {\n // Idempotente: script duplicado na p\u00E1gina n\u00E3o duplica evento.\n if (!instance) instance = new HoldyWeb(opts)\n return instance\n}\n\nexport function track(name: string, properties?: Properties): void {\n instance?.track(name, properties)\n}\n\nexport function identify(userId: string, traits?: Properties): void {\n instance?.identify(userId, traits)\n}\n\nexport function group(accountKey: string, traits?: Properties): void {\n instance?.group(accountKey, traits)\n}\n\nexport function page(name?: string, properties?: Properties): void {\n instance?.page(name, properties)\n}\n\nexport function reset(): void {\n instance?.reset()\n}\n\nexport async function flush(): Promise<void> {\n await instance?.flush()\n}\n\nexport async function shutdown(): Promise<void> {\n await instance?.shutdown()\n instance = null\n}\n\n/** Inst\u00E2ncia corrente \u00B7 \u00FAtil pra depurar no console do browser. */\nexport function getInstance(): HoldyWeb | null {\n return instance\n}\n", "/**\n * Core do @holdyapp/sdk \u00B7 fila + flush pra POST /api/v1/events/bulk.\n *\n * Retry de batch inteiro \u00E9 seguro (partial accept + dedupe server-side).\n * Fila \u00E9 bounded: acima de MAX_QUEUE dropa os mais antigos (telemetria nunca\n * derruba o app do cliente \u2014 nenhum m\u00E9todo p\u00FAblico lan\u00E7a).\n */\n\nimport type { CanonicalEvent, HoldyOptions, Properties, TrackOverrides } from './types'\n\nexport const DEFAULT_BASE_URL = 'https://holdy.chat/api/v1'\nconst DEFAULT_FLUSH_AT = 20\nconst DEFAULT_FLUSH_INTERVAL_MS = 5000\nconst MAX_QUEUE = 1000\nconst BULK_LIMIT = 500\n\nexport class HoldyCore {\n private readonly apiKey: string\n private readonly baseUrl: string\n private readonly flushAt: number\n private readonly debug: boolean\n private readonly source: 'sdk_web' | 'sdk_server'\n\n private queue: CanonicalEvent[] = []\n private timer: ReturnType<typeof setInterval> | null = null\n private flushing: Promise<void> | null = null\n\n protected userId: string | null = null\n protected anonymousId: string | null = null\n protected accountKey: string | null = null\n\n constructor(source: 'sdk_web' | 'sdk_server', opts: HoldyOptions) {\n this.source = source\n this.apiKey = opts.apiKey\n this.accountKey = opts.accountKey ?? null\n this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, '')\n this.flushAt = Math.max(1, opts.flushAt ?? DEFAULT_FLUSH_AT)\n this.debug = opts.debug ?? false\n\n const intervalMs = opts.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS\n if (intervalMs > 0) {\n this.timer = setInterval(() => void this.flush(), intervalMs)\n // Node: n\u00E3o segura o processo vivo s\u00F3 pelo timer do SDK.\n if (typeof (this.timer as { unref?: () => void }).unref === 'function') {\n ;(this.timer as unknown as { unref: () => void }).unref()\n }\n }\n }\n\n track(name: string, properties?: Properties, overrides?: TrackOverrides): void {\n if (!name || typeof name !== 'string') return\n\n const userId = overrides?.userId ?? this.userId ?? undefined\n const accountKey = overrides?.accountKey ?? this.accountKey ?? undefined\n const occurredAt = overrides?.occurredAt\n ? new Date(overrides.occurredAt).toISOString()\n : new Date().toISOString()\n\n // Ordem: ambiente observado pelo SDK \u2192 context da chamada \u2192 carrier de\n // account. O que o caller passou vence o inferido; o carrier vence tudo,\n // porque \u00E9 o que a Data layer usa pra resolver a conta.\n const context: Properties = {\n ...this.environmentContext(),\n ...(overrides?.context ?? {}),\n }\n if (accountKey) context.account_external_id = accountKey\n\n const event: CanonicalEvent = {\n event: name,\n occurred_at: occurredAt,\n source: this.source,\n ...(userId ? { user_id: userId } : {}),\n ...(this.anonymousId ? { anonymous_id: this.anonymousId } : {}),\n properties: properties ?? {},\n ...(Object.keys(context).length > 0 ? { context } : {}),\n }\n\n this.queue.push(event)\n if (this.queue.length > MAX_QUEUE) this.queue.splice(0, this.queue.length - MAX_QUEUE)\n if (this.queue.length >= this.flushAt) void this.flush()\n }\n\n identify(userId: string, traits?: Properties): void {\n if (!userId || typeof userId !== 'string') return\n this.userId = userId\n this.track('$identify', traits ?? {})\n }\n\n group(accountKey: string, traits?: Properties): void {\n if (!accountKey || typeof accountKey !== 'string') return\n this.accountKey = accountKey\n this.track('$group', traits ?? {})\n }\n\n /** Drena a fila. Nunca lan\u00E7a \u2014 falha re-enfileira e loga em debug. */\n async flush(): Promise<void> {\n // Serializa flushes concorrentes (timer + flushAt + manual).\n if (this.flushing) return this.flushing\n if (this.queue.length === 0) return\n\n this.flushing = this.drain().finally(() => {\n this.flushing = null\n })\n return this.flushing\n }\n\n /** Flush final + para o timer. Chamar em shutdown do server / unload do web. */\n async shutdown(): Promise<void> {\n if (this.timer) {\n clearInterval(this.timer)\n this.timer = null\n }\n await this.flush()\n }\n\n private async drain(): Promise<void> {\n while (this.queue.length > 0) {\n const batch = this.queue.splice(0, BULK_LIMIT)\n const ok = await this.send(batch)\n if (!ok) {\n // Devolve pra frente da fila e para \u2014 pr\u00F3ximo flush re-tenta (dedupe-safe).\n this.queue.unshift(...batch)\n return\n }\n }\n }\n\n private async send(events: CanonicalEvent[]): Promise<boolean> {\n try {\n const res = await fetch(`${this.baseUrl}/events/bulk`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ events }),\n ...(this.source === 'sdk_web' ? { keepalive: true } : {}),\n })\n if (!res.ok) {\n // 4xx: batch estruturalmente inv\u00E1lido n\u00E3o re-tenta (evita loop infinito).\n if (res.status >= 400 && res.status < 500) {\n this.log(`bulk rejected ${res.status} \u00B7 batch dropped`)\n return true\n }\n this.log(`bulk failed ${res.status} \u00B7 will retry`)\n return false\n }\n return true\n } catch (err) {\n this.log(`bulk network error: ${(err as Error).message} \u00B7 will retry`)\n return false\n }\n }\n\n private log(msg: string): void {\n if (this.debug) console.warn(`[holdy] ${msg}`)\n }\n\n /**\n * Ambiente que o SDK observa sozinho (url, referrer, browser, utm\u2026).\n * Vazio no core; `HoldyWeb` preenche. O server n\u00E3o infere ambiente \u2014 n\u00E3o\n * existe \"p\u00E1gina atual\" num worker.\n */\n protected environmentContext(): Properties {\n return {}\n }\n\n /** Interno \u00B7 exposto pra testes. */\n get pending(): number {\n return this.queue.length\n }\n}\n", "/**\n * Auto-capture de navega\u00E7\u00E3o \u00B7 @holdyapp/sdk/web.\n *\n * Emite pageview no carregamento e a cada mudan\u00E7a de rota, inclusive nas que\n * n\u00E3o recarregam a p\u00E1gina (SPA). Sem isto, instrumentar exige que o cliente\n * chame `page()` em cada tela \u2014 e o primeiro dado s\u00F3 aparece depois de um\n * trabalho que ele ainda n\u00E3o fez.\n *\n * Idempotente: reinstalar \u00E9 no-op (guard global). O retorno desfaz o patch de\n * `history`, ent\u00E3o `destroy()` devolve o browser como estava.\n */\n\nconst INSTALLED_FLAG = '__holdy_autocapture__'\n\n/** pushState seguido de replaceState \u00E9 uma mudan\u00E7a l\u00F3gica s\u00F3. */\nconst ROUTE_DEBOUNCE_MS = 50\n\nexport type NavigationKind = 'initial_load' | 'route_change'\n\nexport interface AutoCaptureOptions {\n onNavigate: (kind: NavigationKind) => void\n}\n\ninterface GlobalWithFlag {\n [INSTALLED_FLAG]?: boolean\n}\n\nexport function installAutoCapture(opts: AutoCaptureOptions): () => void {\n if (typeof window === 'undefined' || typeof history === 'undefined') {\n return () => undefined\n }\n\n const w = window as unknown as GlobalWithFlag\n if (w[INSTALLED_FLAG]) return () => undefined\n w[INSTALLED_FLAG] = true\n\n const cleanups: Array<() => void> = []\n let lastPath = currentPath()\n let pending: ReturnType<typeof setTimeout> | null = null\n\n function emit(kind: NavigationKind): void {\n try {\n opts.onNavigate(kind)\n } catch {\n // Telemetria nunca derruba a app do cliente.\n }\n }\n\n function scheduleRouteEmit(): void {\n if (pending) clearTimeout(pending)\n pending = setTimeout(() => {\n pending = null\n const path = currentPath()\n // S\u00F3 conta como pageview se a rota mudou de fato \u2014 replaceState de\n // estado interno (filtro, scroll) n\u00E3o \u00E9 navega\u00E7\u00E3o.\n if (path === lastPath) return\n lastPath = path\n emit('route_change')\n }, ROUTE_DEBOUNCE_MS)\n }\n\n emit('initial_load')\n\n const originalPush = history.pushState\n const originalReplace = history.replaceState\n\n history.pushState = function patchedPush(...args: Parameters<History['pushState']>) {\n const result = originalPush.apply(this, args)\n scheduleRouteEmit()\n return result\n }\n history.replaceState = function patchedReplace(...args: Parameters<History['replaceState']>) {\n const result = originalReplace.apply(this, args)\n scheduleRouteEmit()\n return result\n }\n cleanups.push(() => {\n history.pushState = originalPush\n history.replaceState = originalReplace\n })\n\n // Voltar/avan\u00E7ar do browser e routers baseados em hash.\n const onPop = (): void => scheduleRouteEmit()\n window.addEventListener('popstate', onPop)\n cleanups.push(() => window.removeEventListener('popstate', onPop))\n\n const onHash = (): void => scheduleRouteEmit()\n window.addEventListener('hashchange', onHash)\n cleanups.push(() => window.removeEventListener('hashchange', onHash))\n\n return () => {\n if (pending) clearTimeout(pending)\n for (const cleanup of cleanups) cleanup()\n w[INSTALLED_FLAG] = false\n }\n}\n\n/** Rota compar\u00E1vel \u00B7 path + query, sem hash de \u00E2ncora. */\nfunction currentPath(): string {\n if (typeof location === 'undefined') return '/'\n return `${location.pathname}${location.search}${location.hash}`\n}\n", "/**\n * Contexto de ambiente do browser \u00B7 @holdyapp/sdk/web.\n *\n * Vai em `context` de todo evento, nunca em `properties`: properties \u00E9 o\n * vocabul\u00E1rio do produto do cliente (`workflow_key`, `plan`), context \u00E9 o que o\n * SDK observou sozinho. A Data layer persiste os dois (facts.context, 00247) e\n * a leitura de dimens\u00E3o consulta os dois.\n *\n * Tr\u00EAs decis\u00F5es que definem se esse dado \u00E9 \u00FAtil ou lixo:\n * \u00B7 `url` sai sem query e sem hash. Href cru tem uma varia\u00E7\u00E3o por sess\u00E3o, e\n * dimens\u00E3o com cardinalidade quase-\u00FAnica \u00E9 descartada na leitura \u2014 a\n * p\u00E1gina vira ru\u00EDdo exatamente por excesso de precis\u00E3o.\n * \u00B7 utm \u00E9 capturado na primeira p\u00E1gina e carregado pelo resto da sess\u00E3o.\n * Atribui\u00E7\u00E3o que s\u00F3 existe no primeiro evento n\u00E3o responde nada.\n * \u00B7 referrer s\u00F3 quando \u00E9 externo. Navega\u00E7\u00E3o interna preenchendo \"Origem do\n * tr\u00E1fego\" com o pr\u00F3prio dom\u00EDnio esconde a origem real.\n *\n * Tudo aqui \u00E9 best-effort: browser em modo privativo, SSR e webview sem\n * storage precisam funcionar. Nenhuma fun\u00E7\u00E3o lan\u00E7a.\n */\n\nexport type WebContext = Record<string, string>\n\nconst UTM_KEYS = [\n 'utm_source',\n 'utm_medium',\n 'utm_campaign',\n 'utm_term',\n 'utm_content',\n] as const\n\nconst SESSION_STORAGE_KEY = 'holdy_session'\nconst ATTRIBUTION_STORAGE_KEY = 'holdy_attribution'\n\n/* \u2500\u2500 URL \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/** `https://app.x.com/a/b?q=1#z` \u2192 `https://app.x.com/a/b`. */\nexport function stripUrl(href: string): string | null {\n try {\n const url = new URL(href)\n return `${url.origin}${url.pathname}`\n } catch {\n return null\n }\n}\n\n/** Par\u00E2metros de campanha presentes na URL, j\u00E1 em lowercase. */\nexport function parseUtm(href: string): WebContext {\n const out: WebContext = {}\n try {\n const params = new URL(href).searchParams\n for (const key of UTM_KEYS) {\n const value = params.get(key)?.trim()\n if (value) out[key] = value.toLowerCase().slice(0, 120)\n }\n } catch {\n // href inv\u00E1lido \u2014 sem utm, sem drama.\n }\n return out\n}\n\n/* \u2500\u2500 User agent \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\ninterface UaParts {\n browser?: string\n os?: string\n device?: string\n}\n\n/**\n * Fatia m\u00EDnima do UA: navegador, sistema e classe de dispositivo.\n *\n * N\u00E3o \u00E9 biblioteca de UA parsing e n\u00E3o deve virar uma \u2014 o valor est\u00E1 em\n * separar mobile de desktop e Safari de Chrome, n\u00E3o em cravar a vers\u00E3o.\n */\nexport function parseUserAgent(ua: string): UaParts {\n if (!ua) return {}\n const s = ua.toLowerCase()\n\n const browser = s.includes('edg/')\n ? 'Edge'\n : s.includes('opr/') || s.includes('opera')\n ? 'Opera'\n : s.includes('samsungbrowser')\n ? 'Samsung Internet'\n : s.includes('firefox') || s.includes('fxios')\n ? 'Firefox'\n : s.includes('chrome') || s.includes('crios')\n ? 'Chrome'\n : s.includes('safari')\n ? 'Safari'\n : undefined\n\n const os = s.includes('iphone') || s.includes('ipad') || s.includes('ipod')\n ? 'iOS'\n : s.includes('android')\n ? 'Android'\n : s.includes('windows')\n ? 'Windows'\n : s.includes('mac os') || s.includes('macintosh')\n ? 'macOS'\n : s.includes('cros')\n ? 'ChromeOS'\n : s.includes('linux')\n ? 'Linux'\n : undefined\n\n // iPad moderno se declara macOS; a pista que sobra \u00E9 o suporte a toque, que\n // o parser puro n\u00E3o v\u00EA. Tablet aqui cobre o caso declarado no UA.\n const device = s.includes('ipad') || (s.includes('android') && !s.includes('mobile'))\n ? 'tablet'\n : s.includes('mobi') || s.includes('iphone') || s.includes('ipod')\n ? 'mobile'\n : 'desktop'\n\n return {\n ...(browser ? { browser } : {}),\n ...(os ? { os } : {}),\n device,\n }\n}\n\n/* \u2500\u2500 Storage \u00B7 nunca lan\u00E7a \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\nfunction readStorage(storage: 'session' | 'local', key: string): string | null {\n try {\n const s = storage === 'session' ? sessionStorage : localStorage\n return s.getItem(key)\n } catch {\n return null\n }\n}\n\nfunction writeStorage(storage: 'session' | 'local', key: string, value: string): void {\n try {\n const s = storage === 'session' ? sessionStorage : localStorage\n s.setItem(key, value)\n } catch {\n // Modo privativo / storage bloqueado \u2014 o contexto degrada, o evento segue.\n }\n}\n\nfunction randomId(): string {\n try {\n return crypto.randomUUID()\n } catch {\n return `hol-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`\n }\n}\n\n/** ID da sess\u00E3o de browser \u00B7 vive enquanto a aba viver. */\nexport function resolveSessionId(): string {\n const existing = readStorage('session', SESSION_STORAGE_KEY)\n if (existing) return existing\n const fresh = randomId()\n writeStorage('session', SESSION_STORAGE_KEY, fresh)\n return fresh\n}\n\n/**\n * Atribui\u00E7\u00E3o da sess\u00E3o: utm da primeira p\u00E1gina + referrer externo de entrada.\n * Gravada uma vez e repetida em todo evento da sess\u00E3o.\n */\nexport function resolveAttribution(href: string, referrer: string, currentHost: string): WebContext {\n const stored = readStorage('session', ATTRIBUTION_STORAGE_KEY)\n if (stored) {\n try {\n return JSON.parse(stored) as WebContext\n } catch {\n // Valor corrompido \u2014 recalcula por cima.\n }\n }\n\n const attribution: WebContext = { ...parseUtm(href) }\n\n const referrerHost = hostOf(referrer)\n // Referrer interno n\u00E3o \u00E9 origem de tr\u00E1fego, \u00E9 navega\u00E7\u00E3o.\n if (referrerHost && referrerHost !== currentHost) {\n const clean = stripUrl(referrer)\n if (clean) attribution.referrer = clean\n attribution.referrer_host = referrerHost\n }\n\n writeStorage('session', ATTRIBUTION_STORAGE_KEY, JSON.stringify(attribution))\n return attribution\n}\n\nfunction hostOf(href: string): string | null {\n try {\n return new URL(href).hostname.toLowerCase() || null\n } catch {\n return null\n }\n}\n\n/* \u2500\u2500 Montagem \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/**\n * Contexto completo do evento atual. Fora do browser devolve `{}` \u2014 o SDK\n * server tem o pr\u00F3prio caminho e n\u00E3o deve inventar ambiente.\n */\nexport function buildWebContext(): WebContext {\n if (typeof window === 'undefined' || typeof location === 'undefined') return {}\n\n const href = location.href\n const referrer = typeof document !== 'undefined' ? document.referrer : ''\n const host = location.hostname.toLowerCase()\n\n const ua = typeof navigator !== 'undefined' ? navigator.userAgent : ''\n const { browser, os, device } = parseUserAgent(ua)\n\n const url = stripUrl(href)\n\n return {\n ...(url ? { url } : {}),\n path: location.pathname || '/',\n host,\n ...(browser ? { browser } : {}),\n ...(os ? { os } : {}),\n ...(device ? { device } : {}),\n ...(typeof navigator !== 'undefined' && navigator.language\n ? { locale: navigator.language }\n : {}),\n session_id: resolveSessionId(),\n ...resolveAttribution(href, referrer, host),\n }\n}\n", "/**\n * @holdyapp/sdk/web \u00B7 browser (source: sdk_web).\n *\n * anonymous_id gerado e persistido em localStorage (holdy_anon_id) \u2014 enviado em\n * todo event at\u00E9 identify() promover pra user_id (identity graph faz o merge\n * downstream, nunca o SDK). Flush no pagehide via fetch keepalive.\n *\n * Todo evento carrega o contexto de ambiente (url, path, referrer externo,\n * utm da sess\u00E3o, browser, os, device, locale, session_id) em `context` \u2014\n * separado das properties do produto. Pageview \u00E9 autom\u00E1tico por default:\n * carregamento inicial e cada mudan\u00E7a de rota, inclusive em SPA.\n */\n\nimport { HoldyCore } from './core'\nimport { installAutoCapture, type NavigationKind } from './auto-capture'\nimport { buildWebContext } from './web-context'\nimport type { HoldyOptions, Properties } from './types'\n\nconst ANON_STORAGE_KEY = 'holdy_anon_id'\n\nexport interface HoldyWebOptions extends HoldyOptions {\n /**\n * Emitir `$pageview` sozinho no load e a cada mudan\u00E7a de rota \u00B7 default true.\n * Desligue s\u00F3 se o produto j\u00E1 chama `page()` na m\u00E3o em cada tela.\n */\n autoCapture?: boolean\n}\n\nexport class HoldyWeb extends HoldyCore {\n private teardownAutoCapture: (() => void) | null = null\n\n constructor(opts: HoldyWebOptions) {\n super('sdk_web', opts)\n this.anonymousId = resolveAnonymousId()\n\n if (typeof window !== 'undefined') {\n window.addEventListener('pagehide', () => void this.flush())\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'hidden') void this.flush()\n })\n }\n\n if (opts.autoCapture !== false) {\n this.teardownAutoCapture = installAutoCapture({\n onNavigate: (kind: NavigationKind) => this.page(undefined, { navigation: kind }),\n })\n }\n }\n\n /**\n * Pageview. O `$` marca evento emitido pelo SDK, n\u00E3o pelo produto (mesma\n * conven\u00E7\u00E3o de `$identify`/`$group`); a canonicaliza\u00E7\u00E3o downstream remove o\n * sigilo, ent\u00E3o `$pageview` e `pageview` s\u00E3o o mesmo evento na leitura.\n * url/path/referrer/utm j\u00E1 v\u00EAm no context de todo evento \u2014 aqui s\u00F3 entra o\n * que \u00E9 da p\u00E1gina como conte\u00FAdo (nome e t\u00EDtulo).\n */\n page(name?: string, properties?: Properties): void {\n this.track('$pageview', {\n ...(properties ?? {}),\n ...(name ? { page_name: name } : {}),\n ...(typeof document !== 'undefined' && document.title ? { title: document.title } : {}),\n })\n }\n\n reset(): void {\n this.userId = null\n this.accountKey = null\n try {\n localStorage.removeItem(ANON_STORAGE_KEY)\n } catch {\n // storage indispon\u00EDvel (SSR/privacy mode) \u2014 ok\n }\n this.anonymousId = resolveAnonymousId()\n }\n\n /** Flush final + desinstala o auto-capture (devolve `history` ao original). */\n override async shutdown(): Promise<void> {\n this.teardownAutoCapture?.()\n this.teardownAutoCapture = null\n await super.shutdown()\n }\n\n protected override environmentContext(): Properties {\n return buildWebContext()\n }\n}\n\nfunction resolveAnonymousId(): string {\n try {\n const existing = localStorage.getItem(ANON_STORAGE_KEY)\n if (existing) return existing\n const fresh = crypto.randomUUID()\n localStorage.setItem(ANON_STORAGE_KEY, fresh)\n return fresh\n } catch {\n return crypto.randomUUID()\n }\n}\n\nlet singleton: HoldyWeb | null = null\n\n/** Init idempotente \u2014 segunda chamada retorna a inst\u00E2ncia existente. */\nexport function initHoldy(opts: HoldyWebOptions): HoldyWeb {\n if (!singleton) singleton = new HoldyWeb(opts)\n return singleton\n}\n\nexport type { HoldyOptions, Properties }\n"],
|
|
4
|
+
"sourcesContent": ["/**\n * Entrada do bundle IIFE \u00B7 global `Holdy` pra instala\u00E7\u00E3o por `<script>`.\n *\n * Existe pro cliente que n\u00E3o tem bundler (ou n\u00E3o quer mexer no build) poder\n * instrumentar colando duas linhas no HTML. A API \u00E9 a mesma de `@holdyapp/sdk/web`\n * em forma de fun\u00E7\u00F5es soltas, porque `Holdy.track(...)` \u00E9 o que se espera de um\n * global.\n *\n * Chamar antes de `init` n\u00E3o lan\u00E7a: telemetria n\u00E3o derruba a p\u00E1gina do cliente.\n */\n\nimport { HoldyWeb, type HoldyWebOptions } from './web'\nimport type { Properties } from './types'\n\nlet instance: HoldyWeb | null = null\n\nexport function init(opts: HoldyWebOptions): HoldyWeb {\n // Idempotente: script duplicado na p\u00E1gina n\u00E3o duplica evento.\n if (!instance) instance = new HoldyWeb(opts)\n return instance\n}\n\nexport function track(name: string, properties?: Properties): void {\n instance?.track(name, properties)\n}\n\nexport function identify(userId: string, traits?: Properties): void {\n instance?.identify(userId, traits)\n}\n\nexport function group(accountKey: string, traits?: Properties): void {\n instance?.group(accountKey, traits)\n}\n\nexport function page(name?: string, properties?: Properties): void {\n instance?.page(name, properties)\n}\n\nexport function reset(): void {\n instance?.reset()\n}\n\nexport async function flush(): Promise<void> {\n await instance?.flush()\n}\n\nexport async function shutdown(): Promise<void> {\n await instance?.shutdown()\n instance = null\n}\n\n/** Inst\u00E2ncia corrente \u00B7 \u00FAtil pra depurar no console do browser. */\nexport function getInstance(): HoldyWeb | null {\n return instance\n}\n", "/**\n * Core do @holdyapp/sdk \u00B7 fila + flush pra POST /api/v1/events/bulk.\n *\n * Retry de batch inteiro \u00E9 seguro (partial accept + dedupe server-side).\n * Fila \u00E9 bounded: acima de MAX_QUEUE dropa os mais antigos (telemetria nunca\n * derruba o app do cliente \u2014 nenhum m\u00E9todo p\u00FAblico lan\u00E7a).\n */\n\nimport type { CanonicalEvent, HoldyOptions, Properties, TrackOverrides } from './types'\n\nexport const DEFAULT_BASE_URL = 'https://holdy.chat/api/v1'\nconst DEFAULT_FLUSH_AT = 20\nconst DEFAULT_FLUSH_INTERVAL_MS = 5000\nconst MAX_QUEUE = 1000\nconst BULK_LIMIT = 500\n\nexport class HoldyCore {\n private readonly apiKey: string\n private readonly baseUrl: string\n private readonly flushAt: number\n private readonly debug: boolean\n private readonly source: 'sdk_web' | 'sdk_server'\n\n private queue: CanonicalEvent[] = []\n private timer: ReturnType<typeof setInterval> | null = null\n private flushing: Promise<void> | null = null\n\n protected userId: string | null = null\n protected anonymousId: string | null = null\n protected accountKey: string | null = null\n\n constructor(source: 'sdk_web' | 'sdk_server', opts: HoldyOptions) {\n this.source = source\n this.apiKey = opts.apiKey\n this.accountKey = opts.accountKey ?? null\n this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, '')\n this.flushAt = Math.max(1, opts.flushAt ?? DEFAULT_FLUSH_AT)\n this.debug = opts.debug ?? false\n\n const intervalMs = opts.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS\n if (intervalMs > 0) {\n this.timer = setInterval(() => void this.flush(), intervalMs)\n // Node: n\u00E3o segura o processo vivo s\u00F3 pelo timer do SDK.\n if (typeof (this.timer as { unref?: () => void }).unref === 'function') {\n ;(this.timer as unknown as { unref: () => void }).unref()\n }\n }\n }\n\n track(name: string, properties?: Properties, overrides?: TrackOverrides): void {\n if (!name || typeof name !== 'string') return\n\n const userId = overrides?.userId ?? this.userId ?? undefined\n const accountKey = overrides?.accountKey ?? this.accountKey ?? undefined\n const occurredAt = overrides?.occurredAt\n ? new Date(overrides.occurredAt).toISOString()\n : new Date().toISOString()\n\n // Ordem: ambiente observado pelo SDK \u2192 context da chamada \u2192 carrier de\n // account. O que o caller passou vence o inferido; o carrier vence tudo,\n // porque \u00E9 o que a Data layer usa pra resolver a conta.\n const context: Properties = {\n ...this.environmentContext(),\n ...(overrides?.context ?? {}),\n }\n if (accountKey) context.account_external_id = accountKey\n\n const event: CanonicalEvent = {\n event: name,\n occurred_at: occurredAt,\n source: this.source,\n ...(userId ? { user_id: userId } : {}),\n ...(this.anonymousId ? { anonymous_id: this.anonymousId } : {}),\n properties: properties ?? {},\n ...(Object.keys(context).length > 0 ? { context } : {}),\n }\n\n this.queue.push(event)\n if (this.queue.length > MAX_QUEUE) this.queue.splice(0, this.queue.length - MAX_QUEUE)\n if (this.queue.length >= this.flushAt) void this.flush()\n }\n\n identify(userId: string, traits?: Properties): void {\n if (!userId || typeof userId !== 'string') return\n this.userId = userId\n this.track('$identify', traits ?? {})\n }\n\n group(accountKey: string, traits?: Properties): void {\n if (!accountKey || typeof accountKey !== 'string') return\n this.accountKey = accountKey\n this.track('$group', traits ?? {})\n }\n\n /** Drena a fila. Nunca lan\u00E7a \u2014 falha re-enfileira e loga em debug. */\n async flush(): Promise<void> {\n // Serializa flushes concorrentes (timer + flushAt + manual).\n if (this.flushing) return this.flushing\n if (this.queue.length === 0) return\n\n this.flushing = this.drain().finally(() => {\n this.flushing = null\n })\n return this.flushing\n }\n\n /** Flush final + para o timer. Chamar em shutdown do server / unload do web. */\n async shutdown(): Promise<void> {\n if (this.timer) {\n clearInterval(this.timer)\n this.timer = null\n }\n await this.flush()\n }\n\n private async drain(): Promise<void> {\n while (this.queue.length > 0) {\n const batch = this.queue.splice(0, BULK_LIMIT)\n const ok = await this.send(batch)\n if (!ok) {\n // Devolve pra frente da fila e para \u2014 pr\u00F3ximo flush re-tenta (dedupe-safe).\n this.queue.unshift(...batch)\n return\n }\n }\n }\n\n private async send(events: CanonicalEvent[]): Promise<boolean> {\n try {\n const res = await fetch(`${this.baseUrl}/events/bulk`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ events }),\n ...(this.source === 'sdk_web' ? { keepalive: true } : {}),\n })\n if (!res.ok) {\n // 4xx: batch estruturalmente inv\u00E1lido n\u00E3o re-tenta (evita loop infinito).\n if (res.status >= 400 && res.status < 500) {\n this.log(`bulk rejected ${res.status} \u00B7 batch dropped`)\n return true\n }\n this.log(`bulk failed ${res.status} \u00B7 will retry`)\n return false\n }\n return true\n } catch (err) {\n this.log(`bulk network error: ${(err as Error).message} \u00B7 will retry`)\n return false\n }\n }\n\n private log(msg: string): void {\n if (this.debug) console.warn(`[holdy] ${msg}`)\n }\n\n /**\n * Ambiente que o SDK observa sozinho (url, referrer, browser, utm\u2026).\n * Vazio no core; `HoldyWeb` preenche. O server n\u00E3o infere ambiente \u2014 n\u00E3o\n * existe \"p\u00E1gina atual\" num worker.\n */\n protected environmentContext(): Properties {\n return {}\n }\n\n /** Interno \u00B7 exposto pra testes. */\n get pending(): number {\n return this.queue.length\n }\n}\n", "/**\n * Auto-capture de navega\u00E7\u00E3o \u00B7 @holdyapp/sdk/web.\n *\n * Emite pageview no carregamento e a cada mudan\u00E7a de rota, inclusive nas que\n * n\u00E3o recarregam a p\u00E1gina (SPA). Sem isto, instrumentar exige que o cliente\n * chame `page()` em cada tela \u2014 e o primeiro dado s\u00F3 aparece depois de um\n * trabalho que ele ainda n\u00E3o fez.\n *\n * Idempotente: reinstalar \u00E9 no-op (guard global). O retorno desfaz o patch de\n * `history`, ent\u00E3o `destroy()` devolve o browser como estava.\n */\n\nconst INSTALLED_FLAG = '__holdy_autocapture__'\n\n/** pushState seguido de replaceState \u00E9 uma mudan\u00E7a l\u00F3gica s\u00F3. */\nconst ROUTE_DEBOUNCE_MS = 50\n\nexport type NavigationKind = 'initial_load' | 'route_change'\n\nexport interface AutoCaptureOptions {\n onNavigate: (kind: NavigationKind) => void\n}\n\ninterface GlobalWithFlag {\n [INSTALLED_FLAG]?: boolean\n}\n\nexport function installAutoCapture(opts: AutoCaptureOptions): () => void {\n if (typeof window === 'undefined' || typeof history === 'undefined') {\n return () => undefined\n }\n\n const w = window as unknown as GlobalWithFlag\n if (w[INSTALLED_FLAG]) return () => undefined\n w[INSTALLED_FLAG] = true\n\n const cleanups: Array<() => void> = []\n let lastPath = currentPath()\n let pending: ReturnType<typeof setTimeout> | null = null\n\n function emit(kind: NavigationKind): void {\n try {\n opts.onNavigate(kind)\n } catch {\n // Telemetria nunca derruba a app do cliente.\n }\n }\n\n function scheduleRouteEmit(): void {\n if (pending) clearTimeout(pending)\n pending = setTimeout(() => {\n pending = null\n const path = currentPath()\n // S\u00F3 conta como pageview se a rota mudou de fato \u2014 replaceState de\n // estado interno (filtro, scroll) n\u00E3o \u00E9 navega\u00E7\u00E3o.\n if (path === lastPath) return\n lastPath = path\n emit('route_change')\n }, ROUTE_DEBOUNCE_MS)\n }\n\n emit('initial_load')\n\n const originalPush = history.pushState\n const originalReplace = history.replaceState\n\n history.pushState = function patchedPush(...args: Parameters<History['pushState']>) {\n const result = originalPush.apply(this, args)\n scheduleRouteEmit()\n return result\n }\n history.replaceState = function patchedReplace(...args: Parameters<History['replaceState']>) {\n const result = originalReplace.apply(this, args)\n scheduleRouteEmit()\n return result\n }\n cleanups.push(() => {\n history.pushState = originalPush\n history.replaceState = originalReplace\n })\n\n // Voltar/avan\u00E7ar do browser e routers baseados em hash.\n const onPop = (): void => scheduleRouteEmit()\n window.addEventListener('popstate', onPop)\n cleanups.push(() => window.removeEventListener('popstate', onPop))\n\n const onHash = (): void => scheduleRouteEmit()\n window.addEventListener('hashchange', onHash)\n cleanups.push(() => window.removeEventListener('hashchange', onHash))\n\n return () => {\n if (pending) clearTimeout(pending)\n for (const cleanup of cleanups) cleanup()\n w[INSTALLED_FLAG] = false\n }\n}\n\n/** Rota compar\u00E1vel \u00B7 path + query, sem hash de \u00E2ncora. */\nfunction currentPath(): string {\n if (typeof location === 'undefined') return '/'\n return `${location.pathname}${location.search}${location.hash}`\n}\n", "/**\n * Contexto de ambiente do browser \u00B7 @holdyapp/sdk/web.\n *\n * Vai em `context` de todo evento, nunca em `properties`: properties \u00E9 o\n * vocabul\u00E1rio do produto do cliente (`workflow_key`, `plan`), context \u00E9 o que o\n * SDK observou sozinho. A Data layer persiste os dois (facts.context, 00247) e\n * a leitura de dimens\u00E3o consulta os dois.\n *\n * Tr\u00EAs decis\u00F5es que definem se esse dado \u00E9 \u00FAtil ou lixo:\n * \u00B7 `url` sai sem query e sem hash. Href cru tem uma varia\u00E7\u00E3o por sess\u00E3o, e\n * dimens\u00E3o com cardinalidade quase-\u00FAnica \u00E9 descartada na leitura \u2014 a\n * p\u00E1gina vira ru\u00EDdo exatamente por excesso de precis\u00E3o.\n * \u00B7 utm \u00E9 capturado na primeira p\u00E1gina e carregado pelo resto da sess\u00E3o.\n * Atribui\u00E7\u00E3o que s\u00F3 existe no primeiro evento n\u00E3o responde nada.\n * \u00B7 referrer s\u00F3 quando \u00E9 externo. Navega\u00E7\u00E3o interna preenchendo \"Origem do\n * tr\u00E1fego\" com o pr\u00F3prio dom\u00EDnio esconde a origem real.\n *\n * Tudo aqui \u00E9 best-effort: browser em modo privativo, SSR e webview sem\n * storage precisam funcionar. Nenhuma fun\u00E7\u00E3o lan\u00E7a.\n */\n\nexport type WebContext = Record<string, string>\n\nconst UTM_KEYS = [\n 'utm_source',\n 'utm_medium',\n 'utm_campaign',\n 'utm_term',\n 'utm_content',\n] as const\n\nconst SESSION_STORAGE_KEY = 'holdy_session'\nconst ATTRIBUTION_STORAGE_KEY = 'holdy_attribution'\n\n/* \u2500\u2500 URL \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/** `https://app.x.com/a/b?q=1#z` \u2192 `https://app.x.com/a/b`. */\nexport function stripUrl(href: string): string | null {\n try {\n const url = new URL(href)\n return `${url.origin}${url.pathname}`\n } catch {\n return null\n }\n}\n\n/** Par\u00E2metros de campanha presentes na URL, j\u00E1 em lowercase. */\nexport function parseUtm(href: string): WebContext {\n const out: WebContext = {}\n try {\n const params = new URL(href).searchParams\n for (const key of UTM_KEYS) {\n const value = params.get(key)?.trim()\n if (value) out[key] = value.toLowerCase().slice(0, 120)\n }\n } catch {\n // href inv\u00E1lido \u2014 sem utm, sem drama.\n }\n return out\n}\n\n/* \u2500\u2500 User agent \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\ninterface UaParts {\n browser?: string\n os?: string\n device?: string\n}\n\n/**\n * Fatia m\u00EDnima do UA: navegador, sistema e classe de dispositivo.\n *\n * N\u00E3o \u00E9 biblioteca de UA parsing e n\u00E3o deve virar uma \u2014 o valor est\u00E1 em\n * separar mobile de desktop e Safari de Chrome, n\u00E3o em cravar a vers\u00E3o.\n */\nexport function parseUserAgent(ua: string): UaParts {\n if (!ua) return {}\n const s = ua.toLowerCase()\n\n const browser = s.includes('edg/')\n ? 'Edge'\n : s.includes('opr/') || s.includes('opera')\n ? 'Opera'\n : s.includes('samsungbrowser')\n ? 'Samsung Internet'\n : s.includes('firefox') || s.includes('fxios')\n ? 'Firefox'\n : s.includes('chrome') || s.includes('crios')\n ? 'Chrome'\n : s.includes('safari')\n ? 'Safari'\n : undefined\n\n const os = s.includes('iphone') || s.includes('ipad') || s.includes('ipod')\n ? 'iOS'\n : s.includes('android')\n ? 'Android'\n : s.includes('windows')\n ? 'Windows'\n : s.includes('mac os') || s.includes('macintosh')\n ? 'macOS'\n : s.includes('cros')\n ? 'ChromeOS'\n : s.includes('linux')\n ? 'Linux'\n : undefined\n\n // iPad moderno se declara macOS; a pista que sobra \u00E9 o suporte a toque, que\n // o parser puro n\u00E3o v\u00EA. Tablet aqui cobre o caso declarado no UA.\n const device = s.includes('ipad') || (s.includes('android') && !s.includes('mobile'))\n ? 'tablet'\n : s.includes('mobi') || s.includes('iphone') || s.includes('ipod')\n ? 'mobile'\n : 'desktop'\n\n return {\n ...(browser ? { browser } : {}),\n ...(os ? { os } : {}),\n device,\n }\n}\n\n/* \u2500\u2500 Storage \u00B7 nunca lan\u00E7a \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\nfunction readStorage(storage: 'session' | 'local', key: string): string | null {\n try {\n const s = storage === 'session' ? sessionStorage : localStorage\n return s.getItem(key)\n } catch {\n return null\n }\n}\n\nfunction writeStorage(storage: 'session' | 'local', key: string, value: string): void {\n try {\n const s = storage === 'session' ? sessionStorage : localStorage\n s.setItem(key, value)\n } catch {\n // Modo privativo / storage bloqueado \u2014 o contexto degrada, o evento segue.\n }\n}\n\nfunction randomId(): string {\n try {\n return crypto.randomUUID()\n } catch {\n return `hol-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`\n }\n}\n\n/** ID da sess\u00E3o de browser \u00B7 vive enquanto a aba viver. */\nexport function resolveSessionId(): string {\n const existing = readStorage('session', SESSION_STORAGE_KEY)\n if (existing) return existing\n const fresh = randomId()\n writeStorage('session', SESSION_STORAGE_KEY, fresh)\n return fresh\n}\n\n/**\n * Atribui\u00E7\u00E3o da sess\u00E3o: utm da primeira p\u00E1gina + referrer externo de entrada.\n * Gravada uma vez e repetida em todo evento da sess\u00E3o.\n */\nexport function resolveAttribution(href: string, referrer: string, currentHost: string): WebContext {\n const stored = readStorage('session', ATTRIBUTION_STORAGE_KEY)\n if (stored) {\n try {\n return JSON.parse(stored) as WebContext\n } catch {\n // Valor corrompido \u2014 recalcula por cima.\n }\n }\n\n const attribution: WebContext = { ...parseUtm(href) }\n\n const referrerHost = hostOf(referrer)\n // Referrer interno n\u00E3o \u00E9 origem de tr\u00E1fego, \u00E9 navega\u00E7\u00E3o.\n if (referrerHost && referrerHost !== currentHost) {\n const clean = stripUrl(referrer)\n if (clean) attribution.referrer = clean\n attribution.referrer_host = referrerHost\n }\n\n writeStorage('session', ATTRIBUTION_STORAGE_KEY, JSON.stringify(attribution))\n return attribution\n}\n\nfunction hostOf(href: string): string | null {\n try {\n return new URL(href).hostname.toLowerCase() || null\n } catch {\n return null\n }\n}\n\n/* \u2500\u2500 Montagem \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/**\n * Contexto completo do evento atual. Fora do browser devolve `{}` \u2014 o SDK\n * server tem o pr\u00F3prio caminho e n\u00E3o deve inventar ambiente.\n */\nexport function buildWebContext(): WebContext {\n if (typeof window === 'undefined' || typeof location === 'undefined') return {}\n\n const href = location.href\n const referrer = typeof document !== 'undefined' ? document.referrer : ''\n const host = location.hostname.toLowerCase()\n\n const ua = typeof navigator !== 'undefined' ? navigator.userAgent : ''\n const { browser, os, device } = parseUserAgent(ua)\n\n const url = stripUrl(href)\n\n return {\n ...(url ? { url } : {}),\n path: location.pathname || '/',\n host,\n ...(browser ? { browser } : {}),\n ...(os ? { os } : {}),\n ...(device ? { device } : {}),\n ...(typeof navigator !== 'undefined' && navigator.language\n ? { locale: navigator.language }\n : {}),\n session_id: resolveSessionId(),\n ...resolveAttribution(href, referrer, host),\n }\n}\n", "/**\n * @holdyapp/sdk/web \u00B7 browser (source: sdk_web).\n *\n * anonymous_id gerado e persistido em localStorage (holdy_anon_id) \u2014 enviado em\n * todo event at\u00E9 identify() promover pra user_id (identity graph faz o merge\n * downstream, nunca o SDK). Flush no pagehide via fetch keepalive.\n *\n * Todo evento carrega o contexto de ambiente (url, path, referrer externo,\n * utm da sess\u00E3o, browser, os, device, locale, session_id) em `context` \u2014\n * separado das properties do produto. Pageview \u00E9 autom\u00E1tico por default:\n * carregamento inicial e cada mudan\u00E7a de rota, inclusive em SPA.\n */\n\nimport { HoldyCore } from './core'\nimport { installAutoCapture, type NavigationKind } from './auto-capture'\nimport { buildWebContext } from './web-context'\nimport type { HoldyOptions, Properties } from './types'\n\nconst ANON_STORAGE_KEY = 'holdy_anon_id'\n\nexport interface HoldyWebOptions extends HoldyOptions {\n /**\n * Emitir `$pageview` sozinho no load e a cada mudan\u00E7a de rota \u00B7 default true.\n * Desligue s\u00F3 se o produto j\u00E1 chama `page()` na m\u00E3o em cada tela.\n */\n autoCapture?: boolean\n}\n\nexport class HoldyWeb extends HoldyCore {\n private teardownAutoCapture: (() => void) | null = null\n\n constructor(opts: HoldyWebOptions) {\n super('sdk_web', opts)\n this.anonymousId = resolveAnonymousId()\n\n if (typeof window !== 'undefined') {\n window.addEventListener('pagehide', () => void this.flush())\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'hidden') void this.flush()\n })\n }\n\n if (opts.autoCapture !== false) {\n this.teardownAutoCapture = installAutoCapture({\n onNavigate: (kind: NavigationKind) => this.page(undefined, { navigation: kind }),\n })\n }\n }\n\n /**\n * Pageview. O `$` marca evento emitido pelo SDK, n\u00E3o pelo produto (mesma\n * conven\u00E7\u00E3o de `$identify`/`$group`); a canonicaliza\u00E7\u00E3o downstream remove o\n * sigilo, ent\u00E3o `$pageview` e `pageview` s\u00E3o o mesmo evento na leitura.\n * url/path/referrer/utm j\u00E1 v\u00EAm no context de todo evento \u2014 aqui s\u00F3 entra o\n * que \u00E9 da p\u00E1gina como conte\u00FAdo (nome e t\u00EDtulo).\n */\n page(name?: string, properties?: Properties): void {\n this.track('$pageview', {\n ...(properties ?? {}),\n ...(name ? { page_name: name } : {}),\n ...(typeof document !== 'undefined' && document.title ? { title: document.title } : {}),\n })\n }\n\n reset(): void {\n this.userId = null\n this.accountKey = null\n try {\n localStorage.removeItem(ANON_STORAGE_KEY)\n } catch {\n // storage indispon\u00EDvel (SSR/privacy mode) \u2014 ok\n }\n this.anonymousId = resolveAnonymousId()\n }\n\n /** Flush final + desinstala o auto-capture (devolve `history` ao original). */\n override async shutdown(): Promise<void> {\n this.teardownAutoCapture?.()\n this.teardownAutoCapture = null\n await super.shutdown()\n }\n\n protected override environmentContext(): Properties {\n return buildWebContext()\n }\n}\n\nfunction resolveAnonymousId(): string {\n try {\n const existing = localStorage.getItem(ANON_STORAGE_KEY)\n if (existing) return existing\n const fresh = crypto.randomUUID()\n localStorage.setItem(ANON_STORAGE_KEY, fresh)\n return fresh\n } catch {\n return crypto.randomUUID()\n }\n}\n\nlet singleton: HoldyWeb | null = null\n\n/** Init idempotente \u2014 segunda chamada retorna a inst\u00E2ncia existente. */\nexport function initHoldy(opts: HoldyWebOptions): HoldyWeb {\n if (!singleton) {\n singleton = new HoldyWeb(opts)\n // Inst\u00E2ncia no window: track() inserido pelo `--instrument` do CLI n\u00E3o\n // precisa importar nada nem saber onde o init mora. Mesma superf\u00EDcie do\n // loader por script tag, ent\u00E3o o snippet \u00E9 um s\u00F3 nos dois caminhos.\n if (typeof window !== 'undefined') {\n const w = window as unknown as { Holdy?: HoldyWeb }\n if (!w.Holdy) w.Holdy = singleton\n }\n }\n return singleton\n}\n\nexport type { HoldyOptions, Properties }\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": ["browser_exports", "__export", "flush", "getInstance", "group", "identify", "init", "page", "reset", "shutdown", "track", "DEFAULT_BASE_URL", "HoldyCore", "source", "opts", "DEFAULT_BASE_URL", "intervalMs", "name", "properties", "overrides", "userId", "accountKey", "occurredAt", "context", "event", "traits", "batch", "events", "res", "err", "msg", "INSTALLED_FLAG", "installAutoCapture", "opts", "w", "INSTALLED_FLAG", "cleanups", "lastPath", "currentPath", "pending", "emit", "kind", "scheduleRouteEmit", "path", "originalPush", "originalReplace", "args", "result", "onPop", "onHash", "cleanup", "UTM_KEYS", "SESSION_STORAGE_KEY", "ATTRIBUTION_STORAGE_KEY", "stripUrl", "href", "url", "parseUtm", "out", "params", "key", "value", "parseUserAgent", "ua", "s", "browser", "os", "device", "readStorage", "storage", "writeStorage", "randomId", "resolveSessionId", "existing", "fresh", "resolveAttribution", "referrer", "currentHost", "stored", "attribution", "referrerHost", "hostOf", "clean", "buildWebContext", "host", "ANON_STORAGE_KEY", "HoldyWeb", "HoldyCore", "opts", "resolveAnonymousId", "installAutoCapture", "kind", "name", "properties", "buildWebContext", "existing", "fresh", "instance", "init", "opts", "HoldyWeb", "track", "name", "properties", "identify", "userId", "traits", "group", "accountKey", "page", "reset", "flush", "shutdown", "getInstance"]
|
|
7
7
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -416,7 +416,13 @@ function resolveAnonymousId() {
|
|
|
416
416
|
}
|
|
417
417
|
var singleton = null;
|
|
418
418
|
function initHoldy(opts) {
|
|
419
|
-
if (!singleton)
|
|
419
|
+
if (!singleton) {
|
|
420
|
+
singleton = new HoldyWeb(opts);
|
|
421
|
+
if (typeof window !== "undefined") {
|
|
422
|
+
const w = window;
|
|
423
|
+
if (!w.Holdy) w.Holdy = singleton;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
420
426
|
return singleton;
|
|
421
427
|
}
|
|
422
428
|
// Annotate the CommonJS export names for ESM import in node:
|
package/dist/index.cjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts", "../src/core.ts", "../src/server.ts", "../src/auto-capture.ts", "../src/web-context.ts", "../src/web.ts"],
|
|
4
|
-
"sourcesContent": ["export { Holdy, HoldyCore, DEFAULT_BASE_URL } from './server'\nexport { HoldyWeb, initHoldy } from './web'\nexport type { HoldyWebOptions } from './web'\nexport type { HoldyOptions, Properties, TrackOverrides, CanonicalEvent } from './types'\n", "/**\n * Core do @holdyapp/sdk \u00B7 fila + flush pra POST /api/v1/events/bulk.\n *\n * Retry de batch inteiro \u00E9 seguro (partial accept + dedupe server-side).\n * Fila \u00E9 bounded: acima de MAX_QUEUE dropa os mais antigos (telemetria nunca\n * derruba o app do cliente \u2014 nenhum m\u00E9todo p\u00FAblico lan\u00E7a).\n */\n\nimport type { CanonicalEvent, HoldyOptions, Properties, TrackOverrides } from './types'\n\nexport const DEFAULT_BASE_URL = 'https://holdy.chat/api/v1'\nconst DEFAULT_FLUSH_AT = 20\nconst DEFAULT_FLUSH_INTERVAL_MS = 5000\nconst MAX_QUEUE = 1000\nconst BULK_LIMIT = 500\n\nexport class HoldyCore {\n private readonly apiKey: string\n private readonly baseUrl: string\n private readonly flushAt: number\n private readonly debug: boolean\n private readonly source: 'sdk_web' | 'sdk_server'\n\n private queue: CanonicalEvent[] = []\n private timer: ReturnType<typeof setInterval> | null = null\n private flushing: Promise<void> | null = null\n\n protected userId: string | null = null\n protected anonymousId: string | null = null\n protected accountKey: string | null = null\n\n constructor(source: 'sdk_web' | 'sdk_server', opts: HoldyOptions) {\n this.source = source\n this.apiKey = opts.apiKey\n this.accountKey = opts.accountKey ?? null\n this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, '')\n this.flushAt = Math.max(1, opts.flushAt ?? DEFAULT_FLUSH_AT)\n this.debug = opts.debug ?? false\n\n const intervalMs = opts.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS\n if (intervalMs > 0) {\n this.timer = setInterval(() => void this.flush(), intervalMs)\n // Node: n\u00E3o segura o processo vivo s\u00F3 pelo timer do SDK.\n if (typeof (this.timer as { unref?: () => void }).unref === 'function') {\n ;(this.timer as unknown as { unref: () => void }).unref()\n }\n }\n }\n\n track(name: string, properties?: Properties, overrides?: TrackOverrides): void {\n if (!name || typeof name !== 'string') return\n\n const userId = overrides?.userId ?? this.userId ?? undefined\n const accountKey = overrides?.accountKey ?? this.accountKey ?? undefined\n const occurredAt = overrides?.occurredAt\n ? new Date(overrides.occurredAt).toISOString()\n : new Date().toISOString()\n\n // Ordem: ambiente observado pelo SDK \u2192 context da chamada \u2192 carrier de\n // account. O que o caller passou vence o inferido; o carrier vence tudo,\n // porque \u00E9 o que a Data layer usa pra resolver a conta.\n const context: Properties = {\n ...this.environmentContext(),\n ...(overrides?.context ?? {}),\n }\n if (accountKey) context.account_external_id = accountKey\n\n const event: CanonicalEvent = {\n event: name,\n occurred_at: occurredAt,\n source: this.source,\n ...(userId ? { user_id: userId } : {}),\n ...(this.anonymousId ? { anonymous_id: this.anonymousId } : {}),\n properties: properties ?? {},\n ...(Object.keys(context).length > 0 ? { context } : {}),\n }\n\n this.queue.push(event)\n if (this.queue.length > MAX_QUEUE) this.queue.splice(0, this.queue.length - MAX_QUEUE)\n if (this.queue.length >= this.flushAt) void this.flush()\n }\n\n identify(userId: string, traits?: Properties): void {\n if (!userId || typeof userId !== 'string') return\n this.userId = userId\n this.track('$identify', traits ?? {})\n }\n\n group(accountKey: string, traits?: Properties): void {\n if (!accountKey || typeof accountKey !== 'string') return\n this.accountKey = accountKey\n this.track('$group', traits ?? {})\n }\n\n /** Drena a fila. Nunca lan\u00E7a \u2014 falha re-enfileira e loga em debug. */\n async flush(): Promise<void> {\n // Serializa flushes concorrentes (timer + flushAt + manual).\n if (this.flushing) return this.flushing\n if (this.queue.length === 0) return\n\n this.flushing = this.drain().finally(() => {\n this.flushing = null\n })\n return this.flushing\n }\n\n /** Flush final + para o timer. Chamar em shutdown do server / unload do web. */\n async shutdown(): Promise<void> {\n if (this.timer) {\n clearInterval(this.timer)\n this.timer = null\n }\n await this.flush()\n }\n\n private async drain(): Promise<void> {\n while (this.queue.length > 0) {\n const batch = this.queue.splice(0, BULK_LIMIT)\n const ok = await this.send(batch)\n if (!ok) {\n // Devolve pra frente da fila e para \u2014 pr\u00F3ximo flush re-tenta (dedupe-safe).\n this.queue.unshift(...batch)\n return\n }\n }\n }\n\n private async send(events: CanonicalEvent[]): Promise<boolean> {\n try {\n const res = await fetch(`${this.baseUrl}/events/bulk`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ events }),\n ...(this.source === 'sdk_web' ? { keepalive: true } : {}),\n })\n if (!res.ok) {\n // 4xx: batch estruturalmente inv\u00E1lido n\u00E3o re-tenta (evita loop infinito).\n if (res.status >= 400 && res.status < 500) {\n this.log(`bulk rejected ${res.status} \u00B7 batch dropped`)\n return true\n }\n this.log(`bulk failed ${res.status} \u00B7 will retry`)\n return false\n }\n return true\n } catch (err) {\n this.log(`bulk network error: ${(err as Error).message} \u00B7 will retry`)\n return false\n }\n }\n\n private log(msg: string): void {\n if (this.debug) console.warn(`[holdy] ${msg}`)\n }\n\n /**\n * Ambiente que o SDK observa sozinho (url, referrer, browser, utm\u2026).\n * Vazio no core; `HoldyWeb` preenche. O server n\u00E3o infere ambiente \u2014 n\u00E3o\n * existe \"p\u00E1gina atual\" num worker.\n */\n protected environmentContext(): Properties {\n return {}\n }\n\n /** Interno \u00B7 exposto pra testes. */\n get pending(): number {\n return this.queue.length\n }\n}\n", "/**\n * @holdyapp/sdk (server) \u00B7 Node/Edge (source: sdk_server).\n *\n * Stateless por default: user_id/accountKey v\u00E3o por chamada em overrides.\n * identify()/group() de sess\u00E3o existem mas em server multi-tenant prefira\n * overrides \u2014 estado compartilhado entre requests vaza identidade.\n */\n\nimport { HoldyCore } from './core'\nimport type { HoldyOptions } from './types'\n\nexport class Holdy extends HoldyCore {\n constructor(opts: HoldyOptions) {\n super('sdk_server', opts)\n }\n}\n\nexport { HoldyCore, DEFAULT_BASE_URL } from './core'\nexport type { HoldyOptions, Properties, TrackOverrides, CanonicalEvent } from './types'\n", "/**\n * Auto-capture de navega\u00E7\u00E3o \u00B7 @holdyapp/sdk/web.\n *\n * Emite pageview no carregamento e a cada mudan\u00E7a de rota, inclusive nas que\n * n\u00E3o recarregam a p\u00E1gina (SPA). Sem isto, instrumentar exige que o cliente\n * chame `page()` em cada tela \u2014 e o primeiro dado s\u00F3 aparece depois de um\n * trabalho que ele ainda n\u00E3o fez.\n *\n * Idempotente: reinstalar \u00E9 no-op (guard global). O retorno desfaz o patch de\n * `history`, ent\u00E3o `destroy()` devolve o browser como estava.\n */\n\nconst INSTALLED_FLAG = '__holdy_autocapture__'\n\n/** pushState seguido de replaceState \u00E9 uma mudan\u00E7a l\u00F3gica s\u00F3. */\nconst ROUTE_DEBOUNCE_MS = 50\n\nexport type NavigationKind = 'initial_load' | 'route_change'\n\nexport interface AutoCaptureOptions {\n onNavigate: (kind: NavigationKind) => void\n}\n\ninterface GlobalWithFlag {\n [INSTALLED_FLAG]?: boolean\n}\n\nexport function installAutoCapture(opts: AutoCaptureOptions): () => void {\n if (typeof window === 'undefined' || typeof history === 'undefined') {\n return () => undefined\n }\n\n const w = window as unknown as GlobalWithFlag\n if (w[INSTALLED_FLAG]) return () => undefined\n w[INSTALLED_FLAG] = true\n\n const cleanups: Array<() => void> = []\n let lastPath = currentPath()\n let pending: ReturnType<typeof setTimeout> | null = null\n\n function emit(kind: NavigationKind): void {\n try {\n opts.onNavigate(kind)\n } catch {\n // Telemetria nunca derruba a app do cliente.\n }\n }\n\n function scheduleRouteEmit(): void {\n if (pending) clearTimeout(pending)\n pending = setTimeout(() => {\n pending = null\n const path = currentPath()\n // S\u00F3 conta como pageview se a rota mudou de fato \u2014 replaceState de\n // estado interno (filtro, scroll) n\u00E3o \u00E9 navega\u00E7\u00E3o.\n if (path === lastPath) return\n lastPath = path\n emit('route_change')\n }, ROUTE_DEBOUNCE_MS)\n }\n\n emit('initial_load')\n\n const originalPush = history.pushState\n const originalReplace = history.replaceState\n\n history.pushState = function patchedPush(...args: Parameters<History['pushState']>) {\n const result = originalPush.apply(this, args)\n scheduleRouteEmit()\n return result\n }\n history.replaceState = function patchedReplace(...args: Parameters<History['replaceState']>) {\n const result = originalReplace.apply(this, args)\n scheduleRouteEmit()\n return result\n }\n cleanups.push(() => {\n history.pushState = originalPush\n history.replaceState = originalReplace\n })\n\n // Voltar/avan\u00E7ar do browser e routers baseados em hash.\n const onPop = (): void => scheduleRouteEmit()\n window.addEventListener('popstate', onPop)\n cleanups.push(() => window.removeEventListener('popstate', onPop))\n\n const onHash = (): void => scheduleRouteEmit()\n window.addEventListener('hashchange', onHash)\n cleanups.push(() => window.removeEventListener('hashchange', onHash))\n\n return () => {\n if (pending) clearTimeout(pending)\n for (const cleanup of cleanups) cleanup()\n w[INSTALLED_FLAG] = false\n }\n}\n\n/** Rota compar\u00E1vel \u00B7 path + query, sem hash de \u00E2ncora. */\nfunction currentPath(): string {\n if (typeof location === 'undefined') return '/'\n return `${location.pathname}${location.search}${location.hash}`\n}\n", "/**\n * Contexto de ambiente do browser \u00B7 @holdyapp/sdk/web.\n *\n * Vai em `context` de todo evento, nunca em `properties`: properties \u00E9 o\n * vocabul\u00E1rio do produto do cliente (`workflow_key`, `plan`), context \u00E9 o que o\n * SDK observou sozinho. A Data layer persiste os dois (facts.context, 00247) e\n * a leitura de dimens\u00E3o consulta os dois.\n *\n * Tr\u00EAs decis\u00F5es que definem se esse dado \u00E9 \u00FAtil ou lixo:\n * \u00B7 `url` sai sem query e sem hash. Href cru tem uma varia\u00E7\u00E3o por sess\u00E3o, e\n * dimens\u00E3o com cardinalidade quase-\u00FAnica \u00E9 descartada na leitura \u2014 a\n * p\u00E1gina vira ru\u00EDdo exatamente por excesso de precis\u00E3o.\n * \u00B7 utm \u00E9 capturado na primeira p\u00E1gina e carregado pelo resto da sess\u00E3o.\n * Atribui\u00E7\u00E3o que s\u00F3 existe no primeiro evento n\u00E3o responde nada.\n * \u00B7 referrer s\u00F3 quando \u00E9 externo. Navega\u00E7\u00E3o interna preenchendo \"Origem do\n * tr\u00E1fego\" com o pr\u00F3prio dom\u00EDnio esconde a origem real.\n *\n * Tudo aqui \u00E9 best-effort: browser em modo privativo, SSR e webview sem\n * storage precisam funcionar. Nenhuma fun\u00E7\u00E3o lan\u00E7a.\n */\n\nexport type WebContext = Record<string, string>\n\nconst UTM_KEYS = [\n 'utm_source',\n 'utm_medium',\n 'utm_campaign',\n 'utm_term',\n 'utm_content',\n] as const\n\nconst SESSION_STORAGE_KEY = 'holdy_session'\nconst ATTRIBUTION_STORAGE_KEY = 'holdy_attribution'\n\n/* \u2500\u2500 URL \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/** `https://app.x.com/a/b?q=1#z` \u2192 `https://app.x.com/a/b`. */\nexport function stripUrl(href: string): string | null {\n try {\n const url = new URL(href)\n return `${url.origin}${url.pathname}`\n } catch {\n return null\n }\n}\n\n/** Par\u00E2metros de campanha presentes na URL, j\u00E1 em lowercase. */\nexport function parseUtm(href: string): WebContext {\n const out: WebContext = {}\n try {\n const params = new URL(href).searchParams\n for (const key of UTM_KEYS) {\n const value = params.get(key)?.trim()\n if (value) out[key] = value.toLowerCase().slice(0, 120)\n }\n } catch {\n // href inv\u00E1lido \u2014 sem utm, sem drama.\n }\n return out\n}\n\n/* \u2500\u2500 User agent \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\ninterface UaParts {\n browser?: string\n os?: string\n device?: string\n}\n\n/**\n * Fatia m\u00EDnima do UA: navegador, sistema e classe de dispositivo.\n *\n * N\u00E3o \u00E9 biblioteca de UA parsing e n\u00E3o deve virar uma \u2014 o valor est\u00E1 em\n * separar mobile de desktop e Safari de Chrome, n\u00E3o em cravar a vers\u00E3o.\n */\nexport function parseUserAgent(ua: string): UaParts {\n if (!ua) return {}\n const s = ua.toLowerCase()\n\n const browser = s.includes('edg/')\n ? 'Edge'\n : s.includes('opr/') || s.includes('opera')\n ? 'Opera'\n : s.includes('samsungbrowser')\n ? 'Samsung Internet'\n : s.includes('firefox') || s.includes('fxios')\n ? 'Firefox'\n : s.includes('chrome') || s.includes('crios')\n ? 'Chrome'\n : s.includes('safari')\n ? 'Safari'\n : undefined\n\n const os = s.includes('iphone') || s.includes('ipad') || s.includes('ipod')\n ? 'iOS'\n : s.includes('android')\n ? 'Android'\n : s.includes('windows')\n ? 'Windows'\n : s.includes('mac os') || s.includes('macintosh')\n ? 'macOS'\n : s.includes('cros')\n ? 'ChromeOS'\n : s.includes('linux')\n ? 'Linux'\n : undefined\n\n // iPad moderno se declara macOS; a pista que sobra \u00E9 o suporte a toque, que\n // o parser puro n\u00E3o v\u00EA. Tablet aqui cobre o caso declarado no UA.\n const device = s.includes('ipad') || (s.includes('android') && !s.includes('mobile'))\n ? 'tablet'\n : s.includes('mobi') || s.includes('iphone') || s.includes('ipod')\n ? 'mobile'\n : 'desktop'\n\n return {\n ...(browser ? { browser } : {}),\n ...(os ? { os } : {}),\n device,\n }\n}\n\n/* \u2500\u2500 Storage \u00B7 nunca lan\u00E7a \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\nfunction readStorage(storage: 'session' | 'local', key: string): string | null {\n try {\n const s = storage === 'session' ? sessionStorage : localStorage\n return s.getItem(key)\n } catch {\n return null\n }\n}\n\nfunction writeStorage(storage: 'session' | 'local', key: string, value: string): void {\n try {\n const s = storage === 'session' ? sessionStorage : localStorage\n s.setItem(key, value)\n } catch {\n // Modo privativo / storage bloqueado \u2014 o contexto degrada, o evento segue.\n }\n}\n\nfunction randomId(): string {\n try {\n return crypto.randomUUID()\n } catch {\n return `hol-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`\n }\n}\n\n/** ID da sess\u00E3o de browser \u00B7 vive enquanto a aba viver. */\nexport function resolveSessionId(): string {\n const existing = readStorage('session', SESSION_STORAGE_KEY)\n if (existing) return existing\n const fresh = randomId()\n writeStorage('session', SESSION_STORAGE_KEY, fresh)\n return fresh\n}\n\n/**\n * Atribui\u00E7\u00E3o da sess\u00E3o: utm da primeira p\u00E1gina + referrer externo de entrada.\n * Gravada uma vez e repetida em todo evento da sess\u00E3o.\n */\nexport function resolveAttribution(href: string, referrer: string, currentHost: string): WebContext {\n const stored = readStorage('session', ATTRIBUTION_STORAGE_KEY)\n if (stored) {\n try {\n return JSON.parse(stored) as WebContext\n } catch {\n // Valor corrompido \u2014 recalcula por cima.\n }\n }\n\n const attribution: WebContext = { ...parseUtm(href) }\n\n const referrerHost = hostOf(referrer)\n // Referrer interno n\u00E3o \u00E9 origem de tr\u00E1fego, \u00E9 navega\u00E7\u00E3o.\n if (referrerHost && referrerHost !== currentHost) {\n const clean = stripUrl(referrer)\n if (clean) attribution.referrer = clean\n attribution.referrer_host = referrerHost\n }\n\n writeStorage('session', ATTRIBUTION_STORAGE_KEY, JSON.stringify(attribution))\n return attribution\n}\n\nfunction hostOf(href: string): string | null {\n try {\n return new URL(href).hostname.toLowerCase() || null\n } catch {\n return null\n }\n}\n\n/* \u2500\u2500 Montagem \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/**\n * Contexto completo do evento atual. Fora do browser devolve `{}` \u2014 o SDK\n * server tem o pr\u00F3prio caminho e n\u00E3o deve inventar ambiente.\n */\nexport function buildWebContext(): WebContext {\n if (typeof window === 'undefined' || typeof location === 'undefined') return {}\n\n const href = location.href\n const referrer = typeof document !== 'undefined' ? document.referrer : ''\n const host = location.hostname.toLowerCase()\n\n const ua = typeof navigator !== 'undefined' ? navigator.userAgent : ''\n const { browser, os, device } = parseUserAgent(ua)\n\n const url = stripUrl(href)\n\n return {\n ...(url ? { url } : {}),\n path: location.pathname || '/',\n host,\n ...(browser ? { browser } : {}),\n ...(os ? { os } : {}),\n ...(device ? { device } : {}),\n ...(typeof navigator !== 'undefined' && navigator.language\n ? { locale: navigator.language }\n : {}),\n session_id: resolveSessionId(),\n ...resolveAttribution(href, referrer, host),\n }\n}\n", "/**\n * @holdyapp/sdk/web \u00B7 browser (source: sdk_web).\n *\n * anonymous_id gerado e persistido em localStorage (holdy_anon_id) \u2014 enviado em\n * todo event at\u00E9 identify() promover pra user_id (identity graph faz o merge\n * downstream, nunca o SDK). Flush no pagehide via fetch keepalive.\n *\n * Todo evento carrega o contexto de ambiente (url, path, referrer externo,\n * utm da sess\u00E3o, browser, os, device, locale, session_id) em `context` \u2014\n * separado das properties do produto. Pageview \u00E9 autom\u00E1tico por default:\n * carregamento inicial e cada mudan\u00E7a de rota, inclusive em SPA.\n */\n\nimport { HoldyCore } from './core'\nimport { installAutoCapture, type NavigationKind } from './auto-capture'\nimport { buildWebContext } from './web-context'\nimport type { HoldyOptions, Properties } from './types'\n\nconst ANON_STORAGE_KEY = 'holdy_anon_id'\n\nexport interface HoldyWebOptions extends HoldyOptions {\n /**\n * Emitir `$pageview` sozinho no load e a cada mudan\u00E7a de rota \u00B7 default true.\n * Desligue s\u00F3 se o produto j\u00E1 chama `page()` na m\u00E3o em cada tela.\n */\n autoCapture?: boolean\n}\n\nexport class HoldyWeb extends HoldyCore {\n private teardownAutoCapture: (() => void) | null = null\n\n constructor(opts: HoldyWebOptions) {\n super('sdk_web', opts)\n this.anonymousId = resolveAnonymousId()\n\n if (typeof window !== 'undefined') {\n window.addEventListener('pagehide', () => void this.flush())\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'hidden') void this.flush()\n })\n }\n\n if (opts.autoCapture !== false) {\n this.teardownAutoCapture = installAutoCapture({\n onNavigate: (kind: NavigationKind) => this.page(undefined, { navigation: kind }),\n })\n }\n }\n\n /**\n * Pageview. O `$` marca evento emitido pelo SDK, n\u00E3o pelo produto (mesma\n * conven\u00E7\u00E3o de `$identify`/`$group`); a canonicaliza\u00E7\u00E3o downstream remove o\n * sigilo, ent\u00E3o `$pageview` e `pageview` s\u00E3o o mesmo evento na leitura.\n * url/path/referrer/utm j\u00E1 v\u00EAm no context de todo evento \u2014 aqui s\u00F3 entra o\n * que \u00E9 da p\u00E1gina como conte\u00FAdo (nome e t\u00EDtulo).\n */\n page(name?: string, properties?: Properties): void {\n this.track('$pageview', {\n ...(properties ?? {}),\n ...(name ? { page_name: name } : {}),\n ...(typeof document !== 'undefined' && document.title ? { title: document.title } : {}),\n })\n }\n\n reset(): void {\n this.userId = null\n this.accountKey = null\n try {\n localStorage.removeItem(ANON_STORAGE_KEY)\n } catch {\n // storage indispon\u00EDvel (SSR/privacy mode) \u2014 ok\n }\n this.anonymousId = resolveAnonymousId()\n }\n\n /** Flush final + desinstala o auto-capture (devolve `history` ao original). */\n override async shutdown(): Promise<void> {\n this.teardownAutoCapture?.()\n this.teardownAutoCapture = null\n await super.shutdown()\n }\n\n protected override environmentContext(): Properties {\n return buildWebContext()\n }\n}\n\nfunction resolveAnonymousId(): string {\n try {\n const existing = localStorage.getItem(ANON_STORAGE_KEY)\n if (existing) return existing\n const fresh = crypto.randomUUID()\n localStorage.setItem(ANON_STORAGE_KEY, fresh)\n return fresh\n } catch {\n return crypto.randomUUID()\n }\n}\n\nlet singleton: HoldyWeb | null = null\n\n/** Init idempotente \u2014 segunda chamada retorna a inst\u00E2ncia existente. */\nexport function initHoldy(opts: HoldyWebOptions): HoldyWeb {\n if (!singleton) singleton = new HoldyWeb(opts)\n return singleton\n}\n\nexport type { HoldyOptions, Properties }\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["export { Holdy, HoldyCore, DEFAULT_BASE_URL } from './server'\nexport { HoldyWeb, initHoldy } from './web'\nexport type { HoldyWebOptions } from './web'\nexport type { HoldyOptions, Properties, TrackOverrides, CanonicalEvent } from './types'\n", "/**\n * Core do @holdyapp/sdk \u00B7 fila + flush pra POST /api/v1/events/bulk.\n *\n * Retry de batch inteiro \u00E9 seguro (partial accept + dedupe server-side).\n * Fila \u00E9 bounded: acima de MAX_QUEUE dropa os mais antigos (telemetria nunca\n * derruba o app do cliente \u2014 nenhum m\u00E9todo p\u00FAblico lan\u00E7a).\n */\n\nimport type { CanonicalEvent, HoldyOptions, Properties, TrackOverrides } from './types'\n\nexport const DEFAULT_BASE_URL = 'https://holdy.chat/api/v1'\nconst DEFAULT_FLUSH_AT = 20\nconst DEFAULT_FLUSH_INTERVAL_MS = 5000\nconst MAX_QUEUE = 1000\nconst BULK_LIMIT = 500\n\nexport class HoldyCore {\n private readonly apiKey: string\n private readonly baseUrl: string\n private readonly flushAt: number\n private readonly debug: boolean\n private readonly source: 'sdk_web' | 'sdk_server'\n\n private queue: CanonicalEvent[] = []\n private timer: ReturnType<typeof setInterval> | null = null\n private flushing: Promise<void> | null = null\n\n protected userId: string | null = null\n protected anonymousId: string | null = null\n protected accountKey: string | null = null\n\n constructor(source: 'sdk_web' | 'sdk_server', opts: HoldyOptions) {\n this.source = source\n this.apiKey = opts.apiKey\n this.accountKey = opts.accountKey ?? null\n this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, '')\n this.flushAt = Math.max(1, opts.flushAt ?? DEFAULT_FLUSH_AT)\n this.debug = opts.debug ?? false\n\n const intervalMs = opts.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS\n if (intervalMs > 0) {\n this.timer = setInterval(() => void this.flush(), intervalMs)\n // Node: n\u00E3o segura o processo vivo s\u00F3 pelo timer do SDK.\n if (typeof (this.timer as { unref?: () => void }).unref === 'function') {\n ;(this.timer as unknown as { unref: () => void }).unref()\n }\n }\n }\n\n track(name: string, properties?: Properties, overrides?: TrackOverrides): void {\n if (!name || typeof name !== 'string') return\n\n const userId = overrides?.userId ?? this.userId ?? undefined\n const accountKey = overrides?.accountKey ?? this.accountKey ?? undefined\n const occurredAt = overrides?.occurredAt\n ? new Date(overrides.occurredAt).toISOString()\n : new Date().toISOString()\n\n // Ordem: ambiente observado pelo SDK \u2192 context da chamada \u2192 carrier de\n // account. O que o caller passou vence o inferido; o carrier vence tudo,\n // porque \u00E9 o que a Data layer usa pra resolver a conta.\n const context: Properties = {\n ...this.environmentContext(),\n ...(overrides?.context ?? {}),\n }\n if (accountKey) context.account_external_id = accountKey\n\n const event: CanonicalEvent = {\n event: name,\n occurred_at: occurredAt,\n source: this.source,\n ...(userId ? { user_id: userId } : {}),\n ...(this.anonymousId ? { anonymous_id: this.anonymousId } : {}),\n properties: properties ?? {},\n ...(Object.keys(context).length > 0 ? { context } : {}),\n }\n\n this.queue.push(event)\n if (this.queue.length > MAX_QUEUE) this.queue.splice(0, this.queue.length - MAX_QUEUE)\n if (this.queue.length >= this.flushAt) void this.flush()\n }\n\n identify(userId: string, traits?: Properties): void {\n if (!userId || typeof userId !== 'string') return\n this.userId = userId\n this.track('$identify', traits ?? {})\n }\n\n group(accountKey: string, traits?: Properties): void {\n if (!accountKey || typeof accountKey !== 'string') return\n this.accountKey = accountKey\n this.track('$group', traits ?? {})\n }\n\n /** Drena a fila. Nunca lan\u00E7a \u2014 falha re-enfileira e loga em debug. */\n async flush(): Promise<void> {\n // Serializa flushes concorrentes (timer + flushAt + manual).\n if (this.flushing) return this.flushing\n if (this.queue.length === 0) return\n\n this.flushing = this.drain().finally(() => {\n this.flushing = null\n })\n return this.flushing\n }\n\n /** Flush final + para o timer. Chamar em shutdown do server / unload do web. */\n async shutdown(): Promise<void> {\n if (this.timer) {\n clearInterval(this.timer)\n this.timer = null\n }\n await this.flush()\n }\n\n private async drain(): Promise<void> {\n while (this.queue.length > 0) {\n const batch = this.queue.splice(0, BULK_LIMIT)\n const ok = await this.send(batch)\n if (!ok) {\n // Devolve pra frente da fila e para \u2014 pr\u00F3ximo flush re-tenta (dedupe-safe).\n this.queue.unshift(...batch)\n return\n }\n }\n }\n\n private async send(events: CanonicalEvent[]): Promise<boolean> {\n try {\n const res = await fetch(`${this.baseUrl}/events/bulk`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ events }),\n ...(this.source === 'sdk_web' ? { keepalive: true } : {}),\n })\n if (!res.ok) {\n // 4xx: batch estruturalmente inv\u00E1lido n\u00E3o re-tenta (evita loop infinito).\n if (res.status >= 400 && res.status < 500) {\n this.log(`bulk rejected ${res.status} \u00B7 batch dropped`)\n return true\n }\n this.log(`bulk failed ${res.status} \u00B7 will retry`)\n return false\n }\n return true\n } catch (err) {\n this.log(`bulk network error: ${(err as Error).message} \u00B7 will retry`)\n return false\n }\n }\n\n private log(msg: string): void {\n if (this.debug) console.warn(`[holdy] ${msg}`)\n }\n\n /**\n * Ambiente que o SDK observa sozinho (url, referrer, browser, utm\u2026).\n * Vazio no core; `HoldyWeb` preenche. O server n\u00E3o infere ambiente \u2014 n\u00E3o\n * existe \"p\u00E1gina atual\" num worker.\n */\n protected environmentContext(): Properties {\n return {}\n }\n\n /** Interno \u00B7 exposto pra testes. */\n get pending(): number {\n return this.queue.length\n }\n}\n", "/**\n * @holdyapp/sdk (server) \u00B7 Node/Edge (source: sdk_server).\n *\n * Stateless por default: user_id/accountKey v\u00E3o por chamada em overrides.\n * identify()/group() de sess\u00E3o existem mas em server multi-tenant prefira\n * overrides \u2014 estado compartilhado entre requests vaza identidade.\n */\n\nimport { HoldyCore } from './core'\nimport type { HoldyOptions } from './types'\n\nexport class Holdy extends HoldyCore {\n constructor(opts: HoldyOptions) {\n super('sdk_server', opts)\n }\n}\n\nexport { HoldyCore, DEFAULT_BASE_URL } from './core'\nexport type { HoldyOptions, Properties, TrackOverrides, CanonicalEvent } from './types'\n", "/**\n * Auto-capture de navega\u00E7\u00E3o \u00B7 @holdyapp/sdk/web.\n *\n * Emite pageview no carregamento e a cada mudan\u00E7a de rota, inclusive nas que\n * n\u00E3o recarregam a p\u00E1gina (SPA). Sem isto, instrumentar exige que o cliente\n * chame `page()` em cada tela \u2014 e o primeiro dado s\u00F3 aparece depois de um\n * trabalho que ele ainda n\u00E3o fez.\n *\n * Idempotente: reinstalar \u00E9 no-op (guard global). O retorno desfaz o patch de\n * `history`, ent\u00E3o `destroy()` devolve o browser como estava.\n */\n\nconst INSTALLED_FLAG = '__holdy_autocapture__'\n\n/** pushState seguido de replaceState \u00E9 uma mudan\u00E7a l\u00F3gica s\u00F3. */\nconst ROUTE_DEBOUNCE_MS = 50\n\nexport type NavigationKind = 'initial_load' | 'route_change'\n\nexport interface AutoCaptureOptions {\n onNavigate: (kind: NavigationKind) => void\n}\n\ninterface GlobalWithFlag {\n [INSTALLED_FLAG]?: boolean\n}\n\nexport function installAutoCapture(opts: AutoCaptureOptions): () => void {\n if (typeof window === 'undefined' || typeof history === 'undefined') {\n return () => undefined\n }\n\n const w = window as unknown as GlobalWithFlag\n if (w[INSTALLED_FLAG]) return () => undefined\n w[INSTALLED_FLAG] = true\n\n const cleanups: Array<() => void> = []\n let lastPath = currentPath()\n let pending: ReturnType<typeof setTimeout> | null = null\n\n function emit(kind: NavigationKind): void {\n try {\n opts.onNavigate(kind)\n } catch {\n // Telemetria nunca derruba a app do cliente.\n }\n }\n\n function scheduleRouteEmit(): void {\n if (pending) clearTimeout(pending)\n pending = setTimeout(() => {\n pending = null\n const path = currentPath()\n // S\u00F3 conta como pageview se a rota mudou de fato \u2014 replaceState de\n // estado interno (filtro, scroll) n\u00E3o \u00E9 navega\u00E7\u00E3o.\n if (path === lastPath) return\n lastPath = path\n emit('route_change')\n }, ROUTE_DEBOUNCE_MS)\n }\n\n emit('initial_load')\n\n const originalPush = history.pushState\n const originalReplace = history.replaceState\n\n history.pushState = function patchedPush(...args: Parameters<History['pushState']>) {\n const result = originalPush.apply(this, args)\n scheduleRouteEmit()\n return result\n }\n history.replaceState = function patchedReplace(...args: Parameters<History['replaceState']>) {\n const result = originalReplace.apply(this, args)\n scheduleRouteEmit()\n return result\n }\n cleanups.push(() => {\n history.pushState = originalPush\n history.replaceState = originalReplace\n })\n\n // Voltar/avan\u00E7ar do browser e routers baseados em hash.\n const onPop = (): void => scheduleRouteEmit()\n window.addEventListener('popstate', onPop)\n cleanups.push(() => window.removeEventListener('popstate', onPop))\n\n const onHash = (): void => scheduleRouteEmit()\n window.addEventListener('hashchange', onHash)\n cleanups.push(() => window.removeEventListener('hashchange', onHash))\n\n return () => {\n if (pending) clearTimeout(pending)\n for (const cleanup of cleanups) cleanup()\n w[INSTALLED_FLAG] = false\n }\n}\n\n/** Rota compar\u00E1vel \u00B7 path + query, sem hash de \u00E2ncora. */\nfunction currentPath(): string {\n if (typeof location === 'undefined') return '/'\n return `${location.pathname}${location.search}${location.hash}`\n}\n", "/**\n * Contexto de ambiente do browser \u00B7 @holdyapp/sdk/web.\n *\n * Vai em `context` de todo evento, nunca em `properties`: properties \u00E9 o\n * vocabul\u00E1rio do produto do cliente (`workflow_key`, `plan`), context \u00E9 o que o\n * SDK observou sozinho. A Data layer persiste os dois (facts.context, 00247) e\n * a leitura de dimens\u00E3o consulta os dois.\n *\n * Tr\u00EAs decis\u00F5es que definem se esse dado \u00E9 \u00FAtil ou lixo:\n * \u00B7 `url` sai sem query e sem hash. Href cru tem uma varia\u00E7\u00E3o por sess\u00E3o, e\n * dimens\u00E3o com cardinalidade quase-\u00FAnica \u00E9 descartada na leitura \u2014 a\n * p\u00E1gina vira ru\u00EDdo exatamente por excesso de precis\u00E3o.\n * \u00B7 utm \u00E9 capturado na primeira p\u00E1gina e carregado pelo resto da sess\u00E3o.\n * Atribui\u00E7\u00E3o que s\u00F3 existe no primeiro evento n\u00E3o responde nada.\n * \u00B7 referrer s\u00F3 quando \u00E9 externo. Navega\u00E7\u00E3o interna preenchendo \"Origem do\n * tr\u00E1fego\" com o pr\u00F3prio dom\u00EDnio esconde a origem real.\n *\n * Tudo aqui \u00E9 best-effort: browser em modo privativo, SSR e webview sem\n * storage precisam funcionar. Nenhuma fun\u00E7\u00E3o lan\u00E7a.\n */\n\nexport type WebContext = Record<string, string>\n\nconst UTM_KEYS = [\n 'utm_source',\n 'utm_medium',\n 'utm_campaign',\n 'utm_term',\n 'utm_content',\n] as const\n\nconst SESSION_STORAGE_KEY = 'holdy_session'\nconst ATTRIBUTION_STORAGE_KEY = 'holdy_attribution'\n\n/* \u2500\u2500 URL \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/** `https://app.x.com/a/b?q=1#z` \u2192 `https://app.x.com/a/b`. */\nexport function stripUrl(href: string): string | null {\n try {\n const url = new URL(href)\n return `${url.origin}${url.pathname}`\n } catch {\n return null\n }\n}\n\n/** Par\u00E2metros de campanha presentes na URL, j\u00E1 em lowercase. */\nexport function parseUtm(href: string): WebContext {\n const out: WebContext = {}\n try {\n const params = new URL(href).searchParams\n for (const key of UTM_KEYS) {\n const value = params.get(key)?.trim()\n if (value) out[key] = value.toLowerCase().slice(0, 120)\n }\n } catch {\n // href inv\u00E1lido \u2014 sem utm, sem drama.\n }\n return out\n}\n\n/* \u2500\u2500 User agent \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\ninterface UaParts {\n browser?: string\n os?: string\n device?: string\n}\n\n/**\n * Fatia m\u00EDnima do UA: navegador, sistema e classe de dispositivo.\n *\n * N\u00E3o \u00E9 biblioteca de UA parsing e n\u00E3o deve virar uma \u2014 o valor est\u00E1 em\n * separar mobile de desktop e Safari de Chrome, n\u00E3o em cravar a vers\u00E3o.\n */\nexport function parseUserAgent(ua: string): UaParts {\n if (!ua) return {}\n const s = ua.toLowerCase()\n\n const browser = s.includes('edg/')\n ? 'Edge'\n : s.includes('opr/') || s.includes('opera')\n ? 'Opera'\n : s.includes('samsungbrowser')\n ? 'Samsung Internet'\n : s.includes('firefox') || s.includes('fxios')\n ? 'Firefox'\n : s.includes('chrome') || s.includes('crios')\n ? 'Chrome'\n : s.includes('safari')\n ? 'Safari'\n : undefined\n\n const os = s.includes('iphone') || s.includes('ipad') || s.includes('ipod')\n ? 'iOS'\n : s.includes('android')\n ? 'Android'\n : s.includes('windows')\n ? 'Windows'\n : s.includes('mac os') || s.includes('macintosh')\n ? 'macOS'\n : s.includes('cros')\n ? 'ChromeOS'\n : s.includes('linux')\n ? 'Linux'\n : undefined\n\n // iPad moderno se declara macOS; a pista que sobra \u00E9 o suporte a toque, que\n // o parser puro n\u00E3o v\u00EA. Tablet aqui cobre o caso declarado no UA.\n const device = s.includes('ipad') || (s.includes('android') && !s.includes('mobile'))\n ? 'tablet'\n : s.includes('mobi') || s.includes('iphone') || s.includes('ipod')\n ? 'mobile'\n : 'desktop'\n\n return {\n ...(browser ? { browser } : {}),\n ...(os ? { os } : {}),\n device,\n }\n}\n\n/* \u2500\u2500 Storage \u00B7 nunca lan\u00E7a \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\nfunction readStorage(storage: 'session' | 'local', key: string): string | null {\n try {\n const s = storage === 'session' ? sessionStorage : localStorage\n return s.getItem(key)\n } catch {\n return null\n }\n}\n\nfunction writeStorage(storage: 'session' | 'local', key: string, value: string): void {\n try {\n const s = storage === 'session' ? sessionStorage : localStorage\n s.setItem(key, value)\n } catch {\n // Modo privativo / storage bloqueado \u2014 o contexto degrada, o evento segue.\n }\n}\n\nfunction randomId(): string {\n try {\n return crypto.randomUUID()\n } catch {\n return `hol-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`\n }\n}\n\n/** ID da sess\u00E3o de browser \u00B7 vive enquanto a aba viver. */\nexport function resolveSessionId(): string {\n const existing = readStorage('session', SESSION_STORAGE_KEY)\n if (existing) return existing\n const fresh = randomId()\n writeStorage('session', SESSION_STORAGE_KEY, fresh)\n return fresh\n}\n\n/**\n * Atribui\u00E7\u00E3o da sess\u00E3o: utm da primeira p\u00E1gina + referrer externo de entrada.\n * Gravada uma vez e repetida em todo evento da sess\u00E3o.\n */\nexport function resolveAttribution(href: string, referrer: string, currentHost: string): WebContext {\n const stored = readStorage('session', ATTRIBUTION_STORAGE_KEY)\n if (stored) {\n try {\n return JSON.parse(stored) as WebContext\n } catch {\n // Valor corrompido \u2014 recalcula por cima.\n }\n }\n\n const attribution: WebContext = { ...parseUtm(href) }\n\n const referrerHost = hostOf(referrer)\n // Referrer interno n\u00E3o \u00E9 origem de tr\u00E1fego, \u00E9 navega\u00E7\u00E3o.\n if (referrerHost && referrerHost !== currentHost) {\n const clean = stripUrl(referrer)\n if (clean) attribution.referrer = clean\n attribution.referrer_host = referrerHost\n }\n\n writeStorage('session', ATTRIBUTION_STORAGE_KEY, JSON.stringify(attribution))\n return attribution\n}\n\nfunction hostOf(href: string): string | null {\n try {\n return new URL(href).hostname.toLowerCase() || null\n } catch {\n return null\n }\n}\n\n/* \u2500\u2500 Montagem \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/**\n * Contexto completo do evento atual. Fora do browser devolve `{}` \u2014 o SDK\n * server tem o pr\u00F3prio caminho e n\u00E3o deve inventar ambiente.\n */\nexport function buildWebContext(): WebContext {\n if (typeof window === 'undefined' || typeof location === 'undefined') return {}\n\n const href = location.href\n const referrer = typeof document !== 'undefined' ? document.referrer : ''\n const host = location.hostname.toLowerCase()\n\n const ua = typeof navigator !== 'undefined' ? navigator.userAgent : ''\n const { browser, os, device } = parseUserAgent(ua)\n\n const url = stripUrl(href)\n\n return {\n ...(url ? { url } : {}),\n path: location.pathname || '/',\n host,\n ...(browser ? { browser } : {}),\n ...(os ? { os } : {}),\n ...(device ? { device } : {}),\n ...(typeof navigator !== 'undefined' && navigator.language\n ? { locale: navigator.language }\n : {}),\n session_id: resolveSessionId(),\n ...resolveAttribution(href, referrer, host),\n }\n}\n", "/**\n * @holdyapp/sdk/web \u00B7 browser (source: sdk_web).\n *\n * anonymous_id gerado e persistido em localStorage (holdy_anon_id) \u2014 enviado em\n * todo event at\u00E9 identify() promover pra user_id (identity graph faz o merge\n * downstream, nunca o SDK). Flush no pagehide via fetch keepalive.\n *\n * Todo evento carrega o contexto de ambiente (url, path, referrer externo,\n * utm da sess\u00E3o, browser, os, device, locale, session_id) em `context` \u2014\n * separado das properties do produto. Pageview \u00E9 autom\u00E1tico por default:\n * carregamento inicial e cada mudan\u00E7a de rota, inclusive em SPA.\n */\n\nimport { HoldyCore } from './core'\nimport { installAutoCapture, type NavigationKind } from './auto-capture'\nimport { buildWebContext } from './web-context'\nimport type { HoldyOptions, Properties } from './types'\n\nconst ANON_STORAGE_KEY = 'holdy_anon_id'\n\nexport interface HoldyWebOptions extends HoldyOptions {\n /**\n * Emitir `$pageview` sozinho no load e a cada mudan\u00E7a de rota \u00B7 default true.\n * Desligue s\u00F3 se o produto j\u00E1 chama `page()` na m\u00E3o em cada tela.\n */\n autoCapture?: boolean\n}\n\nexport class HoldyWeb extends HoldyCore {\n private teardownAutoCapture: (() => void) | null = null\n\n constructor(opts: HoldyWebOptions) {\n super('sdk_web', opts)\n this.anonymousId = resolveAnonymousId()\n\n if (typeof window !== 'undefined') {\n window.addEventListener('pagehide', () => void this.flush())\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'hidden') void this.flush()\n })\n }\n\n if (opts.autoCapture !== false) {\n this.teardownAutoCapture = installAutoCapture({\n onNavigate: (kind: NavigationKind) => this.page(undefined, { navigation: kind }),\n })\n }\n }\n\n /**\n * Pageview. O `$` marca evento emitido pelo SDK, n\u00E3o pelo produto (mesma\n * conven\u00E7\u00E3o de `$identify`/`$group`); a canonicaliza\u00E7\u00E3o downstream remove o\n * sigilo, ent\u00E3o `$pageview` e `pageview` s\u00E3o o mesmo evento na leitura.\n * url/path/referrer/utm j\u00E1 v\u00EAm no context de todo evento \u2014 aqui s\u00F3 entra o\n * que \u00E9 da p\u00E1gina como conte\u00FAdo (nome e t\u00EDtulo).\n */\n page(name?: string, properties?: Properties): void {\n this.track('$pageview', {\n ...(properties ?? {}),\n ...(name ? { page_name: name } : {}),\n ...(typeof document !== 'undefined' && document.title ? { title: document.title } : {}),\n })\n }\n\n reset(): void {\n this.userId = null\n this.accountKey = null\n try {\n localStorage.removeItem(ANON_STORAGE_KEY)\n } catch {\n // storage indispon\u00EDvel (SSR/privacy mode) \u2014 ok\n }\n this.anonymousId = resolveAnonymousId()\n }\n\n /** Flush final + desinstala o auto-capture (devolve `history` ao original). */\n override async shutdown(): Promise<void> {\n this.teardownAutoCapture?.()\n this.teardownAutoCapture = null\n await super.shutdown()\n }\n\n protected override environmentContext(): Properties {\n return buildWebContext()\n }\n}\n\nfunction resolveAnonymousId(): string {\n try {\n const existing = localStorage.getItem(ANON_STORAGE_KEY)\n if (existing) return existing\n const fresh = crypto.randomUUID()\n localStorage.setItem(ANON_STORAGE_KEY, fresh)\n return fresh\n } catch {\n return crypto.randomUUID()\n }\n}\n\nlet singleton: HoldyWeb | null = null\n\n/** Init idempotente \u2014 segunda chamada retorna a inst\u00E2ncia existente. */\nexport function initHoldy(opts: HoldyWebOptions): HoldyWeb {\n if (!singleton) {\n singleton = new HoldyWeb(opts)\n // Inst\u00E2ncia no window: track() inserido pelo `--instrument` do CLI n\u00E3o\n // precisa importar nada nem saber onde o init mora. Mesma superf\u00EDcie do\n // loader por script tag, ent\u00E3o o snippet \u00E9 um s\u00F3 nos dois caminhos.\n if (typeof window !== 'undefined') {\n const w = window as unknown as { Holdy?: HoldyWeb }\n if (!w.Holdy) w.Holdy = singleton\n }\n }\n return singleton\n}\n\nexport type { HoldyOptions, Properties }\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -386,7 +386,13 @@ function resolveAnonymousId() {
|
|
|
386
386
|
}
|
|
387
387
|
var singleton = null;
|
|
388
388
|
function initHoldy(opts) {
|
|
389
|
-
if (!singleton)
|
|
389
|
+
if (!singleton) {
|
|
390
|
+
singleton = new HoldyWeb(opts);
|
|
391
|
+
if (typeof window !== "undefined") {
|
|
392
|
+
const w = window;
|
|
393
|
+
if (!w.Holdy) w.Holdy = singleton;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
390
396
|
return singleton;
|
|
391
397
|
}
|
|
392
398
|
export {
|
package/dist/index.mjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/core.ts", "../src/server.ts", "../src/auto-capture.ts", "../src/web-context.ts", "../src/web.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Core do @holdyapp/sdk \u00B7 fila + flush pra POST /api/v1/events/bulk.\n *\n * Retry de batch inteiro \u00E9 seguro (partial accept + dedupe server-side).\n * Fila \u00E9 bounded: acima de MAX_QUEUE dropa os mais antigos (telemetria nunca\n * derruba o app do cliente \u2014 nenhum m\u00E9todo p\u00FAblico lan\u00E7a).\n */\n\nimport type { CanonicalEvent, HoldyOptions, Properties, TrackOverrides } from './types'\n\nexport const DEFAULT_BASE_URL = 'https://holdy.chat/api/v1'\nconst DEFAULT_FLUSH_AT = 20\nconst DEFAULT_FLUSH_INTERVAL_MS = 5000\nconst MAX_QUEUE = 1000\nconst BULK_LIMIT = 500\n\nexport class HoldyCore {\n private readonly apiKey: string\n private readonly baseUrl: string\n private readonly flushAt: number\n private readonly debug: boolean\n private readonly source: 'sdk_web' | 'sdk_server'\n\n private queue: CanonicalEvent[] = []\n private timer: ReturnType<typeof setInterval> | null = null\n private flushing: Promise<void> | null = null\n\n protected userId: string | null = null\n protected anonymousId: string | null = null\n protected accountKey: string | null = null\n\n constructor(source: 'sdk_web' | 'sdk_server', opts: HoldyOptions) {\n this.source = source\n this.apiKey = opts.apiKey\n this.accountKey = opts.accountKey ?? null\n this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, '')\n this.flushAt = Math.max(1, opts.flushAt ?? DEFAULT_FLUSH_AT)\n this.debug = opts.debug ?? false\n\n const intervalMs = opts.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS\n if (intervalMs > 0) {\n this.timer = setInterval(() => void this.flush(), intervalMs)\n // Node: n\u00E3o segura o processo vivo s\u00F3 pelo timer do SDK.\n if (typeof (this.timer as { unref?: () => void }).unref === 'function') {\n ;(this.timer as unknown as { unref: () => void }).unref()\n }\n }\n }\n\n track(name: string, properties?: Properties, overrides?: TrackOverrides): void {\n if (!name || typeof name !== 'string') return\n\n const userId = overrides?.userId ?? this.userId ?? undefined\n const accountKey = overrides?.accountKey ?? this.accountKey ?? undefined\n const occurredAt = overrides?.occurredAt\n ? new Date(overrides.occurredAt).toISOString()\n : new Date().toISOString()\n\n // Ordem: ambiente observado pelo SDK \u2192 context da chamada \u2192 carrier de\n // account. O que o caller passou vence o inferido; o carrier vence tudo,\n // porque \u00E9 o que a Data layer usa pra resolver a conta.\n const context: Properties = {\n ...this.environmentContext(),\n ...(overrides?.context ?? {}),\n }\n if (accountKey) context.account_external_id = accountKey\n\n const event: CanonicalEvent = {\n event: name,\n occurred_at: occurredAt,\n source: this.source,\n ...(userId ? { user_id: userId } : {}),\n ...(this.anonymousId ? { anonymous_id: this.anonymousId } : {}),\n properties: properties ?? {},\n ...(Object.keys(context).length > 0 ? { context } : {}),\n }\n\n this.queue.push(event)\n if (this.queue.length > MAX_QUEUE) this.queue.splice(0, this.queue.length - MAX_QUEUE)\n if (this.queue.length >= this.flushAt) void this.flush()\n }\n\n identify(userId: string, traits?: Properties): void {\n if (!userId || typeof userId !== 'string') return\n this.userId = userId\n this.track('$identify', traits ?? {})\n }\n\n group(accountKey: string, traits?: Properties): void {\n if (!accountKey || typeof accountKey !== 'string') return\n this.accountKey = accountKey\n this.track('$group', traits ?? {})\n }\n\n /** Drena a fila. Nunca lan\u00E7a \u2014 falha re-enfileira e loga em debug. */\n async flush(): Promise<void> {\n // Serializa flushes concorrentes (timer + flushAt + manual).\n if (this.flushing) return this.flushing\n if (this.queue.length === 0) return\n\n this.flushing = this.drain().finally(() => {\n this.flushing = null\n })\n return this.flushing\n }\n\n /** Flush final + para o timer. Chamar em shutdown do server / unload do web. */\n async shutdown(): Promise<void> {\n if (this.timer) {\n clearInterval(this.timer)\n this.timer = null\n }\n await this.flush()\n }\n\n private async drain(): Promise<void> {\n while (this.queue.length > 0) {\n const batch = this.queue.splice(0, BULK_LIMIT)\n const ok = await this.send(batch)\n if (!ok) {\n // Devolve pra frente da fila e para \u2014 pr\u00F3ximo flush re-tenta (dedupe-safe).\n this.queue.unshift(...batch)\n return\n }\n }\n }\n\n private async send(events: CanonicalEvent[]): Promise<boolean> {\n try {\n const res = await fetch(`${this.baseUrl}/events/bulk`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ events }),\n ...(this.source === 'sdk_web' ? { keepalive: true } : {}),\n })\n if (!res.ok) {\n // 4xx: batch estruturalmente inv\u00E1lido n\u00E3o re-tenta (evita loop infinito).\n if (res.status >= 400 && res.status < 500) {\n this.log(`bulk rejected ${res.status} \u00B7 batch dropped`)\n return true\n }\n this.log(`bulk failed ${res.status} \u00B7 will retry`)\n return false\n }\n return true\n } catch (err) {\n this.log(`bulk network error: ${(err as Error).message} \u00B7 will retry`)\n return false\n }\n }\n\n private log(msg: string): void {\n if (this.debug) console.warn(`[holdy] ${msg}`)\n }\n\n /**\n * Ambiente que o SDK observa sozinho (url, referrer, browser, utm\u2026).\n * Vazio no core; `HoldyWeb` preenche. O server n\u00E3o infere ambiente \u2014 n\u00E3o\n * existe \"p\u00E1gina atual\" num worker.\n */\n protected environmentContext(): Properties {\n return {}\n }\n\n /** Interno \u00B7 exposto pra testes. */\n get pending(): number {\n return this.queue.length\n }\n}\n", "/**\n * @holdyapp/sdk (server) \u00B7 Node/Edge (source: sdk_server).\n *\n * Stateless por default: user_id/accountKey v\u00E3o por chamada em overrides.\n * identify()/group() de sess\u00E3o existem mas em server multi-tenant prefira\n * overrides \u2014 estado compartilhado entre requests vaza identidade.\n */\n\nimport { HoldyCore } from './core'\nimport type { HoldyOptions } from './types'\n\nexport class Holdy extends HoldyCore {\n constructor(opts: HoldyOptions) {\n super('sdk_server', opts)\n }\n}\n\nexport { HoldyCore, DEFAULT_BASE_URL } from './core'\nexport type { HoldyOptions, Properties, TrackOverrides, CanonicalEvent } from './types'\n", "/**\n * Auto-capture de navega\u00E7\u00E3o \u00B7 @holdyapp/sdk/web.\n *\n * Emite pageview no carregamento e a cada mudan\u00E7a de rota, inclusive nas que\n * n\u00E3o recarregam a p\u00E1gina (SPA). Sem isto, instrumentar exige que o cliente\n * chame `page()` em cada tela \u2014 e o primeiro dado s\u00F3 aparece depois de um\n * trabalho que ele ainda n\u00E3o fez.\n *\n * Idempotente: reinstalar \u00E9 no-op (guard global). O retorno desfaz o patch de\n * `history`, ent\u00E3o `destroy()` devolve o browser como estava.\n */\n\nconst INSTALLED_FLAG = '__holdy_autocapture__'\n\n/** pushState seguido de replaceState \u00E9 uma mudan\u00E7a l\u00F3gica s\u00F3. */\nconst ROUTE_DEBOUNCE_MS = 50\n\nexport type NavigationKind = 'initial_load' | 'route_change'\n\nexport interface AutoCaptureOptions {\n onNavigate: (kind: NavigationKind) => void\n}\n\ninterface GlobalWithFlag {\n [INSTALLED_FLAG]?: boolean\n}\n\nexport function installAutoCapture(opts: AutoCaptureOptions): () => void {\n if (typeof window === 'undefined' || typeof history === 'undefined') {\n return () => undefined\n }\n\n const w = window as unknown as GlobalWithFlag\n if (w[INSTALLED_FLAG]) return () => undefined\n w[INSTALLED_FLAG] = true\n\n const cleanups: Array<() => void> = []\n let lastPath = currentPath()\n let pending: ReturnType<typeof setTimeout> | null = null\n\n function emit(kind: NavigationKind): void {\n try {\n opts.onNavigate(kind)\n } catch {\n // Telemetria nunca derruba a app do cliente.\n }\n }\n\n function scheduleRouteEmit(): void {\n if (pending) clearTimeout(pending)\n pending = setTimeout(() => {\n pending = null\n const path = currentPath()\n // S\u00F3 conta como pageview se a rota mudou de fato \u2014 replaceState de\n // estado interno (filtro, scroll) n\u00E3o \u00E9 navega\u00E7\u00E3o.\n if (path === lastPath) return\n lastPath = path\n emit('route_change')\n }, ROUTE_DEBOUNCE_MS)\n }\n\n emit('initial_load')\n\n const originalPush = history.pushState\n const originalReplace = history.replaceState\n\n history.pushState = function patchedPush(...args: Parameters<History['pushState']>) {\n const result = originalPush.apply(this, args)\n scheduleRouteEmit()\n return result\n }\n history.replaceState = function patchedReplace(...args: Parameters<History['replaceState']>) {\n const result = originalReplace.apply(this, args)\n scheduleRouteEmit()\n return result\n }\n cleanups.push(() => {\n history.pushState = originalPush\n history.replaceState = originalReplace\n })\n\n // Voltar/avan\u00E7ar do browser e routers baseados em hash.\n const onPop = (): void => scheduleRouteEmit()\n window.addEventListener('popstate', onPop)\n cleanups.push(() => window.removeEventListener('popstate', onPop))\n\n const onHash = (): void => scheduleRouteEmit()\n window.addEventListener('hashchange', onHash)\n cleanups.push(() => window.removeEventListener('hashchange', onHash))\n\n return () => {\n if (pending) clearTimeout(pending)\n for (const cleanup of cleanups) cleanup()\n w[INSTALLED_FLAG] = false\n }\n}\n\n/** Rota compar\u00E1vel \u00B7 path + query, sem hash de \u00E2ncora. */\nfunction currentPath(): string {\n if (typeof location === 'undefined') return '/'\n return `${location.pathname}${location.search}${location.hash}`\n}\n", "/**\n * Contexto de ambiente do browser \u00B7 @holdyapp/sdk/web.\n *\n * Vai em `context` de todo evento, nunca em `properties`: properties \u00E9 o\n * vocabul\u00E1rio do produto do cliente (`workflow_key`, `plan`), context \u00E9 o que o\n * SDK observou sozinho. A Data layer persiste os dois (facts.context, 00247) e\n * a leitura de dimens\u00E3o consulta os dois.\n *\n * Tr\u00EAs decis\u00F5es que definem se esse dado \u00E9 \u00FAtil ou lixo:\n * \u00B7 `url` sai sem query e sem hash. Href cru tem uma varia\u00E7\u00E3o por sess\u00E3o, e\n * dimens\u00E3o com cardinalidade quase-\u00FAnica \u00E9 descartada na leitura \u2014 a\n * p\u00E1gina vira ru\u00EDdo exatamente por excesso de precis\u00E3o.\n * \u00B7 utm \u00E9 capturado na primeira p\u00E1gina e carregado pelo resto da sess\u00E3o.\n * Atribui\u00E7\u00E3o que s\u00F3 existe no primeiro evento n\u00E3o responde nada.\n * \u00B7 referrer s\u00F3 quando \u00E9 externo. Navega\u00E7\u00E3o interna preenchendo \"Origem do\n * tr\u00E1fego\" com o pr\u00F3prio dom\u00EDnio esconde a origem real.\n *\n * Tudo aqui \u00E9 best-effort: browser em modo privativo, SSR e webview sem\n * storage precisam funcionar. Nenhuma fun\u00E7\u00E3o lan\u00E7a.\n */\n\nexport type WebContext = Record<string, string>\n\nconst UTM_KEYS = [\n 'utm_source',\n 'utm_medium',\n 'utm_campaign',\n 'utm_term',\n 'utm_content',\n] as const\n\nconst SESSION_STORAGE_KEY = 'holdy_session'\nconst ATTRIBUTION_STORAGE_KEY = 'holdy_attribution'\n\n/* \u2500\u2500 URL \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/** `https://app.x.com/a/b?q=1#z` \u2192 `https://app.x.com/a/b`. */\nexport function stripUrl(href: string): string | null {\n try {\n const url = new URL(href)\n return `${url.origin}${url.pathname}`\n } catch {\n return null\n }\n}\n\n/** Par\u00E2metros de campanha presentes na URL, j\u00E1 em lowercase. */\nexport function parseUtm(href: string): WebContext {\n const out: WebContext = {}\n try {\n const params = new URL(href).searchParams\n for (const key of UTM_KEYS) {\n const value = params.get(key)?.trim()\n if (value) out[key] = value.toLowerCase().slice(0, 120)\n }\n } catch {\n // href inv\u00E1lido \u2014 sem utm, sem drama.\n }\n return out\n}\n\n/* \u2500\u2500 User agent \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\ninterface UaParts {\n browser?: string\n os?: string\n device?: string\n}\n\n/**\n * Fatia m\u00EDnima do UA: navegador, sistema e classe de dispositivo.\n *\n * N\u00E3o \u00E9 biblioteca de UA parsing e n\u00E3o deve virar uma \u2014 o valor est\u00E1 em\n * separar mobile de desktop e Safari de Chrome, n\u00E3o em cravar a vers\u00E3o.\n */\nexport function parseUserAgent(ua: string): UaParts {\n if (!ua) return {}\n const s = ua.toLowerCase()\n\n const browser = s.includes('edg/')\n ? 'Edge'\n : s.includes('opr/') || s.includes('opera')\n ? 'Opera'\n : s.includes('samsungbrowser')\n ? 'Samsung Internet'\n : s.includes('firefox') || s.includes('fxios')\n ? 'Firefox'\n : s.includes('chrome') || s.includes('crios')\n ? 'Chrome'\n : s.includes('safari')\n ? 'Safari'\n : undefined\n\n const os = s.includes('iphone') || s.includes('ipad') || s.includes('ipod')\n ? 'iOS'\n : s.includes('android')\n ? 'Android'\n : s.includes('windows')\n ? 'Windows'\n : s.includes('mac os') || s.includes('macintosh')\n ? 'macOS'\n : s.includes('cros')\n ? 'ChromeOS'\n : s.includes('linux')\n ? 'Linux'\n : undefined\n\n // iPad moderno se declara macOS; a pista que sobra \u00E9 o suporte a toque, que\n // o parser puro n\u00E3o v\u00EA. Tablet aqui cobre o caso declarado no UA.\n const device = s.includes('ipad') || (s.includes('android') && !s.includes('mobile'))\n ? 'tablet'\n : s.includes('mobi') || s.includes('iphone') || s.includes('ipod')\n ? 'mobile'\n : 'desktop'\n\n return {\n ...(browser ? { browser } : {}),\n ...(os ? { os } : {}),\n device,\n }\n}\n\n/* \u2500\u2500 Storage \u00B7 nunca lan\u00E7a \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\nfunction readStorage(storage: 'session' | 'local', key: string): string | null {\n try {\n const s = storage === 'session' ? sessionStorage : localStorage\n return s.getItem(key)\n } catch {\n return null\n }\n}\n\nfunction writeStorage(storage: 'session' | 'local', key: string, value: string): void {\n try {\n const s = storage === 'session' ? sessionStorage : localStorage\n s.setItem(key, value)\n } catch {\n // Modo privativo / storage bloqueado \u2014 o contexto degrada, o evento segue.\n }\n}\n\nfunction randomId(): string {\n try {\n return crypto.randomUUID()\n } catch {\n return `hol-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`\n }\n}\n\n/** ID da sess\u00E3o de browser \u00B7 vive enquanto a aba viver. */\nexport function resolveSessionId(): string {\n const existing = readStorage('session', SESSION_STORAGE_KEY)\n if (existing) return existing\n const fresh = randomId()\n writeStorage('session', SESSION_STORAGE_KEY, fresh)\n return fresh\n}\n\n/**\n * Atribui\u00E7\u00E3o da sess\u00E3o: utm da primeira p\u00E1gina + referrer externo de entrada.\n * Gravada uma vez e repetida em todo evento da sess\u00E3o.\n */\nexport function resolveAttribution(href: string, referrer: string, currentHost: string): WebContext {\n const stored = readStorage('session', ATTRIBUTION_STORAGE_KEY)\n if (stored) {\n try {\n return JSON.parse(stored) as WebContext\n } catch {\n // Valor corrompido \u2014 recalcula por cima.\n }\n }\n\n const attribution: WebContext = { ...parseUtm(href) }\n\n const referrerHost = hostOf(referrer)\n // Referrer interno n\u00E3o \u00E9 origem de tr\u00E1fego, \u00E9 navega\u00E7\u00E3o.\n if (referrerHost && referrerHost !== currentHost) {\n const clean = stripUrl(referrer)\n if (clean) attribution.referrer = clean\n attribution.referrer_host = referrerHost\n }\n\n writeStorage('session', ATTRIBUTION_STORAGE_KEY, JSON.stringify(attribution))\n return attribution\n}\n\nfunction hostOf(href: string): string | null {\n try {\n return new URL(href).hostname.toLowerCase() || null\n } catch {\n return null\n }\n}\n\n/* \u2500\u2500 Montagem \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/**\n * Contexto completo do evento atual. Fora do browser devolve `{}` \u2014 o SDK\n * server tem o pr\u00F3prio caminho e n\u00E3o deve inventar ambiente.\n */\nexport function buildWebContext(): WebContext {\n if (typeof window === 'undefined' || typeof location === 'undefined') return {}\n\n const href = location.href\n const referrer = typeof document !== 'undefined' ? document.referrer : ''\n const host = location.hostname.toLowerCase()\n\n const ua = typeof navigator !== 'undefined' ? navigator.userAgent : ''\n const { browser, os, device } = parseUserAgent(ua)\n\n const url = stripUrl(href)\n\n return {\n ...(url ? { url } : {}),\n path: location.pathname || '/',\n host,\n ...(browser ? { browser } : {}),\n ...(os ? { os } : {}),\n ...(device ? { device } : {}),\n ...(typeof navigator !== 'undefined' && navigator.language\n ? { locale: navigator.language }\n : {}),\n session_id: resolveSessionId(),\n ...resolveAttribution(href, referrer, host),\n }\n}\n", "/**\n * @holdyapp/sdk/web \u00B7 browser (source: sdk_web).\n *\n * anonymous_id gerado e persistido em localStorage (holdy_anon_id) \u2014 enviado em\n * todo event at\u00E9 identify() promover pra user_id (identity graph faz o merge\n * downstream, nunca o SDK). Flush no pagehide via fetch keepalive.\n *\n * Todo evento carrega o contexto de ambiente (url, path, referrer externo,\n * utm da sess\u00E3o, browser, os, device, locale, session_id) em `context` \u2014\n * separado das properties do produto. Pageview \u00E9 autom\u00E1tico por default:\n * carregamento inicial e cada mudan\u00E7a de rota, inclusive em SPA.\n */\n\nimport { HoldyCore } from './core'\nimport { installAutoCapture, type NavigationKind } from './auto-capture'\nimport { buildWebContext } from './web-context'\nimport type { HoldyOptions, Properties } from './types'\n\nconst ANON_STORAGE_KEY = 'holdy_anon_id'\n\nexport interface HoldyWebOptions extends HoldyOptions {\n /**\n * Emitir `$pageview` sozinho no load e a cada mudan\u00E7a de rota \u00B7 default true.\n * Desligue s\u00F3 se o produto j\u00E1 chama `page()` na m\u00E3o em cada tela.\n */\n autoCapture?: boolean\n}\n\nexport class HoldyWeb extends HoldyCore {\n private teardownAutoCapture: (() => void) | null = null\n\n constructor(opts: HoldyWebOptions) {\n super('sdk_web', opts)\n this.anonymousId = resolveAnonymousId()\n\n if (typeof window !== 'undefined') {\n window.addEventListener('pagehide', () => void this.flush())\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'hidden') void this.flush()\n })\n }\n\n if (opts.autoCapture !== false) {\n this.teardownAutoCapture = installAutoCapture({\n onNavigate: (kind: NavigationKind) => this.page(undefined, { navigation: kind }),\n })\n }\n }\n\n /**\n * Pageview. O `$` marca evento emitido pelo SDK, n\u00E3o pelo produto (mesma\n * conven\u00E7\u00E3o de `$identify`/`$group`); a canonicaliza\u00E7\u00E3o downstream remove o\n * sigilo, ent\u00E3o `$pageview` e `pageview` s\u00E3o o mesmo evento na leitura.\n * url/path/referrer/utm j\u00E1 v\u00EAm no context de todo evento \u2014 aqui s\u00F3 entra o\n * que \u00E9 da p\u00E1gina como conte\u00FAdo (nome e t\u00EDtulo).\n */\n page(name?: string, properties?: Properties): void {\n this.track('$pageview', {\n ...(properties ?? {}),\n ...(name ? { page_name: name } : {}),\n ...(typeof document !== 'undefined' && document.title ? { title: document.title } : {}),\n })\n }\n\n reset(): void {\n this.userId = null\n this.accountKey = null\n try {\n localStorage.removeItem(ANON_STORAGE_KEY)\n } catch {\n // storage indispon\u00EDvel (SSR/privacy mode) \u2014 ok\n }\n this.anonymousId = resolveAnonymousId()\n }\n\n /** Flush final + desinstala o auto-capture (devolve `history` ao original). */\n override async shutdown(): Promise<void> {\n this.teardownAutoCapture?.()\n this.teardownAutoCapture = null\n await super.shutdown()\n }\n\n protected override environmentContext(): Properties {\n return buildWebContext()\n }\n}\n\nfunction resolveAnonymousId(): string {\n try {\n const existing = localStorage.getItem(ANON_STORAGE_KEY)\n if (existing) return existing\n const fresh = crypto.randomUUID()\n localStorage.setItem(ANON_STORAGE_KEY, fresh)\n return fresh\n } catch {\n return crypto.randomUUID()\n }\n}\n\nlet singleton: HoldyWeb | null = null\n\n/** Init idempotente \u2014 segunda chamada retorna a inst\u00E2ncia existente. */\nexport function initHoldy(opts: HoldyWebOptions): HoldyWeb {\n if (!singleton) singleton = new HoldyWeb(opts)\n return singleton\n}\n\nexport type { HoldyOptions, Properties }\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["/**\n * Core do @holdyapp/sdk \u00B7 fila + flush pra POST /api/v1/events/bulk.\n *\n * Retry de batch inteiro \u00E9 seguro (partial accept + dedupe server-side).\n * Fila \u00E9 bounded: acima de MAX_QUEUE dropa os mais antigos (telemetria nunca\n * derruba o app do cliente \u2014 nenhum m\u00E9todo p\u00FAblico lan\u00E7a).\n */\n\nimport type { CanonicalEvent, HoldyOptions, Properties, TrackOverrides } from './types'\n\nexport const DEFAULT_BASE_URL = 'https://holdy.chat/api/v1'\nconst DEFAULT_FLUSH_AT = 20\nconst DEFAULT_FLUSH_INTERVAL_MS = 5000\nconst MAX_QUEUE = 1000\nconst BULK_LIMIT = 500\n\nexport class HoldyCore {\n private readonly apiKey: string\n private readonly baseUrl: string\n private readonly flushAt: number\n private readonly debug: boolean\n private readonly source: 'sdk_web' | 'sdk_server'\n\n private queue: CanonicalEvent[] = []\n private timer: ReturnType<typeof setInterval> | null = null\n private flushing: Promise<void> | null = null\n\n protected userId: string | null = null\n protected anonymousId: string | null = null\n protected accountKey: string | null = null\n\n constructor(source: 'sdk_web' | 'sdk_server', opts: HoldyOptions) {\n this.source = source\n this.apiKey = opts.apiKey\n this.accountKey = opts.accountKey ?? null\n this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, '')\n this.flushAt = Math.max(1, opts.flushAt ?? DEFAULT_FLUSH_AT)\n this.debug = opts.debug ?? false\n\n const intervalMs = opts.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS\n if (intervalMs > 0) {\n this.timer = setInterval(() => void this.flush(), intervalMs)\n // Node: n\u00E3o segura o processo vivo s\u00F3 pelo timer do SDK.\n if (typeof (this.timer as { unref?: () => void }).unref === 'function') {\n ;(this.timer as unknown as { unref: () => void }).unref()\n }\n }\n }\n\n track(name: string, properties?: Properties, overrides?: TrackOverrides): void {\n if (!name || typeof name !== 'string') return\n\n const userId = overrides?.userId ?? this.userId ?? undefined\n const accountKey = overrides?.accountKey ?? this.accountKey ?? undefined\n const occurredAt = overrides?.occurredAt\n ? new Date(overrides.occurredAt).toISOString()\n : new Date().toISOString()\n\n // Ordem: ambiente observado pelo SDK \u2192 context da chamada \u2192 carrier de\n // account. O que o caller passou vence o inferido; o carrier vence tudo,\n // porque \u00E9 o que a Data layer usa pra resolver a conta.\n const context: Properties = {\n ...this.environmentContext(),\n ...(overrides?.context ?? {}),\n }\n if (accountKey) context.account_external_id = accountKey\n\n const event: CanonicalEvent = {\n event: name,\n occurred_at: occurredAt,\n source: this.source,\n ...(userId ? { user_id: userId } : {}),\n ...(this.anonymousId ? { anonymous_id: this.anonymousId } : {}),\n properties: properties ?? {},\n ...(Object.keys(context).length > 0 ? { context } : {}),\n }\n\n this.queue.push(event)\n if (this.queue.length > MAX_QUEUE) this.queue.splice(0, this.queue.length - MAX_QUEUE)\n if (this.queue.length >= this.flushAt) void this.flush()\n }\n\n identify(userId: string, traits?: Properties): void {\n if (!userId || typeof userId !== 'string') return\n this.userId = userId\n this.track('$identify', traits ?? {})\n }\n\n group(accountKey: string, traits?: Properties): void {\n if (!accountKey || typeof accountKey !== 'string') return\n this.accountKey = accountKey\n this.track('$group', traits ?? {})\n }\n\n /** Drena a fila. Nunca lan\u00E7a \u2014 falha re-enfileira e loga em debug. */\n async flush(): Promise<void> {\n // Serializa flushes concorrentes (timer + flushAt + manual).\n if (this.flushing) return this.flushing\n if (this.queue.length === 0) return\n\n this.flushing = this.drain().finally(() => {\n this.flushing = null\n })\n return this.flushing\n }\n\n /** Flush final + para o timer. Chamar em shutdown do server / unload do web. */\n async shutdown(): Promise<void> {\n if (this.timer) {\n clearInterval(this.timer)\n this.timer = null\n }\n await this.flush()\n }\n\n private async drain(): Promise<void> {\n while (this.queue.length > 0) {\n const batch = this.queue.splice(0, BULK_LIMIT)\n const ok = await this.send(batch)\n if (!ok) {\n // Devolve pra frente da fila e para \u2014 pr\u00F3ximo flush re-tenta (dedupe-safe).\n this.queue.unshift(...batch)\n return\n }\n }\n }\n\n private async send(events: CanonicalEvent[]): Promise<boolean> {\n try {\n const res = await fetch(`${this.baseUrl}/events/bulk`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ events }),\n ...(this.source === 'sdk_web' ? { keepalive: true } : {}),\n })\n if (!res.ok) {\n // 4xx: batch estruturalmente inv\u00E1lido n\u00E3o re-tenta (evita loop infinito).\n if (res.status >= 400 && res.status < 500) {\n this.log(`bulk rejected ${res.status} \u00B7 batch dropped`)\n return true\n }\n this.log(`bulk failed ${res.status} \u00B7 will retry`)\n return false\n }\n return true\n } catch (err) {\n this.log(`bulk network error: ${(err as Error).message} \u00B7 will retry`)\n return false\n }\n }\n\n private log(msg: string): void {\n if (this.debug) console.warn(`[holdy] ${msg}`)\n }\n\n /**\n * Ambiente que o SDK observa sozinho (url, referrer, browser, utm\u2026).\n * Vazio no core; `HoldyWeb` preenche. O server n\u00E3o infere ambiente \u2014 n\u00E3o\n * existe \"p\u00E1gina atual\" num worker.\n */\n protected environmentContext(): Properties {\n return {}\n }\n\n /** Interno \u00B7 exposto pra testes. */\n get pending(): number {\n return this.queue.length\n }\n}\n", "/**\n * @holdyapp/sdk (server) \u00B7 Node/Edge (source: sdk_server).\n *\n * Stateless por default: user_id/accountKey v\u00E3o por chamada em overrides.\n * identify()/group() de sess\u00E3o existem mas em server multi-tenant prefira\n * overrides \u2014 estado compartilhado entre requests vaza identidade.\n */\n\nimport { HoldyCore } from './core'\nimport type { HoldyOptions } from './types'\n\nexport class Holdy extends HoldyCore {\n constructor(opts: HoldyOptions) {\n super('sdk_server', opts)\n }\n}\n\nexport { HoldyCore, DEFAULT_BASE_URL } from './core'\nexport type { HoldyOptions, Properties, TrackOverrides, CanonicalEvent } from './types'\n", "/**\n * Auto-capture de navega\u00E7\u00E3o \u00B7 @holdyapp/sdk/web.\n *\n * Emite pageview no carregamento e a cada mudan\u00E7a de rota, inclusive nas que\n * n\u00E3o recarregam a p\u00E1gina (SPA). Sem isto, instrumentar exige que o cliente\n * chame `page()` em cada tela \u2014 e o primeiro dado s\u00F3 aparece depois de um\n * trabalho que ele ainda n\u00E3o fez.\n *\n * Idempotente: reinstalar \u00E9 no-op (guard global). O retorno desfaz o patch de\n * `history`, ent\u00E3o `destroy()` devolve o browser como estava.\n */\n\nconst INSTALLED_FLAG = '__holdy_autocapture__'\n\n/** pushState seguido de replaceState \u00E9 uma mudan\u00E7a l\u00F3gica s\u00F3. */\nconst ROUTE_DEBOUNCE_MS = 50\n\nexport type NavigationKind = 'initial_load' | 'route_change'\n\nexport interface AutoCaptureOptions {\n onNavigate: (kind: NavigationKind) => void\n}\n\ninterface GlobalWithFlag {\n [INSTALLED_FLAG]?: boolean\n}\n\nexport function installAutoCapture(opts: AutoCaptureOptions): () => void {\n if (typeof window === 'undefined' || typeof history === 'undefined') {\n return () => undefined\n }\n\n const w = window as unknown as GlobalWithFlag\n if (w[INSTALLED_FLAG]) return () => undefined\n w[INSTALLED_FLAG] = true\n\n const cleanups: Array<() => void> = []\n let lastPath = currentPath()\n let pending: ReturnType<typeof setTimeout> | null = null\n\n function emit(kind: NavigationKind): void {\n try {\n opts.onNavigate(kind)\n } catch {\n // Telemetria nunca derruba a app do cliente.\n }\n }\n\n function scheduleRouteEmit(): void {\n if (pending) clearTimeout(pending)\n pending = setTimeout(() => {\n pending = null\n const path = currentPath()\n // S\u00F3 conta como pageview se a rota mudou de fato \u2014 replaceState de\n // estado interno (filtro, scroll) n\u00E3o \u00E9 navega\u00E7\u00E3o.\n if (path === lastPath) return\n lastPath = path\n emit('route_change')\n }, ROUTE_DEBOUNCE_MS)\n }\n\n emit('initial_load')\n\n const originalPush = history.pushState\n const originalReplace = history.replaceState\n\n history.pushState = function patchedPush(...args: Parameters<History['pushState']>) {\n const result = originalPush.apply(this, args)\n scheduleRouteEmit()\n return result\n }\n history.replaceState = function patchedReplace(...args: Parameters<History['replaceState']>) {\n const result = originalReplace.apply(this, args)\n scheduleRouteEmit()\n return result\n }\n cleanups.push(() => {\n history.pushState = originalPush\n history.replaceState = originalReplace\n })\n\n // Voltar/avan\u00E7ar do browser e routers baseados em hash.\n const onPop = (): void => scheduleRouteEmit()\n window.addEventListener('popstate', onPop)\n cleanups.push(() => window.removeEventListener('popstate', onPop))\n\n const onHash = (): void => scheduleRouteEmit()\n window.addEventListener('hashchange', onHash)\n cleanups.push(() => window.removeEventListener('hashchange', onHash))\n\n return () => {\n if (pending) clearTimeout(pending)\n for (const cleanup of cleanups) cleanup()\n w[INSTALLED_FLAG] = false\n }\n}\n\n/** Rota compar\u00E1vel \u00B7 path + query, sem hash de \u00E2ncora. */\nfunction currentPath(): string {\n if (typeof location === 'undefined') return '/'\n return `${location.pathname}${location.search}${location.hash}`\n}\n", "/**\n * Contexto de ambiente do browser \u00B7 @holdyapp/sdk/web.\n *\n * Vai em `context` de todo evento, nunca em `properties`: properties \u00E9 o\n * vocabul\u00E1rio do produto do cliente (`workflow_key`, `plan`), context \u00E9 o que o\n * SDK observou sozinho. A Data layer persiste os dois (facts.context, 00247) e\n * a leitura de dimens\u00E3o consulta os dois.\n *\n * Tr\u00EAs decis\u00F5es que definem se esse dado \u00E9 \u00FAtil ou lixo:\n * \u00B7 `url` sai sem query e sem hash. Href cru tem uma varia\u00E7\u00E3o por sess\u00E3o, e\n * dimens\u00E3o com cardinalidade quase-\u00FAnica \u00E9 descartada na leitura \u2014 a\n * p\u00E1gina vira ru\u00EDdo exatamente por excesso de precis\u00E3o.\n * \u00B7 utm \u00E9 capturado na primeira p\u00E1gina e carregado pelo resto da sess\u00E3o.\n * Atribui\u00E7\u00E3o que s\u00F3 existe no primeiro evento n\u00E3o responde nada.\n * \u00B7 referrer s\u00F3 quando \u00E9 externo. Navega\u00E7\u00E3o interna preenchendo \"Origem do\n * tr\u00E1fego\" com o pr\u00F3prio dom\u00EDnio esconde a origem real.\n *\n * Tudo aqui \u00E9 best-effort: browser em modo privativo, SSR e webview sem\n * storage precisam funcionar. Nenhuma fun\u00E7\u00E3o lan\u00E7a.\n */\n\nexport type WebContext = Record<string, string>\n\nconst UTM_KEYS = [\n 'utm_source',\n 'utm_medium',\n 'utm_campaign',\n 'utm_term',\n 'utm_content',\n] as const\n\nconst SESSION_STORAGE_KEY = 'holdy_session'\nconst ATTRIBUTION_STORAGE_KEY = 'holdy_attribution'\n\n/* \u2500\u2500 URL \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/** `https://app.x.com/a/b?q=1#z` \u2192 `https://app.x.com/a/b`. */\nexport function stripUrl(href: string): string | null {\n try {\n const url = new URL(href)\n return `${url.origin}${url.pathname}`\n } catch {\n return null\n }\n}\n\n/** Par\u00E2metros de campanha presentes na URL, j\u00E1 em lowercase. */\nexport function parseUtm(href: string): WebContext {\n const out: WebContext = {}\n try {\n const params = new URL(href).searchParams\n for (const key of UTM_KEYS) {\n const value = params.get(key)?.trim()\n if (value) out[key] = value.toLowerCase().slice(0, 120)\n }\n } catch {\n // href inv\u00E1lido \u2014 sem utm, sem drama.\n }\n return out\n}\n\n/* \u2500\u2500 User agent \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\ninterface UaParts {\n browser?: string\n os?: string\n device?: string\n}\n\n/**\n * Fatia m\u00EDnima do UA: navegador, sistema e classe de dispositivo.\n *\n * N\u00E3o \u00E9 biblioteca de UA parsing e n\u00E3o deve virar uma \u2014 o valor est\u00E1 em\n * separar mobile de desktop e Safari de Chrome, n\u00E3o em cravar a vers\u00E3o.\n */\nexport function parseUserAgent(ua: string): UaParts {\n if (!ua) return {}\n const s = ua.toLowerCase()\n\n const browser = s.includes('edg/')\n ? 'Edge'\n : s.includes('opr/') || s.includes('opera')\n ? 'Opera'\n : s.includes('samsungbrowser')\n ? 'Samsung Internet'\n : s.includes('firefox') || s.includes('fxios')\n ? 'Firefox'\n : s.includes('chrome') || s.includes('crios')\n ? 'Chrome'\n : s.includes('safari')\n ? 'Safari'\n : undefined\n\n const os = s.includes('iphone') || s.includes('ipad') || s.includes('ipod')\n ? 'iOS'\n : s.includes('android')\n ? 'Android'\n : s.includes('windows')\n ? 'Windows'\n : s.includes('mac os') || s.includes('macintosh')\n ? 'macOS'\n : s.includes('cros')\n ? 'ChromeOS'\n : s.includes('linux')\n ? 'Linux'\n : undefined\n\n // iPad moderno se declara macOS; a pista que sobra \u00E9 o suporte a toque, que\n // o parser puro n\u00E3o v\u00EA. Tablet aqui cobre o caso declarado no UA.\n const device = s.includes('ipad') || (s.includes('android') && !s.includes('mobile'))\n ? 'tablet'\n : s.includes('mobi') || s.includes('iphone') || s.includes('ipod')\n ? 'mobile'\n : 'desktop'\n\n return {\n ...(browser ? { browser } : {}),\n ...(os ? { os } : {}),\n device,\n }\n}\n\n/* \u2500\u2500 Storage \u00B7 nunca lan\u00E7a \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\nfunction readStorage(storage: 'session' | 'local', key: string): string | null {\n try {\n const s = storage === 'session' ? sessionStorage : localStorage\n return s.getItem(key)\n } catch {\n return null\n }\n}\n\nfunction writeStorage(storage: 'session' | 'local', key: string, value: string): void {\n try {\n const s = storage === 'session' ? sessionStorage : localStorage\n s.setItem(key, value)\n } catch {\n // Modo privativo / storage bloqueado \u2014 o contexto degrada, o evento segue.\n }\n}\n\nfunction randomId(): string {\n try {\n return crypto.randomUUID()\n } catch {\n return `hol-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`\n }\n}\n\n/** ID da sess\u00E3o de browser \u00B7 vive enquanto a aba viver. */\nexport function resolveSessionId(): string {\n const existing = readStorage('session', SESSION_STORAGE_KEY)\n if (existing) return existing\n const fresh = randomId()\n writeStorage('session', SESSION_STORAGE_KEY, fresh)\n return fresh\n}\n\n/**\n * Atribui\u00E7\u00E3o da sess\u00E3o: utm da primeira p\u00E1gina + referrer externo de entrada.\n * Gravada uma vez e repetida em todo evento da sess\u00E3o.\n */\nexport function resolveAttribution(href: string, referrer: string, currentHost: string): WebContext {\n const stored = readStorage('session', ATTRIBUTION_STORAGE_KEY)\n if (stored) {\n try {\n return JSON.parse(stored) as WebContext\n } catch {\n // Valor corrompido \u2014 recalcula por cima.\n }\n }\n\n const attribution: WebContext = { ...parseUtm(href) }\n\n const referrerHost = hostOf(referrer)\n // Referrer interno n\u00E3o \u00E9 origem de tr\u00E1fego, \u00E9 navega\u00E7\u00E3o.\n if (referrerHost && referrerHost !== currentHost) {\n const clean = stripUrl(referrer)\n if (clean) attribution.referrer = clean\n attribution.referrer_host = referrerHost\n }\n\n writeStorage('session', ATTRIBUTION_STORAGE_KEY, JSON.stringify(attribution))\n return attribution\n}\n\nfunction hostOf(href: string): string | null {\n try {\n return new URL(href).hostname.toLowerCase() || null\n } catch {\n return null\n }\n}\n\n/* \u2500\u2500 Montagem \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/**\n * Contexto completo do evento atual. Fora do browser devolve `{}` \u2014 o SDK\n * server tem o pr\u00F3prio caminho e n\u00E3o deve inventar ambiente.\n */\nexport function buildWebContext(): WebContext {\n if (typeof window === 'undefined' || typeof location === 'undefined') return {}\n\n const href = location.href\n const referrer = typeof document !== 'undefined' ? document.referrer : ''\n const host = location.hostname.toLowerCase()\n\n const ua = typeof navigator !== 'undefined' ? navigator.userAgent : ''\n const { browser, os, device } = parseUserAgent(ua)\n\n const url = stripUrl(href)\n\n return {\n ...(url ? { url } : {}),\n path: location.pathname || '/',\n host,\n ...(browser ? { browser } : {}),\n ...(os ? { os } : {}),\n ...(device ? { device } : {}),\n ...(typeof navigator !== 'undefined' && navigator.language\n ? { locale: navigator.language }\n : {}),\n session_id: resolveSessionId(),\n ...resolveAttribution(href, referrer, host),\n }\n}\n", "/**\n * @holdyapp/sdk/web \u00B7 browser (source: sdk_web).\n *\n * anonymous_id gerado e persistido em localStorage (holdy_anon_id) \u2014 enviado em\n * todo event at\u00E9 identify() promover pra user_id (identity graph faz o merge\n * downstream, nunca o SDK). Flush no pagehide via fetch keepalive.\n *\n * Todo evento carrega o contexto de ambiente (url, path, referrer externo,\n * utm da sess\u00E3o, browser, os, device, locale, session_id) em `context` \u2014\n * separado das properties do produto. Pageview \u00E9 autom\u00E1tico por default:\n * carregamento inicial e cada mudan\u00E7a de rota, inclusive em SPA.\n */\n\nimport { HoldyCore } from './core'\nimport { installAutoCapture, type NavigationKind } from './auto-capture'\nimport { buildWebContext } from './web-context'\nimport type { HoldyOptions, Properties } from './types'\n\nconst ANON_STORAGE_KEY = 'holdy_anon_id'\n\nexport interface HoldyWebOptions extends HoldyOptions {\n /**\n * Emitir `$pageview` sozinho no load e a cada mudan\u00E7a de rota \u00B7 default true.\n * Desligue s\u00F3 se o produto j\u00E1 chama `page()` na m\u00E3o em cada tela.\n */\n autoCapture?: boolean\n}\n\nexport class HoldyWeb extends HoldyCore {\n private teardownAutoCapture: (() => void) | null = null\n\n constructor(opts: HoldyWebOptions) {\n super('sdk_web', opts)\n this.anonymousId = resolveAnonymousId()\n\n if (typeof window !== 'undefined') {\n window.addEventListener('pagehide', () => void this.flush())\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'hidden') void this.flush()\n })\n }\n\n if (opts.autoCapture !== false) {\n this.teardownAutoCapture = installAutoCapture({\n onNavigate: (kind: NavigationKind) => this.page(undefined, { navigation: kind }),\n })\n }\n }\n\n /**\n * Pageview. O `$` marca evento emitido pelo SDK, n\u00E3o pelo produto (mesma\n * conven\u00E7\u00E3o de `$identify`/`$group`); a canonicaliza\u00E7\u00E3o downstream remove o\n * sigilo, ent\u00E3o `$pageview` e `pageview` s\u00E3o o mesmo evento na leitura.\n * url/path/referrer/utm j\u00E1 v\u00EAm no context de todo evento \u2014 aqui s\u00F3 entra o\n * que \u00E9 da p\u00E1gina como conte\u00FAdo (nome e t\u00EDtulo).\n */\n page(name?: string, properties?: Properties): void {\n this.track('$pageview', {\n ...(properties ?? {}),\n ...(name ? { page_name: name } : {}),\n ...(typeof document !== 'undefined' && document.title ? { title: document.title } : {}),\n })\n }\n\n reset(): void {\n this.userId = null\n this.accountKey = null\n try {\n localStorage.removeItem(ANON_STORAGE_KEY)\n } catch {\n // storage indispon\u00EDvel (SSR/privacy mode) \u2014 ok\n }\n this.anonymousId = resolveAnonymousId()\n }\n\n /** Flush final + desinstala o auto-capture (devolve `history` ao original). */\n override async shutdown(): Promise<void> {\n this.teardownAutoCapture?.()\n this.teardownAutoCapture = null\n await super.shutdown()\n }\n\n protected override environmentContext(): Properties {\n return buildWebContext()\n }\n}\n\nfunction resolveAnonymousId(): string {\n try {\n const existing = localStorage.getItem(ANON_STORAGE_KEY)\n if (existing) return existing\n const fresh = crypto.randomUUID()\n localStorage.setItem(ANON_STORAGE_KEY, fresh)\n return fresh\n } catch {\n return crypto.randomUUID()\n }\n}\n\nlet singleton: HoldyWeb | null = null\n\n/** Init idempotente \u2014 segunda chamada retorna a inst\u00E2ncia existente. */\nexport function initHoldy(opts: HoldyWebOptions): HoldyWeb {\n if (!singleton) {\n singleton = new HoldyWeb(opts)\n // Inst\u00E2ncia no window: track() inserido pelo `--instrument` do CLI n\u00E3o\n // precisa importar nada nem saber onde o init mora. Mesma superf\u00EDcie do\n // loader por script tag, ent\u00E3o o snippet \u00E9 um s\u00F3 nos dois caminhos.\n if (typeof window !== 'undefined') {\n const w = window as unknown as { Holdy?: HoldyWeb }\n if (!w.Holdy) w.Holdy = singleton\n }\n }\n return singleton\n}\n\nexport type { HoldyOptions, Properties }\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/web.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web.d.ts","sourceRoot":"","sources":["../src/web.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAA;AAGlC,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAIvD,MAAM,WAAW,eAAgB,SAAQ,YAAY;IACnD;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB;AAED,qBAAa,QAAS,SAAQ,SAAS;IACrC,OAAO,CAAC,mBAAmB,CAA4B;gBAE3C,IAAI,EAAE,eAAe;IAkBjC;;;;;;OAMG;IACH,IAAI,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,UAAU,GAAG,IAAI;IAQlD,KAAK,IAAI,IAAI;IAWb,+EAA+E;IAChE,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;cAMrB,kBAAkB,IAAI,UAAU;CAGpD;AAgBD,wEAAwE;AACxE,wBAAgB,SAAS,CAAC,IAAI,EAAE,eAAe,GAAG,QAAQ,
|
|
1
|
+
{"version":3,"file":"web.d.ts","sourceRoot":"","sources":["../src/web.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAA;AAGlC,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAIvD,MAAM,WAAW,eAAgB,SAAQ,YAAY;IACnD;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB;AAED,qBAAa,QAAS,SAAQ,SAAS;IACrC,OAAO,CAAC,mBAAmB,CAA4B;gBAE3C,IAAI,EAAE,eAAe;IAkBjC;;;;;;OAMG;IACH,IAAI,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,UAAU,GAAG,IAAI;IAQlD,KAAK,IAAI,IAAI;IAWb,+EAA+E;IAChE,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;cAMrB,kBAAkB,IAAI,UAAU;CAGpD;AAgBD,wEAAwE;AACxE,wBAAgB,SAAS,CAAC,IAAI,EAAE,eAAe,GAAG,QAAQ,CAYzD;AAED,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,CAAA"}
|
package/dist/web.mjs
CHANGED
|
@@ -379,7 +379,13 @@ function resolveAnonymousId() {
|
|
|
379
379
|
}
|
|
380
380
|
var singleton = null;
|
|
381
381
|
function initHoldy(opts) {
|
|
382
|
-
if (!singleton)
|
|
382
|
+
if (!singleton) {
|
|
383
|
+
singleton = new HoldyWeb(opts);
|
|
384
|
+
if (typeof window !== "undefined") {
|
|
385
|
+
const w = window;
|
|
386
|
+
if (!w.Holdy) w.Holdy = singleton;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
383
389
|
return singleton;
|
|
384
390
|
}
|
|
385
391
|
export {
|
package/dist/web.mjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/core.ts", "../src/auto-capture.ts", "../src/web-context.ts", "../src/web.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Core do @holdyapp/sdk \u00B7 fila + flush pra POST /api/v1/events/bulk.\n *\n * Retry de batch inteiro \u00E9 seguro (partial accept + dedupe server-side).\n * Fila \u00E9 bounded: acima de MAX_QUEUE dropa os mais antigos (telemetria nunca\n * derruba o app do cliente \u2014 nenhum m\u00E9todo p\u00FAblico lan\u00E7a).\n */\n\nimport type { CanonicalEvent, HoldyOptions, Properties, TrackOverrides } from './types'\n\nexport const DEFAULT_BASE_URL = 'https://holdy.chat/api/v1'\nconst DEFAULT_FLUSH_AT = 20\nconst DEFAULT_FLUSH_INTERVAL_MS = 5000\nconst MAX_QUEUE = 1000\nconst BULK_LIMIT = 500\n\nexport class HoldyCore {\n private readonly apiKey: string\n private readonly baseUrl: string\n private readonly flushAt: number\n private readonly debug: boolean\n private readonly source: 'sdk_web' | 'sdk_server'\n\n private queue: CanonicalEvent[] = []\n private timer: ReturnType<typeof setInterval> | null = null\n private flushing: Promise<void> | null = null\n\n protected userId: string | null = null\n protected anonymousId: string | null = null\n protected accountKey: string | null = null\n\n constructor(source: 'sdk_web' | 'sdk_server', opts: HoldyOptions) {\n this.source = source\n this.apiKey = opts.apiKey\n this.accountKey = opts.accountKey ?? null\n this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, '')\n this.flushAt = Math.max(1, opts.flushAt ?? DEFAULT_FLUSH_AT)\n this.debug = opts.debug ?? false\n\n const intervalMs = opts.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS\n if (intervalMs > 0) {\n this.timer = setInterval(() => void this.flush(), intervalMs)\n // Node: n\u00E3o segura o processo vivo s\u00F3 pelo timer do SDK.\n if (typeof (this.timer as { unref?: () => void }).unref === 'function') {\n ;(this.timer as unknown as { unref: () => void }).unref()\n }\n }\n }\n\n track(name: string, properties?: Properties, overrides?: TrackOverrides): void {\n if (!name || typeof name !== 'string') return\n\n const userId = overrides?.userId ?? this.userId ?? undefined\n const accountKey = overrides?.accountKey ?? this.accountKey ?? undefined\n const occurredAt = overrides?.occurredAt\n ? new Date(overrides.occurredAt).toISOString()\n : new Date().toISOString()\n\n // Ordem: ambiente observado pelo SDK \u2192 context da chamada \u2192 carrier de\n // account. O que o caller passou vence o inferido; o carrier vence tudo,\n // porque \u00E9 o que a Data layer usa pra resolver a conta.\n const context: Properties = {\n ...this.environmentContext(),\n ...(overrides?.context ?? {}),\n }\n if (accountKey) context.account_external_id = accountKey\n\n const event: CanonicalEvent = {\n event: name,\n occurred_at: occurredAt,\n source: this.source,\n ...(userId ? { user_id: userId } : {}),\n ...(this.anonymousId ? { anonymous_id: this.anonymousId } : {}),\n properties: properties ?? {},\n ...(Object.keys(context).length > 0 ? { context } : {}),\n }\n\n this.queue.push(event)\n if (this.queue.length > MAX_QUEUE) this.queue.splice(0, this.queue.length - MAX_QUEUE)\n if (this.queue.length >= this.flushAt) void this.flush()\n }\n\n identify(userId: string, traits?: Properties): void {\n if (!userId || typeof userId !== 'string') return\n this.userId = userId\n this.track('$identify', traits ?? {})\n }\n\n group(accountKey: string, traits?: Properties): void {\n if (!accountKey || typeof accountKey !== 'string') return\n this.accountKey = accountKey\n this.track('$group', traits ?? {})\n }\n\n /** Drena a fila. Nunca lan\u00E7a \u2014 falha re-enfileira e loga em debug. */\n async flush(): Promise<void> {\n // Serializa flushes concorrentes (timer + flushAt + manual).\n if (this.flushing) return this.flushing\n if (this.queue.length === 0) return\n\n this.flushing = this.drain().finally(() => {\n this.flushing = null\n })\n return this.flushing\n }\n\n /** Flush final + para o timer. Chamar em shutdown do server / unload do web. */\n async shutdown(): Promise<void> {\n if (this.timer) {\n clearInterval(this.timer)\n this.timer = null\n }\n await this.flush()\n }\n\n private async drain(): Promise<void> {\n while (this.queue.length > 0) {\n const batch = this.queue.splice(0, BULK_LIMIT)\n const ok = await this.send(batch)\n if (!ok) {\n // Devolve pra frente da fila e para \u2014 pr\u00F3ximo flush re-tenta (dedupe-safe).\n this.queue.unshift(...batch)\n return\n }\n }\n }\n\n private async send(events: CanonicalEvent[]): Promise<boolean> {\n try {\n const res = await fetch(`${this.baseUrl}/events/bulk`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ events }),\n ...(this.source === 'sdk_web' ? { keepalive: true } : {}),\n })\n if (!res.ok) {\n // 4xx: batch estruturalmente inv\u00E1lido n\u00E3o re-tenta (evita loop infinito).\n if (res.status >= 400 && res.status < 500) {\n this.log(`bulk rejected ${res.status} \u00B7 batch dropped`)\n return true\n }\n this.log(`bulk failed ${res.status} \u00B7 will retry`)\n return false\n }\n return true\n } catch (err) {\n this.log(`bulk network error: ${(err as Error).message} \u00B7 will retry`)\n return false\n }\n }\n\n private log(msg: string): void {\n if (this.debug) console.warn(`[holdy] ${msg}`)\n }\n\n /**\n * Ambiente que o SDK observa sozinho (url, referrer, browser, utm\u2026).\n * Vazio no core; `HoldyWeb` preenche. O server n\u00E3o infere ambiente \u2014 n\u00E3o\n * existe \"p\u00E1gina atual\" num worker.\n */\n protected environmentContext(): Properties {\n return {}\n }\n\n /** Interno \u00B7 exposto pra testes. */\n get pending(): number {\n return this.queue.length\n }\n}\n", "/**\n * Auto-capture de navega\u00E7\u00E3o \u00B7 @holdyapp/sdk/web.\n *\n * Emite pageview no carregamento e a cada mudan\u00E7a de rota, inclusive nas que\n * n\u00E3o recarregam a p\u00E1gina (SPA). Sem isto, instrumentar exige que o cliente\n * chame `page()` em cada tela \u2014 e o primeiro dado s\u00F3 aparece depois de um\n * trabalho que ele ainda n\u00E3o fez.\n *\n * Idempotente: reinstalar \u00E9 no-op (guard global). O retorno desfaz o patch de\n * `history`, ent\u00E3o `destroy()` devolve o browser como estava.\n */\n\nconst INSTALLED_FLAG = '__holdy_autocapture__'\n\n/** pushState seguido de replaceState \u00E9 uma mudan\u00E7a l\u00F3gica s\u00F3. */\nconst ROUTE_DEBOUNCE_MS = 50\n\nexport type NavigationKind = 'initial_load' | 'route_change'\n\nexport interface AutoCaptureOptions {\n onNavigate: (kind: NavigationKind) => void\n}\n\ninterface GlobalWithFlag {\n [INSTALLED_FLAG]?: boolean\n}\n\nexport function installAutoCapture(opts: AutoCaptureOptions): () => void {\n if (typeof window === 'undefined' || typeof history === 'undefined') {\n return () => undefined\n }\n\n const w = window as unknown as GlobalWithFlag\n if (w[INSTALLED_FLAG]) return () => undefined\n w[INSTALLED_FLAG] = true\n\n const cleanups: Array<() => void> = []\n let lastPath = currentPath()\n let pending: ReturnType<typeof setTimeout> | null = null\n\n function emit(kind: NavigationKind): void {\n try {\n opts.onNavigate(kind)\n } catch {\n // Telemetria nunca derruba a app do cliente.\n }\n }\n\n function scheduleRouteEmit(): void {\n if (pending) clearTimeout(pending)\n pending = setTimeout(() => {\n pending = null\n const path = currentPath()\n // S\u00F3 conta como pageview se a rota mudou de fato \u2014 replaceState de\n // estado interno (filtro, scroll) n\u00E3o \u00E9 navega\u00E7\u00E3o.\n if (path === lastPath) return\n lastPath = path\n emit('route_change')\n }, ROUTE_DEBOUNCE_MS)\n }\n\n emit('initial_load')\n\n const originalPush = history.pushState\n const originalReplace = history.replaceState\n\n history.pushState = function patchedPush(...args: Parameters<History['pushState']>) {\n const result = originalPush.apply(this, args)\n scheduleRouteEmit()\n return result\n }\n history.replaceState = function patchedReplace(...args: Parameters<History['replaceState']>) {\n const result = originalReplace.apply(this, args)\n scheduleRouteEmit()\n return result\n }\n cleanups.push(() => {\n history.pushState = originalPush\n history.replaceState = originalReplace\n })\n\n // Voltar/avan\u00E7ar do browser e routers baseados em hash.\n const onPop = (): void => scheduleRouteEmit()\n window.addEventListener('popstate', onPop)\n cleanups.push(() => window.removeEventListener('popstate', onPop))\n\n const onHash = (): void => scheduleRouteEmit()\n window.addEventListener('hashchange', onHash)\n cleanups.push(() => window.removeEventListener('hashchange', onHash))\n\n return () => {\n if (pending) clearTimeout(pending)\n for (const cleanup of cleanups) cleanup()\n w[INSTALLED_FLAG] = false\n }\n}\n\n/** Rota compar\u00E1vel \u00B7 path + query, sem hash de \u00E2ncora. */\nfunction currentPath(): string {\n if (typeof location === 'undefined') return '/'\n return `${location.pathname}${location.search}${location.hash}`\n}\n", "/**\n * Contexto de ambiente do browser \u00B7 @holdyapp/sdk/web.\n *\n * Vai em `context` de todo evento, nunca em `properties`: properties \u00E9 o\n * vocabul\u00E1rio do produto do cliente (`workflow_key`, `plan`), context \u00E9 o que o\n * SDK observou sozinho. A Data layer persiste os dois (facts.context, 00247) e\n * a leitura de dimens\u00E3o consulta os dois.\n *\n * Tr\u00EAs decis\u00F5es que definem se esse dado \u00E9 \u00FAtil ou lixo:\n * \u00B7 `url` sai sem query e sem hash. Href cru tem uma varia\u00E7\u00E3o por sess\u00E3o, e\n * dimens\u00E3o com cardinalidade quase-\u00FAnica \u00E9 descartada na leitura \u2014 a\n * p\u00E1gina vira ru\u00EDdo exatamente por excesso de precis\u00E3o.\n * \u00B7 utm \u00E9 capturado na primeira p\u00E1gina e carregado pelo resto da sess\u00E3o.\n * Atribui\u00E7\u00E3o que s\u00F3 existe no primeiro evento n\u00E3o responde nada.\n * \u00B7 referrer s\u00F3 quando \u00E9 externo. Navega\u00E7\u00E3o interna preenchendo \"Origem do\n * tr\u00E1fego\" com o pr\u00F3prio dom\u00EDnio esconde a origem real.\n *\n * Tudo aqui \u00E9 best-effort: browser em modo privativo, SSR e webview sem\n * storage precisam funcionar. Nenhuma fun\u00E7\u00E3o lan\u00E7a.\n */\n\nexport type WebContext = Record<string, string>\n\nconst UTM_KEYS = [\n 'utm_source',\n 'utm_medium',\n 'utm_campaign',\n 'utm_term',\n 'utm_content',\n] as const\n\nconst SESSION_STORAGE_KEY = 'holdy_session'\nconst ATTRIBUTION_STORAGE_KEY = 'holdy_attribution'\n\n/* \u2500\u2500 URL \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/** `https://app.x.com/a/b?q=1#z` \u2192 `https://app.x.com/a/b`. */\nexport function stripUrl(href: string): string | null {\n try {\n const url = new URL(href)\n return `${url.origin}${url.pathname}`\n } catch {\n return null\n }\n}\n\n/** Par\u00E2metros de campanha presentes na URL, j\u00E1 em lowercase. */\nexport function parseUtm(href: string): WebContext {\n const out: WebContext = {}\n try {\n const params = new URL(href).searchParams\n for (const key of UTM_KEYS) {\n const value = params.get(key)?.trim()\n if (value) out[key] = value.toLowerCase().slice(0, 120)\n }\n } catch {\n // href inv\u00E1lido \u2014 sem utm, sem drama.\n }\n return out\n}\n\n/* \u2500\u2500 User agent \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\ninterface UaParts {\n browser?: string\n os?: string\n device?: string\n}\n\n/**\n * Fatia m\u00EDnima do UA: navegador, sistema e classe de dispositivo.\n *\n * N\u00E3o \u00E9 biblioteca de UA parsing e n\u00E3o deve virar uma \u2014 o valor est\u00E1 em\n * separar mobile de desktop e Safari de Chrome, n\u00E3o em cravar a vers\u00E3o.\n */\nexport function parseUserAgent(ua: string): UaParts {\n if (!ua) return {}\n const s = ua.toLowerCase()\n\n const browser = s.includes('edg/')\n ? 'Edge'\n : s.includes('opr/') || s.includes('opera')\n ? 'Opera'\n : s.includes('samsungbrowser')\n ? 'Samsung Internet'\n : s.includes('firefox') || s.includes('fxios')\n ? 'Firefox'\n : s.includes('chrome') || s.includes('crios')\n ? 'Chrome'\n : s.includes('safari')\n ? 'Safari'\n : undefined\n\n const os = s.includes('iphone') || s.includes('ipad') || s.includes('ipod')\n ? 'iOS'\n : s.includes('android')\n ? 'Android'\n : s.includes('windows')\n ? 'Windows'\n : s.includes('mac os') || s.includes('macintosh')\n ? 'macOS'\n : s.includes('cros')\n ? 'ChromeOS'\n : s.includes('linux')\n ? 'Linux'\n : undefined\n\n // iPad moderno se declara macOS; a pista que sobra \u00E9 o suporte a toque, que\n // o parser puro n\u00E3o v\u00EA. Tablet aqui cobre o caso declarado no UA.\n const device = s.includes('ipad') || (s.includes('android') && !s.includes('mobile'))\n ? 'tablet'\n : s.includes('mobi') || s.includes('iphone') || s.includes('ipod')\n ? 'mobile'\n : 'desktop'\n\n return {\n ...(browser ? { browser } : {}),\n ...(os ? { os } : {}),\n device,\n }\n}\n\n/* \u2500\u2500 Storage \u00B7 nunca lan\u00E7a \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\nfunction readStorage(storage: 'session' | 'local', key: string): string | null {\n try {\n const s = storage === 'session' ? sessionStorage : localStorage\n return s.getItem(key)\n } catch {\n return null\n }\n}\n\nfunction writeStorage(storage: 'session' | 'local', key: string, value: string): void {\n try {\n const s = storage === 'session' ? sessionStorage : localStorage\n s.setItem(key, value)\n } catch {\n // Modo privativo / storage bloqueado \u2014 o contexto degrada, o evento segue.\n }\n}\n\nfunction randomId(): string {\n try {\n return crypto.randomUUID()\n } catch {\n return `hol-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`\n }\n}\n\n/** ID da sess\u00E3o de browser \u00B7 vive enquanto a aba viver. */\nexport function resolveSessionId(): string {\n const existing = readStorage('session', SESSION_STORAGE_KEY)\n if (existing) return existing\n const fresh = randomId()\n writeStorage('session', SESSION_STORAGE_KEY, fresh)\n return fresh\n}\n\n/**\n * Atribui\u00E7\u00E3o da sess\u00E3o: utm da primeira p\u00E1gina + referrer externo de entrada.\n * Gravada uma vez e repetida em todo evento da sess\u00E3o.\n */\nexport function resolveAttribution(href: string, referrer: string, currentHost: string): WebContext {\n const stored = readStorage('session', ATTRIBUTION_STORAGE_KEY)\n if (stored) {\n try {\n return JSON.parse(stored) as WebContext\n } catch {\n // Valor corrompido \u2014 recalcula por cima.\n }\n }\n\n const attribution: WebContext = { ...parseUtm(href) }\n\n const referrerHost = hostOf(referrer)\n // Referrer interno n\u00E3o \u00E9 origem de tr\u00E1fego, \u00E9 navega\u00E7\u00E3o.\n if (referrerHost && referrerHost !== currentHost) {\n const clean = stripUrl(referrer)\n if (clean) attribution.referrer = clean\n attribution.referrer_host = referrerHost\n }\n\n writeStorage('session', ATTRIBUTION_STORAGE_KEY, JSON.stringify(attribution))\n return attribution\n}\n\nfunction hostOf(href: string): string | null {\n try {\n return new URL(href).hostname.toLowerCase() || null\n } catch {\n return null\n }\n}\n\n/* \u2500\u2500 Montagem \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/**\n * Contexto completo do evento atual. Fora do browser devolve `{}` \u2014 o SDK\n * server tem o pr\u00F3prio caminho e n\u00E3o deve inventar ambiente.\n */\nexport function buildWebContext(): WebContext {\n if (typeof window === 'undefined' || typeof location === 'undefined') return {}\n\n const href = location.href\n const referrer = typeof document !== 'undefined' ? document.referrer : ''\n const host = location.hostname.toLowerCase()\n\n const ua = typeof navigator !== 'undefined' ? navigator.userAgent : ''\n const { browser, os, device } = parseUserAgent(ua)\n\n const url = stripUrl(href)\n\n return {\n ...(url ? { url } : {}),\n path: location.pathname || '/',\n host,\n ...(browser ? { browser } : {}),\n ...(os ? { os } : {}),\n ...(device ? { device } : {}),\n ...(typeof navigator !== 'undefined' && navigator.language\n ? { locale: navigator.language }\n : {}),\n session_id: resolveSessionId(),\n ...resolveAttribution(href, referrer, host),\n }\n}\n", "/**\n * @holdyapp/sdk/web \u00B7 browser (source: sdk_web).\n *\n * anonymous_id gerado e persistido em localStorage (holdy_anon_id) \u2014 enviado em\n * todo event at\u00E9 identify() promover pra user_id (identity graph faz o merge\n * downstream, nunca o SDK). Flush no pagehide via fetch keepalive.\n *\n * Todo evento carrega o contexto de ambiente (url, path, referrer externo,\n * utm da sess\u00E3o, browser, os, device, locale, session_id) em `context` \u2014\n * separado das properties do produto. Pageview \u00E9 autom\u00E1tico por default:\n * carregamento inicial e cada mudan\u00E7a de rota, inclusive em SPA.\n */\n\nimport { HoldyCore } from './core'\nimport { installAutoCapture, type NavigationKind } from './auto-capture'\nimport { buildWebContext } from './web-context'\nimport type { HoldyOptions, Properties } from './types'\n\nconst ANON_STORAGE_KEY = 'holdy_anon_id'\n\nexport interface HoldyWebOptions extends HoldyOptions {\n /**\n * Emitir `$pageview` sozinho no load e a cada mudan\u00E7a de rota \u00B7 default true.\n * Desligue s\u00F3 se o produto j\u00E1 chama `page()` na m\u00E3o em cada tela.\n */\n autoCapture?: boolean\n}\n\nexport class HoldyWeb extends HoldyCore {\n private teardownAutoCapture: (() => void) | null = null\n\n constructor(opts: HoldyWebOptions) {\n super('sdk_web', opts)\n this.anonymousId = resolveAnonymousId()\n\n if (typeof window !== 'undefined') {\n window.addEventListener('pagehide', () => void this.flush())\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'hidden') void this.flush()\n })\n }\n\n if (opts.autoCapture !== false) {\n this.teardownAutoCapture = installAutoCapture({\n onNavigate: (kind: NavigationKind) => this.page(undefined, { navigation: kind }),\n })\n }\n }\n\n /**\n * Pageview. O `$` marca evento emitido pelo SDK, n\u00E3o pelo produto (mesma\n * conven\u00E7\u00E3o de `$identify`/`$group`); a canonicaliza\u00E7\u00E3o downstream remove o\n * sigilo, ent\u00E3o `$pageview` e `pageview` s\u00E3o o mesmo evento na leitura.\n * url/path/referrer/utm j\u00E1 v\u00EAm no context de todo evento \u2014 aqui s\u00F3 entra o\n * que \u00E9 da p\u00E1gina como conte\u00FAdo (nome e t\u00EDtulo).\n */\n page(name?: string, properties?: Properties): void {\n this.track('$pageview', {\n ...(properties ?? {}),\n ...(name ? { page_name: name } : {}),\n ...(typeof document !== 'undefined' && document.title ? { title: document.title } : {}),\n })\n }\n\n reset(): void {\n this.userId = null\n this.accountKey = null\n try {\n localStorage.removeItem(ANON_STORAGE_KEY)\n } catch {\n // storage indispon\u00EDvel (SSR/privacy mode) \u2014 ok\n }\n this.anonymousId = resolveAnonymousId()\n }\n\n /** Flush final + desinstala o auto-capture (devolve `history` ao original). */\n override async shutdown(): Promise<void> {\n this.teardownAutoCapture?.()\n this.teardownAutoCapture = null\n await super.shutdown()\n }\n\n protected override environmentContext(): Properties {\n return buildWebContext()\n }\n}\n\nfunction resolveAnonymousId(): string {\n try {\n const existing = localStorage.getItem(ANON_STORAGE_KEY)\n if (existing) return existing\n const fresh = crypto.randomUUID()\n localStorage.setItem(ANON_STORAGE_KEY, fresh)\n return fresh\n } catch {\n return crypto.randomUUID()\n }\n}\n\nlet singleton: HoldyWeb | null = null\n\n/** Init idempotente \u2014 segunda chamada retorna a inst\u00E2ncia existente. */\nexport function initHoldy(opts: HoldyWebOptions): HoldyWeb {\n if (!singleton) singleton = new HoldyWeb(opts)\n return singleton\n}\n\nexport type { HoldyOptions, Properties }\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["/**\n * Core do @holdyapp/sdk \u00B7 fila + flush pra POST /api/v1/events/bulk.\n *\n * Retry de batch inteiro \u00E9 seguro (partial accept + dedupe server-side).\n * Fila \u00E9 bounded: acima de MAX_QUEUE dropa os mais antigos (telemetria nunca\n * derruba o app do cliente \u2014 nenhum m\u00E9todo p\u00FAblico lan\u00E7a).\n */\n\nimport type { CanonicalEvent, HoldyOptions, Properties, TrackOverrides } from './types'\n\nexport const DEFAULT_BASE_URL = 'https://holdy.chat/api/v1'\nconst DEFAULT_FLUSH_AT = 20\nconst DEFAULT_FLUSH_INTERVAL_MS = 5000\nconst MAX_QUEUE = 1000\nconst BULK_LIMIT = 500\n\nexport class HoldyCore {\n private readonly apiKey: string\n private readonly baseUrl: string\n private readonly flushAt: number\n private readonly debug: boolean\n private readonly source: 'sdk_web' | 'sdk_server'\n\n private queue: CanonicalEvent[] = []\n private timer: ReturnType<typeof setInterval> | null = null\n private flushing: Promise<void> | null = null\n\n protected userId: string | null = null\n protected anonymousId: string | null = null\n protected accountKey: string | null = null\n\n constructor(source: 'sdk_web' | 'sdk_server', opts: HoldyOptions) {\n this.source = source\n this.apiKey = opts.apiKey\n this.accountKey = opts.accountKey ?? null\n this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, '')\n this.flushAt = Math.max(1, opts.flushAt ?? DEFAULT_FLUSH_AT)\n this.debug = opts.debug ?? false\n\n const intervalMs = opts.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS\n if (intervalMs > 0) {\n this.timer = setInterval(() => void this.flush(), intervalMs)\n // Node: n\u00E3o segura o processo vivo s\u00F3 pelo timer do SDK.\n if (typeof (this.timer as { unref?: () => void }).unref === 'function') {\n ;(this.timer as unknown as { unref: () => void }).unref()\n }\n }\n }\n\n track(name: string, properties?: Properties, overrides?: TrackOverrides): void {\n if (!name || typeof name !== 'string') return\n\n const userId = overrides?.userId ?? this.userId ?? undefined\n const accountKey = overrides?.accountKey ?? this.accountKey ?? undefined\n const occurredAt = overrides?.occurredAt\n ? new Date(overrides.occurredAt).toISOString()\n : new Date().toISOString()\n\n // Ordem: ambiente observado pelo SDK \u2192 context da chamada \u2192 carrier de\n // account. O que o caller passou vence o inferido; o carrier vence tudo,\n // porque \u00E9 o que a Data layer usa pra resolver a conta.\n const context: Properties = {\n ...this.environmentContext(),\n ...(overrides?.context ?? {}),\n }\n if (accountKey) context.account_external_id = accountKey\n\n const event: CanonicalEvent = {\n event: name,\n occurred_at: occurredAt,\n source: this.source,\n ...(userId ? { user_id: userId } : {}),\n ...(this.anonymousId ? { anonymous_id: this.anonymousId } : {}),\n properties: properties ?? {},\n ...(Object.keys(context).length > 0 ? { context } : {}),\n }\n\n this.queue.push(event)\n if (this.queue.length > MAX_QUEUE) this.queue.splice(0, this.queue.length - MAX_QUEUE)\n if (this.queue.length >= this.flushAt) void this.flush()\n }\n\n identify(userId: string, traits?: Properties): void {\n if (!userId || typeof userId !== 'string') return\n this.userId = userId\n this.track('$identify', traits ?? {})\n }\n\n group(accountKey: string, traits?: Properties): void {\n if (!accountKey || typeof accountKey !== 'string') return\n this.accountKey = accountKey\n this.track('$group', traits ?? {})\n }\n\n /** Drena a fila. Nunca lan\u00E7a \u2014 falha re-enfileira e loga em debug. */\n async flush(): Promise<void> {\n // Serializa flushes concorrentes (timer + flushAt + manual).\n if (this.flushing) return this.flushing\n if (this.queue.length === 0) return\n\n this.flushing = this.drain().finally(() => {\n this.flushing = null\n })\n return this.flushing\n }\n\n /** Flush final + para o timer. Chamar em shutdown do server / unload do web. */\n async shutdown(): Promise<void> {\n if (this.timer) {\n clearInterval(this.timer)\n this.timer = null\n }\n await this.flush()\n }\n\n private async drain(): Promise<void> {\n while (this.queue.length > 0) {\n const batch = this.queue.splice(0, BULK_LIMIT)\n const ok = await this.send(batch)\n if (!ok) {\n // Devolve pra frente da fila e para \u2014 pr\u00F3ximo flush re-tenta (dedupe-safe).\n this.queue.unshift(...batch)\n return\n }\n }\n }\n\n private async send(events: CanonicalEvent[]): Promise<boolean> {\n try {\n const res = await fetch(`${this.baseUrl}/events/bulk`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ events }),\n ...(this.source === 'sdk_web' ? { keepalive: true } : {}),\n })\n if (!res.ok) {\n // 4xx: batch estruturalmente inv\u00E1lido n\u00E3o re-tenta (evita loop infinito).\n if (res.status >= 400 && res.status < 500) {\n this.log(`bulk rejected ${res.status} \u00B7 batch dropped`)\n return true\n }\n this.log(`bulk failed ${res.status} \u00B7 will retry`)\n return false\n }\n return true\n } catch (err) {\n this.log(`bulk network error: ${(err as Error).message} \u00B7 will retry`)\n return false\n }\n }\n\n private log(msg: string): void {\n if (this.debug) console.warn(`[holdy] ${msg}`)\n }\n\n /**\n * Ambiente que o SDK observa sozinho (url, referrer, browser, utm\u2026).\n * Vazio no core; `HoldyWeb` preenche. O server n\u00E3o infere ambiente \u2014 n\u00E3o\n * existe \"p\u00E1gina atual\" num worker.\n */\n protected environmentContext(): Properties {\n return {}\n }\n\n /** Interno \u00B7 exposto pra testes. */\n get pending(): number {\n return this.queue.length\n }\n}\n", "/**\n * Auto-capture de navega\u00E7\u00E3o \u00B7 @holdyapp/sdk/web.\n *\n * Emite pageview no carregamento e a cada mudan\u00E7a de rota, inclusive nas que\n * n\u00E3o recarregam a p\u00E1gina (SPA). Sem isto, instrumentar exige que o cliente\n * chame `page()` em cada tela \u2014 e o primeiro dado s\u00F3 aparece depois de um\n * trabalho que ele ainda n\u00E3o fez.\n *\n * Idempotente: reinstalar \u00E9 no-op (guard global). O retorno desfaz o patch de\n * `history`, ent\u00E3o `destroy()` devolve o browser como estava.\n */\n\nconst INSTALLED_FLAG = '__holdy_autocapture__'\n\n/** pushState seguido de replaceState \u00E9 uma mudan\u00E7a l\u00F3gica s\u00F3. */\nconst ROUTE_DEBOUNCE_MS = 50\n\nexport type NavigationKind = 'initial_load' | 'route_change'\n\nexport interface AutoCaptureOptions {\n onNavigate: (kind: NavigationKind) => void\n}\n\ninterface GlobalWithFlag {\n [INSTALLED_FLAG]?: boolean\n}\n\nexport function installAutoCapture(opts: AutoCaptureOptions): () => void {\n if (typeof window === 'undefined' || typeof history === 'undefined') {\n return () => undefined\n }\n\n const w = window as unknown as GlobalWithFlag\n if (w[INSTALLED_FLAG]) return () => undefined\n w[INSTALLED_FLAG] = true\n\n const cleanups: Array<() => void> = []\n let lastPath = currentPath()\n let pending: ReturnType<typeof setTimeout> | null = null\n\n function emit(kind: NavigationKind): void {\n try {\n opts.onNavigate(kind)\n } catch {\n // Telemetria nunca derruba a app do cliente.\n }\n }\n\n function scheduleRouteEmit(): void {\n if (pending) clearTimeout(pending)\n pending = setTimeout(() => {\n pending = null\n const path = currentPath()\n // S\u00F3 conta como pageview se a rota mudou de fato \u2014 replaceState de\n // estado interno (filtro, scroll) n\u00E3o \u00E9 navega\u00E7\u00E3o.\n if (path === lastPath) return\n lastPath = path\n emit('route_change')\n }, ROUTE_DEBOUNCE_MS)\n }\n\n emit('initial_load')\n\n const originalPush = history.pushState\n const originalReplace = history.replaceState\n\n history.pushState = function patchedPush(...args: Parameters<History['pushState']>) {\n const result = originalPush.apply(this, args)\n scheduleRouteEmit()\n return result\n }\n history.replaceState = function patchedReplace(...args: Parameters<History['replaceState']>) {\n const result = originalReplace.apply(this, args)\n scheduleRouteEmit()\n return result\n }\n cleanups.push(() => {\n history.pushState = originalPush\n history.replaceState = originalReplace\n })\n\n // Voltar/avan\u00E7ar do browser e routers baseados em hash.\n const onPop = (): void => scheduleRouteEmit()\n window.addEventListener('popstate', onPop)\n cleanups.push(() => window.removeEventListener('popstate', onPop))\n\n const onHash = (): void => scheduleRouteEmit()\n window.addEventListener('hashchange', onHash)\n cleanups.push(() => window.removeEventListener('hashchange', onHash))\n\n return () => {\n if (pending) clearTimeout(pending)\n for (const cleanup of cleanups) cleanup()\n w[INSTALLED_FLAG] = false\n }\n}\n\n/** Rota compar\u00E1vel \u00B7 path + query, sem hash de \u00E2ncora. */\nfunction currentPath(): string {\n if (typeof location === 'undefined') return '/'\n return `${location.pathname}${location.search}${location.hash}`\n}\n", "/**\n * Contexto de ambiente do browser \u00B7 @holdyapp/sdk/web.\n *\n * Vai em `context` de todo evento, nunca em `properties`: properties \u00E9 o\n * vocabul\u00E1rio do produto do cliente (`workflow_key`, `plan`), context \u00E9 o que o\n * SDK observou sozinho. A Data layer persiste os dois (facts.context, 00247) e\n * a leitura de dimens\u00E3o consulta os dois.\n *\n * Tr\u00EAs decis\u00F5es que definem se esse dado \u00E9 \u00FAtil ou lixo:\n * \u00B7 `url` sai sem query e sem hash. Href cru tem uma varia\u00E7\u00E3o por sess\u00E3o, e\n * dimens\u00E3o com cardinalidade quase-\u00FAnica \u00E9 descartada na leitura \u2014 a\n * p\u00E1gina vira ru\u00EDdo exatamente por excesso de precis\u00E3o.\n * \u00B7 utm \u00E9 capturado na primeira p\u00E1gina e carregado pelo resto da sess\u00E3o.\n * Atribui\u00E7\u00E3o que s\u00F3 existe no primeiro evento n\u00E3o responde nada.\n * \u00B7 referrer s\u00F3 quando \u00E9 externo. Navega\u00E7\u00E3o interna preenchendo \"Origem do\n * tr\u00E1fego\" com o pr\u00F3prio dom\u00EDnio esconde a origem real.\n *\n * Tudo aqui \u00E9 best-effort: browser em modo privativo, SSR e webview sem\n * storage precisam funcionar. Nenhuma fun\u00E7\u00E3o lan\u00E7a.\n */\n\nexport type WebContext = Record<string, string>\n\nconst UTM_KEYS = [\n 'utm_source',\n 'utm_medium',\n 'utm_campaign',\n 'utm_term',\n 'utm_content',\n] as const\n\nconst SESSION_STORAGE_KEY = 'holdy_session'\nconst ATTRIBUTION_STORAGE_KEY = 'holdy_attribution'\n\n/* \u2500\u2500 URL \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/** `https://app.x.com/a/b?q=1#z` \u2192 `https://app.x.com/a/b`. */\nexport function stripUrl(href: string): string | null {\n try {\n const url = new URL(href)\n return `${url.origin}${url.pathname}`\n } catch {\n return null\n }\n}\n\n/** Par\u00E2metros de campanha presentes na URL, j\u00E1 em lowercase. */\nexport function parseUtm(href: string): WebContext {\n const out: WebContext = {}\n try {\n const params = new URL(href).searchParams\n for (const key of UTM_KEYS) {\n const value = params.get(key)?.trim()\n if (value) out[key] = value.toLowerCase().slice(0, 120)\n }\n } catch {\n // href inv\u00E1lido \u2014 sem utm, sem drama.\n }\n return out\n}\n\n/* \u2500\u2500 User agent \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\ninterface UaParts {\n browser?: string\n os?: string\n device?: string\n}\n\n/**\n * Fatia m\u00EDnima do UA: navegador, sistema e classe de dispositivo.\n *\n * N\u00E3o \u00E9 biblioteca de UA parsing e n\u00E3o deve virar uma \u2014 o valor est\u00E1 em\n * separar mobile de desktop e Safari de Chrome, n\u00E3o em cravar a vers\u00E3o.\n */\nexport function parseUserAgent(ua: string): UaParts {\n if (!ua) return {}\n const s = ua.toLowerCase()\n\n const browser = s.includes('edg/')\n ? 'Edge'\n : s.includes('opr/') || s.includes('opera')\n ? 'Opera'\n : s.includes('samsungbrowser')\n ? 'Samsung Internet'\n : s.includes('firefox') || s.includes('fxios')\n ? 'Firefox'\n : s.includes('chrome') || s.includes('crios')\n ? 'Chrome'\n : s.includes('safari')\n ? 'Safari'\n : undefined\n\n const os = s.includes('iphone') || s.includes('ipad') || s.includes('ipod')\n ? 'iOS'\n : s.includes('android')\n ? 'Android'\n : s.includes('windows')\n ? 'Windows'\n : s.includes('mac os') || s.includes('macintosh')\n ? 'macOS'\n : s.includes('cros')\n ? 'ChromeOS'\n : s.includes('linux')\n ? 'Linux'\n : undefined\n\n // iPad moderno se declara macOS; a pista que sobra \u00E9 o suporte a toque, que\n // o parser puro n\u00E3o v\u00EA. Tablet aqui cobre o caso declarado no UA.\n const device = s.includes('ipad') || (s.includes('android') && !s.includes('mobile'))\n ? 'tablet'\n : s.includes('mobi') || s.includes('iphone') || s.includes('ipod')\n ? 'mobile'\n : 'desktop'\n\n return {\n ...(browser ? { browser } : {}),\n ...(os ? { os } : {}),\n device,\n }\n}\n\n/* \u2500\u2500 Storage \u00B7 nunca lan\u00E7a \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\nfunction readStorage(storage: 'session' | 'local', key: string): string | null {\n try {\n const s = storage === 'session' ? sessionStorage : localStorage\n return s.getItem(key)\n } catch {\n return null\n }\n}\n\nfunction writeStorage(storage: 'session' | 'local', key: string, value: string): void {\n try {\n const s = storage === 'session' ? sessionStorage : localStorage\n s.setItem(key, value)\n } catch {\n // Modo privativo / storage bloqueado \u2014 o contexto degrada, o evento segue.\n }\n}\n\nfunction randomId(): string {\n try {\n return crypto.randomUUID()\n } catch {\n return `hol-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`\n }\n}\n\n/** ID da sess\u00E3o de browser \u00B7 vive enquanto a aba viver. */\nexport function resolveSessionId(): string {\n const existing = readStorage('session', SESSION_STORAGE_KEY)\n if (existing) return existing\n const fresh = randomId()\n writeStorage('session', SESSION_STORAGE_KEY, fresh)\n return fresh\n}\n\n/**\n * Atribui\u00E7\u00E3o da sess\u00E3o: utm da primeira p\u00E1gina + referrer externo de entrada.\n * Gravada uma vez e repetida em todo evento da sess\u00E3o.\n */\nexport function resolveAttribution(href: string, referrer: string, currentHost: string): WebContext {\n const stored = readStorage('session', ATTRIBUTION_STORAGE_KEY)\n if (stored) {\n try {\n return JSON.parse(stored) as WebContext\n } catch {\n // Valor corrompido \u2014 recalcula por cima.\n }\n }\n\n const attribution: WebContext = { ...parseUtm(href) }\n\n const referrerHost = hostOf(referrer)\n // Referrer interno n\u00E3o \u00E9 origem de tr\u00E1fego, \u00E9 navega\u00E7\u00E3o.\n if (referrerHost && referrerHost !== currentHost) {\n const clean = stripUrl(referrer)\n if (clean) attribution.referrer = clean\n attribution.referrer_host = referrerHost\n }\n\n writeStorage('session', ATTRIBUTION_STORAGE_KEY, JSON.stringify(attribution))\n return attribution\n}\n\nfunction hostOf(href: string): string | null {\n try {\n return new URL(href).hostname.toLowerCase() || null\n } catch {\n return null\n }\n}\n\n/* \u2500\u2500 Montagem \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/**\n * Contexto completo do evento atual. Fora do browser devolve `{}` \u2014 o SDK\n * server tem o pr\u00F3prio caminho e n\u00E3o deve inventar ambiente.\n */\nexport function buildWebContext(): WebContext {\n if (typeof window === 'undefined' || typeof location === 'undefined') return {}\n\n const href = location.href\n const referrer = typeof document !== 'undefined' ? document.referrer : ''\n const host = location.hostname.toLowerCase()\n\n const ua = typeof navigator !== 'undefined' ? navigator.userAgent : ''\n const { browser, os, device } = parseUserAgent(ua)\n\n const url = stripUrl(href)\n\n return {\n ...(url ? { url } : {}),\n path: location.pathname || '/',\n host,\n ...(browser ? { browser } : {}),\n ...(os ? { os } : {}),\n ...(device ? { device } : {}),\n ...(typeof navigator !== 'undefined' && navigator.language\n ? { locale: navigator.language }\n : {}),\n session_id: resolveSessionId(),\n ...resolveAttribution(href, referrer, host),\n }\n}\n", "/**\n * @holdyapp/sdk/web \u00B7 browser (source: sdk_web).\n *\n * anonymous_id gerado e persistido em localStorage (holdy_anon_id) \u2014 enviado em\n * todo event at\u00E9 identify() promover pra user_id (identity graph faz o merge\n * downstream, nunca o SDK). Flush no pagehide via fetch keepalive.\n *\n * Todo evento carrega o contexto de ambiente (url, path, referrer externo,\n * utm da sess\u00E3o, browser, os, device, locale, session_id) em `context` \u2014\n * separado das properties do produto. Pageview \u00E9 autom\u00E1tico por default:\n * carregamento inicial e cada mudan\u00E7a de rota, inclusive em SPA.\n */\n\nimport { HoldyCore } from './core'\nimport { installAutoCapture, type NavigationKind } from './auto-capture'\nimport { buildWebContext } from './web-context'\nimport type { HoldyOptions, Properties } from './types'\n\nconst ANON_STORAGE_KEY = 'holdy_anon_id'\n\nexport interface HoldyWebOptions extends HoldyOptions {\n /**\n * Emitir `$pageview` sozinho no load e a cada mudan\u00E7a de rota \u00B7 default true.\n * Desligue s\u00F3 se o produto j\u00E1 chama `page()` na m\u00E3o em cada tela.\n */\n autoCapture?: boolean\n}\n\nexport class HoldyWeb extends HoldyCore {\n private teardownAutoCapture: (() => void) | null = null\n\n constructor(opts: HoldyWebOptions) {\n super('sdk_web', opts)\n this.anonymousId = resolveAnonymousId()\n\n if (typeof window !== 'undefined') {\n window.addEventListener('pagehide', () => void this.flush())\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'hidden') void this.flush()\n })\n }\n\n if (opts.autoCapture !== false) {\n this.teardownAutoCapture = installAutoCapture({\n onNavigate: (kind: NavigationKind) => this.page(undefined, { navigation: kind }),\n })\n }\n }\n\n /**\n * Pageview. O `$` marca evento emitido pelo SDK, n\u00E3o pelo produto (mesma\n * conven\u00E7\u00E3o de `$identify`/`$group`); a canonicaliza\u00E7\u00E3o downstream remove o\n * sigilo, ent\u00E3o `$pageview` e `pageview` s\u00E3o o mesmo evento na leitura.\n * url/path/referrer/utm j\u00E1 v\u00EAm no context de todo evento \u2014 aqui s\u00F3 entra o\n * que \u00E9 da p\u00E1gina como conte\u00FAdo (nome e t\u00EDtulo).\n */\n page(name?: string, properties?: Properties): void {\n this.track('$pageview', {\n ...(properties ?? {}),\n ...(name ? { page_name: name } : {}),\n ...(typeof document !== 'undefined' && document.title ? { title: document.title } : {}),\n })\n }\n\n reset(): void {\n this.userId = null\n this.accountKey = null\n try {\n localStorage.removeItem(ANON_STORAGE_KEY)\n } catch {\n // storage indispon\u00EDvel (SSR/privacy mode) \u2014 ok\n }\n this.anonymousId = resolveAnonymousId()\n }\n\n /** Flush final + desinstala o auto-capture (devolve `history` ao original). */\n override async shutdown(): Promise<void> {\n this.teardownAutoCapture?.()\n this.teardownAutoCapture = null\n await super.shutdown()\n }\n\n protected override environmentContext(): Properties {\n return buildWebContext()\n }\n}\n\nfunction resolveAnonymousId(): string {\n try {\n const existing = localStorage.getItem(ANON_STORAGE_KEY)\n if (existing) return existing\n const fresh = crypto.randomUUID()\n localStorage.setItem(ANON_STORAGE_KEY, fresh)\n return fresh\n } catch {\n return crypto.randomUUID()\n }\n}\n\nlet singleton: HoldyWeb | null = null\n\n/** Init idempotente \u2014 segunda chamada retorna a inst\u00E2ncia existente. */\nexport function initHoldy(opts: HoldyWebOptions): HoldyWeb {\n if (!singleton) {\n singleton = new HoldyWeb(opts)\n // Inst\u00E2ncia no window: track() inserido pelo `--instrument` do CLI n\u00E3o\n // precisa importar nada nem saber onde o init mora. Mesma superf\u00EDcie do\n // loader por script tag, ent\u00E3o o snippet \u00E9 um s\u00F3 nos dois caminhos.\n if (typeof window !== 'undefined') {\n const w = window as unknown as { Holdy?: HoldyWeb }\n if (!w.Holdy) w.Holdy = singleton\n }\n }\n return singleton\n}\n\nexport type { HoldyOptions, Properties }\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@holdyapp/sdk",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "Holdy SDK
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Holdy SDK · track/identify/group/page → POST /api/v1/events (entry-contract v1) · auto-capture web",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://holdy.chat",
|
|
7
7
|
"repository": {
|
package/src/web.ts
CHANGED
|
@@ -101,7 +101,16 @@ let singleton: HoldyWeb | null = null
|
|
|
101
101
|
|
|
102
102
|
/** Init idempotente — segunda chamada retorna a instância existente. */
|
|
103
103
|
export function initHoldy(opts: HoldyWebOptions): HoldyWeb {
|
|
104
|
-
if (!singleton)
|
|
104
|
+
if (!singleton) {
|
|
105
|
+
singleton = new HoldyWeb(opts)
|
|
106
|
+
// Instância no window: track() inserido pelo `--instrument` do CLI não
|
|
107
|
+
// precisa importar nada nem saber onde o init mora. Mesma superfície do
|
|
108
|
+
// loader por script tag, então o snippet é um só nos dois caminhos.
|
|
109
|
+
if (typeof window !== 'undefined') {
|
|
110
|
+
const w = window as unknown as { Holdy?: HoldyWeb }
|
|
111
|
+
if (!w.Holdy) w.Holdy = singleton
|
|
112
|
+
}
|
|
113
|
+
}
|
|
105
114
|
return singleton
|
|
106
115
|
}
|
|
107
116
|
|