@waniwani/sdk 0.17.2 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,7 +17,7 @@ interface KbSearchTrace {
17
17
  * first-class names at runtime (the widget transport passes these through
18
18
  * verbatim instead of namespacing them as custom widget events).
19
19
  */
20
- declare const EVENT_TYPES: readonly ["session.started", "page.viewed", "tool.called", "quote.requested", "quote.succeeded", "quote.failed", "link.clicked", "purchase.completed", "widget_render", "user.identified", "price_shown", "prices_compared", "option_selected", "lead_qualified", "converted"];
20
+ declare const EVENT_TYPES: readonly ["session.started", "page.viewed", "tool.called", "link.clicked", "widget_render", "user.identified", "price_shown", "prices_compared", "option_selected", "lead_qualified", "converted"];
21
21
  type EventType = (typeof EVENT_TYPES)[number];
22
22
  /**
23
23
  * Properties for `page.viewed` — emitted once when the chat widget initializes
@@ -46,17 +46,9 @@ interface ToolCalledProperties {
46
46
  /** Retrieval traces for kb.search() calls made inside this tool handler. */
47
47
  kbSearch?: KbSearchTrace[];
48
48
  }
49
- interface QuoteSucceededProperties {
50
- amount?: number;
51
- currency?: string;
52
- }
53
49
  interface LinkClickedProperties {
54
50
  url?: string;
55
51
  }
56
- interface PurchaseCompletedProperties {
57
- amount?: number;
58
- currency?: string;
59
- }
60
52
  interface PriceShownProperties {
61
53
  amount: number;
62
54
  currency: string;
@@ -143,19 +135,9 @@ type TrackEvent = ({
143
135
  } & BaseTrackEvent) | ({
144
136
  event: "tool.called";
145
137
  properties?: ToolCalledProperties;
146
- } & BaseTrackEvent) | ({
147
- event: "quote.requested";
148
- } & BaseTrackEvent) | ({
149
- event: "quote.succeeded";
150
- properties?: QuoteSucceededProperties;
151
- } & BaseTrackEvent) | ({
152
- event: "quote.failed";
153
138
  } & BaseTrackEvent) | ({
154
139
  event: "link.clicked";
155
140
  properties?: LinkClickedProperties;
156
- } & BaseTrackEvent) | ({
157
- event: "purchase.completed";
158
- properties?: PurchaseCompletedProperties;
159
141
  } & BaseTrackEvent) | ({
160
142
  event: "user.identified";
161
143
  } & BaseTrackEvent) | ({
@@ -184,11 +166,7 @@ interface LegacyTrackEvent extends TrackingContext {
184
166
  properties?: Record<string, unknown>;
185
167
  toolName?: string;
186
168
  toolType?: ToolCalledProperties["type"];
187
- quoteAmount?: number;
188
- quoteCurrency?: string;
189
169
  linkUrl?: string;
190
- purchaseAmount?: number;
191
- purchaseCurrency?: string;
192
170
  }
193
171
  /**
194
172
  * Public track input accepted by `client.track()`.
@@ -312,7 +290,7 @@ interface WaniwaniWidget {
312
290
  readonly sessionId?: string;
313
291
  /**
314
292
  * Track a typed event. The exact same surface as the server client:
315
- * `track({ event: "quote.succeeded", properties })`,
293
+ * `track({ event: "session.started" })`,
316
294
  * `track.priceShown({ amount, currency })`, `track.converted({ ... })`.
317
295
  * Identity (session, trace, user) is stamped automatically.
318
296
  */
@@ -1,3 +1,3 @@
1
1
  "use client";
2
- "use client";import{useToolInfo as me}from"skybridge/web";import{useEffect as de,useMemo as ue,useRef as ce,useState as le}from"react";function v(e,n){for(let t of n){let r=e[t];if(typeof r=="string"&&r.length>0)return r}}var V=["waniwani/sessionId","openai/sessionId","openai/session","sessionId","conversationId","mcp-session-id"],P=["waniwani/requestId","openai/requestId","requestId","mcp/requestId"],N=["waniwani/traceId","openai/traceId","traceId","mcp/traceId","openai/requestId","requestId"],q=["waniwani/userId","openai/userId","externalUserId","userId","actorId"],K=["waniwani/visitorId","visitorId"],Y=["correlationId","openai/requestId"];var T="waniwani/widget";function R(e){return e?v(e,V):void 0}function x(e){return e?v(e,P):void 0}function _(e){return e?v(e,N):void 0}function b(e){return e?v(e,q):void 0}function M(e){return e?v(e,K):void 0}function F(e){return e?v(e,Y):void 0}var j=[{key:"openai/sessionId",source:"chatgpt"},{key:"openai/session",source:"chatgpt"}],z=[{needle:"claude",source:"claude"},{needle:"chatgpt",source:"chatgpt"},{needle:"openai",source:"chatgpt"},{needle:"gemini",source:"gemini"}];function D(e,n){if(e){let r=e["waniwani/source"];if(typeof r=="string"&&r.length>0)return r;for(let{key:i,source:s}of j){let a=e[i];if(typeof a=="string"&&a.length>0)return s}}let t=n?.name;if(typeof t=="string"&&t.length>0){let r=t.toLowerCase();for(let{needle:i,source:s}of z)if(r.includes(i))return s}}var H="@waniwani/sdk";function U(e,n={}){let t=n.now??(()=>new Date),r=n.generateId??X,i=J(e),s=y(e.meta),a=y(e.metadata),o=Z(e,s),d=u(e.eventId)??r(),h=Q(e.timestamp,t),l=u(e.source)??D(s)??n.source??H,p=E(e)?{...e}:void 0,m={...a};return Object.keys(s).length>0&&(m.meta=s),p&&(m.rawLegacy=p),{id:d,type:"mcp.event",name:i,source:l,timestamp:h,correlation:o,properties:$(e,i),metadata:m,rawLegacy:p}}function X(){return typeof crypto<"u"&&typeof crypto.randomUUID=="function"?`evt_${crypto.randomUUID()}`:`evt_${Math.random().toString(36).slice(2,10)}_${Date.now().toString(36)}`}function $(e,n){if(!E(e))return y(e.properties);let t=G(e,n),r=y(e.properties);return{...t,...r}}function G(e,n){switch(n){case"tool.called":{let t={};return u(e.toolName)&&(t.name=e.toolName),u(e.toolType)&&(t.type=e.toolType),t}case"quote.succeeded":{let t={};return typeof e.quoteAmount=="number"&&(t.amount=e.quoteAmount),u(e.quoteCurrency)&&(t.currency=e.quoteCurrency),t}case"link.clicked":{let t={};return u(e.linkUrl)&&(t.url=e.linkUrl),t}case"purchase.completed":{let t={};return typeof e.purchaseAmount=="number"&&(t.amount=e.purchaseAmount),u(e.purchaseCurrency)&&(t.currency=e.purchaseCurrency),t}default:return{}}}function J(e){return E(e)?e.eventType:e.event}function Z(e,n){let t=u(e.requestId)??x(n),r=u(e.sessionId)??R(n),i=u(e.traceId)??_(n),s=u(e.externalUserId)??b(n),a=u(e.visitorId)??M(n),o=u(e.correlationId)??F(n)??t,d={};return r&&(d.sessionId=r),i&&(d.traceId=i),t&&(d.requestId=t),o&&(d.correlationId=o),s&&(d.externalUserId=s),a&&(d.visitorId=a),d}function Q(e,n){if(e instanceof Date)return e.toISOString();if(typeof e=="string"){let t=new Date(e);if(!Number.isNaN(t.getTime()))return t.toISOString()}return n().toISOString()}function y(e){return!e||typeof e!="object"||Array.isArray(e)?{}:e}function u(e){if(typeof e=="string"&&e.trim().length!==0)return e}function E(e){return"eventType"in e}function A(e){return{priceShown:({amount:n,currency:t,itemId:r,label:i,...s})=>e({event:"price_shown",properties:{amount:n,currency:t,itemId:r,label:i},...s}),pricesCompared:({options:n,...t})=>e({event:"prices_compared",properties:{options:n},...t}),optionSelected:({id:n,amount:t,currency:r,...i})=>e({event:"option_selected",properties:{id:n,amount:t,currency:r},...i}),leadQualified:n=>{let{externalId:t,email:r,name:i,...s}=n??{};return e({event:"lead_qualified",properties:{externalId:t,email:r,name:i},...s})},converted:({amount:n,currency:t,occurredAt:r,...i})=>e({event:"converted",properties:{amount:n,currency:t,occurredAt:r},...i})}}var ee="/api/mcp/events/v2/batch";var O="@waniwani/sdk";var ne=new Set([401,403]),te=new Set([408,425,429,500,502,503,504]);function C(e){return new k(e)}var k=class{endpointUrl;flushIntervalMs;maxBatchSize;maxBufferSize;maxRetries;retryBaseDelayMs;retryMaxDelayMs;shutdownTimeoutMs;sdkVersion;fetchFn;logger;now;sleep;apiKey;buffer=[];flushTimer;flushScheduled=!1;flushScheduledTimer;flushInFlight;inFlightCount=0;isStopped=!1;isShuttingDown=!1;constructor(n){this.endpointUrl=se(n.apiUrl,n.endpointPath??ee),this.flushIntervalMs=n.flushIntervalMs??1e3,this.maxBatchSize=n.maxBatchSize??20,this.maxBufferSize=n.maxBufferSize??1e3,this.maxRetries=n.maxRetries??3,this.retryBaseDelayMs=n.retryBaseDelayMs??200,this.retryMaxDelayMs=n.retryMaxDelayMs??2e3,this.shutdownTimeoutMs=n.shutdownTimeoutMs??2e3,this.fetchFn=n.fetchFn??fetch,this.logger=n.logger??console,this.now=n.now??(()=>new Date),this.sleep=n.sleep??(t=>new Promise(r=>setTimeout(r,t))),this.apiKey=n.apiKey,this.sdkVersion=n.sdkVersion,this.flushIntervalMs>0&&(this.flushTimer=setInterval(()=>{this.flush()},this.flushIntervalMs)),this.registerBrowserTeardown()}enqueue(n){if(this.isStopped||this.isShuttingDown){this.logger.warn("[Waniwani] Tracking transport is stopped, dropping event %s",n.id);return}if(this.buffer.length>=this.maxBufferSize){let t=this.buffer.length-this.maxBufferSize+1;this.buffer.splice(0,t),this.logger.warn("[Waniwani] Tracking buffer overflow, dropped %d oldest event(s)",t)}if(this.buffer.push(n),this.buffer.length>=this.maxBatchSize){this.flush();return}this.scheduleMicroFlush()}pendingEvents(){return this.buffer.length+this.inFlightCount}async flush(){return this.flushInFlight?this.flushInFlight:(this.flushInFlight=this.flushLoop().finally(()=>{this.flushInFlight=void 0}),this.flushInFlight)}async shutdown(n){this.isShuttingDown=!0,this.flushTimer&&(clearInterval(this.flushTimer),this.flushTimer=void 0),this.flushScheduledTimer&&(clearTimeout(this.flushScheduledTimer),this.flushScheduledTimer=void 0,this.flushScheduled=!1);let t=n?.timeoutMs??this.shutdownTimeoutMs,r=this.flush();if(!Number.isFinite(t)||t<=0)return await r,this.isStopped=!0,{timedOut:!1,pendingEvents:this.pendingEvents()};let i=Symbol("shutdown-timeout");return await Promise.race([r.then(()=>"flushed"),this.sleep(t).then(()=>i)])===i?(this.isStopped=!0,{timedOut:!0,pendingEvents:this.pendingEvents()}):(this.isStopped=!0,{timedOut:!1,pendingEvents:this.pendingEvents()})}registerBrowserTeardown(){if(typeof document>"u"||typeof window>"u")return;let n=()=>{document.visibilityState==="hidden"&&this.keepaliveFlush()};document.addEventListener("visibilitychange",n),window.addEventListener("pagehide",()=>this.keepaliveFlush())}keepaliveFlush(){if(this.isStopped||this.buffer.length===0)return;let n=this.buffer.splice(0,this.buffer.length);this.sendKeepaliveChunk(n)}sendKeepaliveChunk(n){let t=JSON.stringify(this.makeBatchRequest(n));if(t.length>6e4&&n.length>1){let r=Math.ceil(n.length/2);this.sendKeepaliveChunk(n.slice(0,r)),this.sendKeepaliveChunk(n.slice(r));return}try{this.fetchFn(this.endpointUrl,{method:"POST",headers:this.requestHeaders(),body:t,keepalive:!0}).catch(()=>{})}catch{}}requestHeaders(){let n={"Content-Type":"application/json","X-WaniWani-SDK":O};return this.apiKey&&(n.Authorization=`Bearer ${this.apiKey}`),n}scheduleMicroFlush(){this.flushScheduled||(this.flushScheduled=!0,this.flushScheduledTimer=setTimeout(()=>{this.flushScheduledTimer=void 0,this.flushScheduled=!1,this.flush()},0))}async flushLoop(){for(;this.buffer.length>0&&!this.isStopped;){let n=this.buffer.splice(0,this.maxBatchSize);await this.sendBatchWithRetry(n)}}async sendBatchWithRetry(n){let t=0,r=n;for(;r.length>0&&!this.isStopped;){this.inFlightCount=r.length;let i=await this.sendBatchOnce(r);switch(this.inFlightCount=0,i.kind){case"success":return;case"auth":this.stopTransportForAuthFailure(i.status,r.length);return;case"permanent":this.logger.error("[Waniwani] Dropping %d event(s) after permanent failure: %s",r.length,i.reason);return;case"retryable":if(t>=this.maxRetries){this.logger.error("[Waniwani] Dropping %d event(s) after retry exhaustion: %s",r.length,i.reason);return}await this.sleep(this.backoffDelayMs(t)),t+=1;continue;case"partial":if(i.permanent.length>0&&this.logger.error("[Waniwani] Dropping %d event(s) rejected as permanent",i.permanent.length),i.retryable.length===0)return;if(t>=this.maxRetries){this.logger.error("[Waniwani] Dropping %d retryable event(s) after retry exhaustion",i.retryable.length);return}r=i.retryable,await this.sleep(this.backoffDelayMs(t)),t+=1;continue}}}async sendBatchOnce(n){let t;try{t=await this.fetchFn(this.endpointUrl,{method:"POST",headers:this.requestHeaders(),body:JSON.stringify(this.makeBatchRequest(n))})}catch(s){return{kind:"retryable",reason:oe(s)}}if(ne.has(t.status))return{kind:"auth",status:t.status};if(te.has(t.status))return{kind:"retryable",reason:`HTTP ${t.status}`};if(!t.ok)return{kind:"permanent",reason:`HTTP ${t.status}`};let r=await ie(t);if(!r?.rejected||r.rejected.length===0)return{kind:"success"};let i=this.classifyRejectedEvents(n,r.rejected);return i.retryable.length===0&&i.permanent.length===0?{kind:"success"}:{kind:"partial",retryable:i.retryable,permanent:i.permanent}}makeBatchRequest(n){return{sentAt:this.now().toISOString(),source:{sdk:O,version:this.sdkVersion??"0.0.0"},events:n}}classifyRejectedEvents(n,t){let r=new Map(n.map(a=>[a.id,a])),i=[],s=[];for(let a of t){let o=r.get(a.eventId);if(o){if(re(a)){i.push(o);continue}s.push(o)}}return{retryable:i,permanent:s}}backoffDelayMs(n){let t=this.retryBaseDelayMs*2**n;return Math.min(t,this.retryMaxDelayMs)}stopTransportForAuthFailure(n,t){this.isStopped=!0;let r=this.buffer.length;this.buffer.splice(0,r),this.logger.error("[Waniwani] Auth failure (HTTP %d). Stopping tracking transport and dropping %d queued event(s)",n,t+r)}};function re(e){if(e.retryable===!0)return!0;let n=e.code.toLowerCase();return n.includes("timeout")||n.includes("temporary")||n.includes("unavailable")||n.includes("rate_limit")||n.includes("transient")||n.includes("server")}async function ie(e){let n=await e.text();if(n)try{return JSON.parse(n)}catch{return}}function se(e,n){let t=e.endsWith("/")?e:`${e}/`,r=n.startsWith("/")?n.slice(1):n;return`${t}${r}`}function oe(e){return e instanceof Error?e.message:String(e)}function W(e){let{apiUrl:n,endpointPath:t}=ae(e.endpoint),r=C({apiUrl:n,apiKey:e.token,endpointPath:t,flushIntervalMs:e.flushIntervalMs}),i,s=o=>typeof o=="function"?o():o,a=async o=>{let d=e.identity?.()??{},h=s(e.channelId),l=h===void 0?o.properties:{channelId:h,...o.properties},p=U({sessionId:d.sessionId,visitorId:d.visitorId,externalUserId:d.externalUserId??i,traceId:d.traceId,...o,properties:l,metadata:{...e.metadata,...o.metadata}},{source:s(e.source)});return r.enqueue(p),{eventId:p.id}};return{track:Object.assign(a,A(a)),identify(o,d){return i=o,a({event:"user.identified",externalUserId:o,properties:d})},flush:()=>r.flush(),shutdown:o=>r.shutdown(o)}}function ae(e){let n=new URL(e);return{apiUrl:n.origin,endpointPath:`${n.pathname}${n.search}`}}var g=async()=>({eventId:""});function pe(){return Object.assign(g,{priceShown:g,pricesCompared:g,optionSelected:g,leadQualified:g,converted:g})}var B={sessionId:void 0,track:pe(),identify:g,flush:async()=>{}},w=null,I=0;function f(e){if(typeof e!="string")return;let n=e.trim();return n.length>0?n:void 0}function fe(e){if(!e)return null;let n=e._meta,t=e[T]??n?.[T],r=f(t?.endpoint);return r?{endpoint:r,token:f(t?.token),sessionId:f(t?.sessionId),source:f(t?.source)}:null}function he(e,n){return e?.endpoint===n?.endpoint&&e?.token===n?.token&&e?.sessionId===n?.sessionId&&e?.source===n?.source}function ge(e,n){let t=e.sessionId??crypto.randomUUID(),r=crypto.randomUUID(),i=W({endpoint:e.endpoint,token:e.token,source:e.source,identity:()=>({sessionId:t,traceId:r}),metadata:n});return i.track({event:"widget_render"}),{config:e,widget:{sessionId:t,track:i.track,identify:(s,a)=>i.identify(s,a),flush:()=>i.flush()},cleanup:()=>{i.shutdown()}}}function L(e={}){let n=f(e.endpoint),t=f(e.token),r=f(e.sessionId),i=f(e.source),s=n?null:fe(e.toolResponseMetadata??null),a=s?.endpoint,o=s?.token,d=s?.sessionId,h=s?.source,l=ue(()=>{let c=i??h;return c?n?{endpoint:n,token:t,sessionId:r,source:c}:a?{endpoint:a,token:t??o,sessionId:r??d,source:c}:null:null},[n,t,r,i,a,o,d,h]),[p,m]=le(B),S=ce(e.metadata);return S.current=e.metadata,de(()=>{if(typeof window>"u")return;if(!l){m(B);return}let c=w;return(!c||!he(c.config,l))&&(c?.cleanup(),c=ge(l,S.current),w=c),m(c.widget),I++,()=>{I=Math.max(I-1,0),I===0&&(w?.cleanup(),w=null)}},[l]),p}function Ue(e={}){let{responseMetadata:n}=me();return L({...e,toolResponseMetadata:n})}export{Ue as useWaniwani};
2
+ "use client";import{useToolInfo as me}from"skybridge/web";import{useEffect as de,useMemo as ue,useRef as ce,useState as le}from"react";function v(e,n){for(let t of n){let r=e[t];if(typeof r=="string"&&r.length>0)return r}}var V=["waniwani/sessionId","openai/sessionId","openai/session","sessionId","conversationId","mcp-session-id"],P=["waniwani/requestId","openai/requestId","requestId","mcp/requestId"],N=["waniwani/traceId","openai/traceId","traceId","mcp/traceId","openai/requestId","requestId"],q=["waniwani/userId","openai/userId","externalUserId","userId","actorId"],K=["waniwani/visitorId","visitorId"],Y=["correlationId","openai/requestId"];var T="waniwani/widget";function R(e){return e?v(e,V):void 0}function x(e){return e?v(e,P):void 0}function _(e){return e?v(e,N):void 0}function M(e){return e?v(e,q):void 0}function b(e){return e?v(e,K):void 0}function F(e){return e?v(e,Y):void 0}var j=[{key:"openai/sessionId",source:"chatgpt"},{key:"openai/session",source:"chatgpt"}],z=[{needle:"claude",source:"claude"},{needle:"chatgpt",source:"chatgpt"},{needle:"openai",source:"chatgpt"},{needle:"gemini",source:"gemini"}];function D(e,n){if(e){let r=e["waniwani/source"];if(typeof r=="string"&&r.length>0)return r;for(let{key:i,source:s}of j){let a=e[i];if(typeof a=="string"&&a.length>0)return s}}let t=n?.name;if(typeof t=="string"&&t.length>0){let r=t.toLowerCase();for(let{needle:i,source:s}of z)if(r.includes(i))return s}}var H="@waniwani/sdk";function U(e,n={}){let t=n.now??(()=>new Date),r=n.generateId??X,i=J(e),s=y(e.meta),a=y(e.metadata),o=Z(e,s),d=u(e.eventId)??r(),h=Q(e.timestamp,t),l=u(e.source)??D(s)??n.source??H,p=E(e)?{...e}:void 0,m={...a};return Object.keys(s).length>0&&(m.meta=s),p&&(m.rawLegacy=p),{id:d,type:"mcp.event",name:i,source:l,timestamp:h,correlation:o,properties:$(e,i),metadata:m,rawLegacy:p}}function X(){return typeof crypto<"u"&&typeof crypto.randomUUID=="function"?`evt_${crypto.randomUUID()}`:`evt_${Math.random().toString(36).slice(2,10)}_${Date.now().toString(36)}`}function $(e,n){if(!E(e))return y(e.properties);let t=G(e,n),r=y(e.properties);return{...t,...r}}function G(e,n){switch(n){case"tool.called":{let t={};return u(e.toolName)&&(t.name=e.toolName),u(e.toolType)&&(t.type=e.toolType),t}case"link.clicked":{let t={};return u(e.linkUrl)&&(t.url=e.linkUrl),t}default:return{}}}function J(e){return E(e)?e.eventType:e.event}function Z(e,n){let t=u(e.requestId)??x(n),r=u(e.sessionId)??R(n),i=u(e.traceId)??_(n),s=u(e.externalUserId)??M(n),a=u(e.visitorId)??b(n),o=u(e.correlationId)??F(n)??t,d={};return r&&(d.sessionId=r),i&&(d.traceId=i),t&&(d.requestId=t),o&&(d.correlationId=o),s&&(d.externalUserId=s),a&&(d.visitorId=a),d}function Q(e,n){if(e instanceof Date)return e.toISOString();if(typeof e=="string"){let t=new Date(e);if(!Number.isNaN(t.getTime()))return t.toISOString()}return n().toISOString()}function y(e){return!e||typeof e!="object"||Array.isArray(e)?{}:e}function u(e){if(typeof e=="string"&&e.trim().length!==0)return e}function E(e){return"eventType"in e}function O(e){return{priceShown:({amount:n,currency:t,itemId:r,label:i,...s})=>e({event:"price_shown",properties:{amount:n,currency:t,itemId:r,label:i},...s}),pricesCompared:({options:n,...t})=>e({event:"prices_compared",properties:{options:n},...t}),optionSelected:({id:n,amount:t,currency:r,...i})=>e({event:"option_selected",properties:{id:n,amount:t,currency:r},...i}),leadQualified:n=>{let{externalId:t,email:r,name:i,...s}=n??{};return e({event:"lead_qualified",properties:{externalId:t,email:r,name:i},...s})},converted:({amount:n,currency:t,occurredAt:r,...i})=>e({event:"converted",properties:{amount:n,currency:t,occurredAt:r},...i})}}var ee="/api/mcp/events/v2/batch";var A="@waniwani/sdk";var ne=new Set([401,403]),te=new Set([408,425,429,500,502,503,504]);function W(e){return new k(e)}var k=class{endpointUrl;flushIntervalMs;maxBatchSize;maxBufferSize;maxRetries;retryBaseDelayMs;retryMaxDelayMs;shutdownTimeoutMs;sdkVersion;fetchFn;logger;now;sleep;apiKey;buffer=[];flushTimer;flushScheduled=!1;flushScheduledTimer;flushInFlight;inFlightCount=0;isStopped=!1;isShuttingDown=!1;constructor(n){this.endpointUrl=se(n.apiUrl,n.endpointPath??ee),this.flushIntervalMs=n.flushIntervalMs??1e3,this.maxBatchSize=n.maxBatchSize??20,this.maxBufferSize=n.maxBufferSize??1e3,this.maxRetries=n.maxRetries??3,this.retryBaseDelayMs=n.retryBaseDelayMs??200,this.retryMaxDelayMs=n.retryMaxDelayMs??2e3,this.shutdownTimeoutMs=n.shutdownTimeoutMs??2e3,this.fetchFn=n.fetchFn??fetch,this.logger=n.logger??console,this.now=n.now??(()=>new Date),this.sleep=n.sleep??(t=>new Promise(r=>setTimeout(r,t))),this.apiKey=n.apiKey,this.sdkVersion=n.sdkVersion,this.flushIntervalMs>0&&(this.flushTimer=setInterval(()=>{this.flush()},this.flushIntervalMs)),this.registerBrowserTeardown()}enqueue(n){if(this.isStopped||this.isShuttingDown){this.logger.warn("[Waniwani] Tracking transport is stopped, dropping event %s",n.id);return}if(this.buffer.length>=this.maxBufferSize){let t=this.buffer.length-this.maxBufferSize+1;this.buffer.splice(0,t),this.logger.warn("[Waniwani] Tracking buffer overflow, dropped %d oldest event(s)",t)}if(this.buffer.push(n),this.buffer.length>=this.maxBatchSize){this.flush();return}this.scheduleMicroFlush()}pendingEvents(){return this.buffer.length+this.inFlightCount}async flush(){return this.flushInFlight?this.flushInFlight:(this.flushInFlight=this.flushLoop().finally(()=>{this.flushInFlight=void 0}),this.flushInFlight)}async shutdown(n){this.isShuttingDown=!0,this.flushTimer&&(clearInterval(this.flushTimer),this.flushTimer=void 0),this.flushScheduledTimer&&(clearTimeout(this.flushScheduledTimer),this.flushScheduledTimer=void 0,this.flushScheduled=!1);let t=n?.timeoutMs??this.shutdownTimeoutMs,r=this.flush();if(!Number.isFinite(t)||t<=0)return await r,this.isStopped=!0,{timedOut:!1,pendingEvents:this.pendingEvents()};let i=Symbol("shutdown-timeout");return await Promise.race([r.then(()=>"flushed"),this.sleep(t).then(()=>i)])===i?(this.isStopped=!0,{timedOut:!0,pendingEvents:this.pendingEvents()}):(this.isStopped=!0,{timedOut:!1,pendingEvents:this.pendingEvents()})}registerBrowserTeardown(){if(typeof document>"u"||typeof window>"u")return;let n=()=>{document.visibilityState==="hidden"&&this.keepaliveFlush()};document.addEventListener("visibilitychange",n),window.addEventListener("pagehide",()=>this.keepaliveFlush())}keepaliveFlush(){if(this.isStopped||this.buffer.length===0)return;let n=this.buffer.splice(0,this.buffer.length);this.sendKeepaliveChunk(n)}sendKeepaliveChunk(n){let t=JSON.stringify(this.makeBatchRequest(n));if(t.length>6e4&&n.length>1){let r=Math.ceil(n.length/2);this.sendKeepaliveChunk(n.slice(0,r)),this.sendKeepaliveChunk(n.slice(r));return}try{this.fetchFn(this.endpointUrl,{method:"POST",headers:this.requestHeaders(),body:t,keepalive:!0}).catch(()=>{})}catch{}}requestHeaders(){let n={"Content-Type":"application/json","X-WaniWani-SDK":A};return this.apiKey&&(n.Authorization=`Bearer ${this.apiKey}`),n}scheduleMicroFlush(){this.flushScheduled||(this.flushScheduled=!0,this.flushScheduledTimer=setTimeout(()=>{this.flushScheduledTimer=void 0,this.flushScheduled=!1,this.flush()},0))}async flushLoop(){for(;this.buffer.length>0&&!this.isStopped;){let n=this.buffer.splice(0,this.maxBatchSize);await this.sendBatchWithRetry(n)}}async sendBatchWithRetry(n){let t=0,r=n;for(;r.length>0&&!this.isStopped;){this.inFlightCount=r.length;let i=await this.sendBatchOnce(r);switch(this.inFlightCount=0,i.kind){case"success":return;case"auth":this.stopTransportForAuthFailure(i.status,r.length);return;case"permanent":this.logger.error("[Waniwani] Dropping %d event(s) after permanent failure: %s",r.length,i.reason);return;case"retryable":if(t>=this.maxRetries){this.logger.error("[Waniwani] Dropping %d event(s) after retry exhaustion: %s",r.length,i.reason);return}await this.sleep(this.backoffDelayMs(t)),t+=1;continue;case"partial":if(i.permanent.length>0&&this.logger.error("[Waniwani] Dropping %d event(s) rejected as permanent",i.permanent.length),i.retryable.length===0)return;if(t>=this.maxRetries){this.logger.error("[Waniwani] Dropping %d retryable event(s) after retry exhaustion",i.retryable.length);return}r=i.retryable,await this.sleep(this.backoffDelayMs(t)),t+=1;continue}}}async sendBatchOnce(n){let t;try{t=await this.fetchFn(this.endpointUrl,{method:"POST",headers:this.requestHeaders(),body:JSON.stringify(this.makeBatchRequest(n))})}catch(s){return{kind:"retryable",reason:oe(s)}}if(ne.has(t.status))return{kind:"auth",status:t.status};if(te.has(t.status))return{kind:"retryable",reason:`HTTP ${t.status}`};if(!t.ok)return{kind:"permanent",reason:`HTTP ${t.status}`};let r=await ie(t);if(!r?.rejected||r.rejected.length===0)return{kind:"success"};let i=this.classifyRejectedEvents(n,r.rejected);return i.retryable.length===0&&i.permanent.length===0?{kind:"success"}:{kind:"partial",retryable:i.retryable,permanent:i.permanent}}makeBatchRequest(n){return{sentAt:this.now().toISOString(),source:{sdk:A,version:this.sdkVersion??"0.0.0"},events:n}}classifyRejectedEvents(n,t){let r=new Map(n.map(a=>[a.id,a])),i=[],s=[];for(let a of t){let o=r.get(a.eventId);if(o){if(re(a)){i.push(o);continue}s.push(o)}}return{retryable:i,permanent:s}}backoffDelayMs(n){let t=this.retryBaseDelayMs*2**n;return Math.min(t,this.retryMaxDelayMs)}stopTransportForAuthFailure(n,t){this.isStopped=!0;let r=this.buffer.length;this.buffer.splice(0,r),this.logger.error("[Waniwani] Auth failure (HTTP %d). Stopping tracking transport and dropping %d queued event(s)",n,t+r)}};function re(e){if(e.retryable===!0)return!0;let n=e.code.toLowerCase();return n.includes("timeout")||n.includes("temporary")||n.includes("unavailable")||n.includes("rate_limit")||n.includes("transient")||n.includes("server")}async function ie(e){let n=await e.text();if(n)try{return JSON.parse(n)}catch{return}}function se(e,n){let t=e.endsWith("/")?e:`${e}/`,r=n.startsWith("/")?n.slice(1):n;return`${t}${r}`}function oe(e){return e instanceof Error?e.message:String(e)}function C(e){let{apiUrl:n,endpointPath:t}=ae(e.endpoint),r=W({apiUrl:n,apiKey:e.token,endpointPath:t,flushIntervalMs:e.flushIntervalMs}),i,s=o=>typeof o=="function"?o():o,a=async o=>{let d=e.identity?.()??{},h=s(e.channelId),l=h===void 0?o.properties:{channelId:h,...o.properties},p=U({sessionId:d.sessionId,visitorId:d.visitorId,externalUserId:d.externalUserId??i,traceId:d.traceId,...o,properties:l,metadata:{...e.metadata,...o.metadata}},{source:s(e.source)});return r.enqueue(p),{eventId:p.id}};return{track:Object.assign(a,O(a)),identify(o,d){return i=o,a({event:"user.identified",externalUserId:o,properties:d})},flush:()=>r.flush(),shutdown:o=>r.shutdown(o)}}function ae(e){let n=new URL(e);return{apiUrl:n.origin,endpointPath:`${n.pathname}${n.search}`}}var g=async()=>({eventId:""});function pe(){return Object.assign(g,{priceShown:g,pricesCompared:g,optionSelected:g,leadQualified:g,converted:g})}var B={sessionId:void 0,track:pe(),identify:g,flush:async()=>{}},I=null,w=0;function f(e){if(typeof e!="string")return;let n=e.trim();return n.length>0?n:void 0}function fe(e){if(!e)return null;let n=e._meta,t=e[T]??n?.[T],r=f(t?.endpoint);return r?{endpoint:r,token:f(t?.token),sessionId:f(t?.sessionId),source:f(t?.source)}:null}function he(e,n){return e?.endpoint===n?.endpoint&&e?.token===n?.token&&e?.sessionId===n?.sessionId&&e?.source===n?.source}function ge(e,n){let t=e.sessionId??crypto.randomUUID(),r=crypto.randomUUID(),i=C({endpoint:e.endpoint,token:e.token,source:e.source,identity:()=>({sessionId:t,traceId:r}),metadata:n});return i.track({event:"widget_render"}),{config:e,widget:{sessionId:t,track:i.track,identify:(s,a)=>i.identify(s,a),flush:()=>i.flush()},cleanup:()=>{i.shutdown()}}}function L(e={}){let n=f(e.endpoint),t=f(e.token),r=f(e.sessionId),i=f(e.source),s=n?null:fe(e.toolResponseMetadata??null),a=s?.endpoint,o=s?.token,d=s?.sessionId,h=s?.source,l=ue(()=>{let c=i??h;return c?n?{endpoint:n,token:t,sessionId:r,source:c}:a?{endpoint:a,token:t??o,sessionId:r??d,source:c}:null:null},[n,t,r,i,a,o,d,h]),[p,m]=le(B),S=ce(e.metadata);return S.current=e.metadata,de(()=>{if(typeof window>"u")return;if(!l){m(B);return}let c=I;return(!c||!he(c.config,l))&&(c?.cleanup(),c=ge(l,S.current),I=c),m(c.widget),w++,()=>{w=Math.max(w-1,0),w===0&&(I?.cleanup(),I=null)}},[l]),p}function Ue(e={}){let{responseMetadata:n}=me();return L({...e,toolResponseMetadata:n})}export{Ue as useWaniwani};
3
3
  //# sourceMappingURL=skybridge.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/mcp/react/skybridge.ts","../../../src/mcp/react/hooks/use-waniwani.ts","../../../src/mcp/server/utils.ts","../../../src/tracking/mapper.ts","../../../src/tracking/revenue.ts","../../../src/tracking/transport.ts","../../../src/tracking/frontend.ts"],"sourcesContent":["\"use client\";\n\n// `@waniwani/sdk/mcp/react/skybridge` — the skybridge-host adapter for\n// `useWaniwani`.\n//\n// The pure `@waniwani/sdk/mcp/react` hook is host-agnostic: it takes the tool\n// response `_meta` as data and never opens a host connection of its own. That\n// keeps the core dependency-free, but it means the widget author has to hand\n// the metadata in.\n//\n// On an MCP-Apps host the tool result `_meta` is delivered once, as the\n// `ui/notifications/tool-result` notification, to whichever `App` is connected\n// and listening at that moment. In a skybridge widget that App is skybridge —\n// it captures the result and exposes it as `useToolInfo().responseMetadata`.\n// This adapter reads that value and feeds it to the core hook, so widgets get\n// the full tracking surface from a bare call:\n//\n// ```tsx\n// import { useWaniwani } from \"@waniwani/sdk/mcp/react/skybridge\";\n//\n// function BookCallWidget() {\n// const wani = useWaniwani();\n// wani.track.leadQualified({ email, name });\n// }\n// ```\n//\n// skybridge is an optional peer dependency, resolved in the consumer app. The\n// SDK depends only on the tiny slice declared in `skybridge-web.d.ts`, so it\n// need not install skybridge to typecheck or build.\n\nimport { useToolInfo } from \"skybridge/web\";\nimport {\n\ttype UseWaniwaniOptions,\n\tuseWaniwani as useWaniwaniCore,\n\ttype WaniwaniWidget,\n} from \"./hooks/use-waniwani\";\n\n/**\n * Options for the skybridge `useWaniwani` adapter. Derived from the core hook's\n * metadata-driven options (the `endpoint?: undefined` branch of\n * {@link UseWaniwaniOptions}) minus the two fields the adapter supplies itself:\n * `endpoint`/`source` and `token`/`sessionId` are resolved from skybridge's\n * `responseMetadata` (`_meta[\"waniwani/widget\"]`), and `toolResponseMetadata` is\n * read from `useToolInfo()`. What remains — `source`/`token`/`sessionId`\n * overrides and `metadata` passthrough — keeps its field docs from the core.\n */\nexport type SkybridgeWaniwaniOptions = Omit<\n\tExtract<UseWaniwaniOptions, { endpoint?: undefined }>,\n\t\"endpoint\" | \"toolResponseMetadata\"\n>;\n\nexport type { WaniwaniWidget } from \"./hooks/use-waniwani\";\n\n/**\n * `useWaniwani` for skybridge-hosted MCP-app widgets. Resolves the Waniwani\n * config from skybridge's `useToolInfo().responseMetadata` and returns the same\n * `track` surface as the core hook, with session identity stamped\n * automatically. Call it bare — no config threading, no provider.\n *\n * @example\n * ```tsx\n * const wani = useWaniwani();\n * wani.track.optionSelected({ id: \"pro\", amount: 49, currency: \"EUR\" });\n * ```\n */\nexport function useWaniwani(\n\toptions: SkybridgeWaniwaniOptions = {},\n): WaniwaniWidget {\n\tconst { responseMetadata } = useToolInfo();\n\treturn useWaniwaniCore({\n\t\t...options,\n\t\ttoolResponseMetadata: responseMetadata,\n\t});\n}\n","\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport type { TrackFn } from \"../../../tracking/@types\";\nimport { createFrontendClient } from \"../../../tracking/frontend\";\nimport { WIDGET_CONFIG_META_KEY } from \"../../server/utils\";\n\n/**\n * Waniwani widget config injected into tool response `_meta` by\n * `withWaniwani` on the server side, under `waniwani/widget` (canonical)\n * or the legacy bare `waniwani` key.\n */\ninterface WaniwaniMeta {\n\ttoken?: string;\n\tendpoint?: string;\n\tsessionId?: string;\n\tsource?: string;\n}\n\ninterface WaniwaniConfig {\n\ttoken?: string;\n\tendpoint: string;\n\tsessionId?: string;\n\tsource?: string;\n}\n\ninterface BaseUseWaniwaniOptions {\n\t/**\n\t * JWT widget token for authenticating directly with the Waniwani backend.\n\t * If omitted, the hook resolves from tool response metadata\n\t * (`_meta[\"waniwani/widget\"].token`).\n\t */\n\ttoken?: string;\n\t/**\n\t * Session ID to use for event correlation.\n\t * If omitted, the hook resolves from tool response metadata\n\t * (`_meta[\"waniwani/widget\"].sessionId`), then falls back to a random UUID\n\t * so the widget's own events still group together.\n\t */\n\tsessionId?: string;\n\t/**\n\t * Additional fields merged into every tracked event's envelope metadata.\n\t */\n\tmetadata?: Record<string, unknown>;\n\t/**\n\t * Tool response metadata from a host you already have a connection to\n\t * (e.g. an MCP-Apps / skybridge host exposing `useToolInfo().responseMetadata`).\n\t *\n\t * The hook resolves its config (`endpoint`, `source`, `token`, `sessionId`)\n\t * from `_meta[\"waniwani/widget\"]` in this object. The hook never opens a host\n\t * connection of its own — your app owns the host bridge and passes the\n\t * metadata in. Ignored when an explicit `endpoint` is passed.\n\t */\n\ttoolResponseMetadata?: Record<string, unknown> | null;\n}\n\n/**\n * Metadata-driven options: `endpoint` and `source` are resolved from the\n * `toolResponseMetadata` you pass (`_meta[\"waniwani/widget\"]`). `source` may\n * be overridden explicitly.\n */\ninterface ContextDrivenOptions extends BaseUseWaniwaniOptions {\n\tendpoint?: undefined;\n\t/** Optional override; otherwise resolved from `toolResponseMetadata`. */\n\tsource?: string;\n}\n\n/**\n * Explicit-endpoint options: when `endpoint` is passed directly, `source`\n * is required so events never get stamped with a placeholder.\n */\ninterface ExplicitEndpointOptions extends BaseUseWaniwaniOptions {\n\t/** V2 batch endpoint URL to POST tracking events to. */\n\tendpoint: string;\n\t/** Required when `endpoint` is explicit (e.g. `\"chatgpt\"`, `\"chatbar\"`). */\n\tsource: string;\n}\n\n/**\n * Options for the useWaniwani hook. Either rely on the widget host\n * (omit `endpoint`) or pass `endpoint` + `source` explicitly.\n */\nexport type UseWaniwaniOptions = ContextDrivenOptions | ExplicitEndpointOptions;\n\n/**\n * The tracking API returned by `useWaniwani()`.\n */\nexport interface WaniwaniWidget {\n\t/**\n\t * The session ID stamped on every event this widget emits, so hosts can\n\t * correlate widget activity with server-side tracking.\n\t *\n\t * Resolved from (1) the explicit `sessionId` option, (2) the widget config\n\t * injected by `withWaniwani` (`_meta[\"waniwani/widget\"].sessionId`), else a\n\t * random UUID generated on mount. `undefined` until the widget initializes\n\t * and when no config resolves (no-op widget).\n\t */\n\treadonly sessionId?: string;\n\t/**\n\t * Track a typed event. The exact same surface as the server client:\n\t * `track({ event: \"quote.succeeded\", properties })`,\n\t * `track.priceShown({ amount, currency })`, `track.converted({ ... })`.\n\t * Identity (session, trace, user) is stamped automatically.\n\t */\n\ttrack: TrackFn;\n\t/** Tie all subsequent widget events to this user (emits `user.identified`). */\n\tidentify(\n\t\tuserId: string,\n\t\ttraits?: Record<string, unknown>,\n\t): Promise<{ eventId: string }>;\n\t/** Flush buffered events immediately instead of waiting for the timer. */\n\tflush(): Promise<void>;\n}\n\nconst NOOP_EMIT = async (): Promise<{ eventId: string }> => ({ eventId: \"\" });\n\nfunction createNoopTrack(): TrackFn {\n\treturn Object.assign(NOOP_EMIT, {\n\t\tpriceShown: NOOP_EMIT,\n\t\tpricesCompared: NOOP_EMIT,\n\t\toptionSelected: NOOP_EMIT,\n\t\tleadQualified: NOOP_EMIT,\n\t\tconverted: NOOP_EMIT,\n\t}) as TrackFn;\n}\n\n/** No-op widget that silently discards all calls. */\nconst NOOP_WIDGET: WaniwaniWidget = {\n\tsessionId: undefined,\n\ttrack: createNoopTrack(),\n\tidentify: NOOP_EMIT,\n\tflush: async () => {},\n};\n\ninterface ResolvedConfig extends WaniwaniConfig {\n\tsource: string;\n}\n\ninterface WidgetState {\n\twidget: WaniwaniWidget;\n\tcleanup: () => void;\n\tconfig: ResolvedConfig | null;\n}\n\n/** Module-level singleton — shared across all hook consumers. */\nlet state: WidgetState | null = null;\nlet consumerCount = 0;\n\nfunction normalizeString(value: unknown): string | undefined {\n\tif (typeof value !== \"string\") {\n\t\treturn undefined;\n\t}\n\tconst trimmed = value.trim();\n\treturn trimmed.length > 0 ? trimmed : undefined;\n}\n\n/**\n * Extract the Waniwani widget config from a tool response metadata object.\n * Reads the `waniwani/widget` key, on both the metadata root and its nested\n * `_meta`. Returns `null` when no endpoint can be resolved.\n *\n * @internal exported for unit testing.\n */\nexport function extractConfigFromMetadata(\n\tmeta: Record<string, unknown> | null,\n): WaniwaniConfig | null {\n\tif (!meta) {\n\t\treturn null;\n\t}\n\n\tconst nestedMeta = meta._meta as Record<string, unknown> | undefined;\n\tconst waniwani = (meta[WIDGET_CONFIG_META_KEY] ??\n\t\tnestedMeta?.[WIDGET_CONFIG_META_KEY]) as WaniwaniMeta | undefined;\n\tconst endpoint = normalizeString(waniwani?.endpoint);\n\tif (!endpoint) {\n\t\treturn null;\n\t}\n\n\treturn {\n\t\tendpoint,\n\t\ttoken: normalizeString(waniwani?.token),\n\t\tsessionId: normalizeString(waniwani?.sessionId),\n\t\tsource: normalizeString(waniwani?.source),\n\t};\n}\n\nfunction isSameConfig(\n\ta: WaniwaniConfig | null | undefined,\n\tb: WaniwaniConfig | null | undefined,\n): boolean {\n\treturn (\n\t\ta?.endpoint === b?.endpoint &&\n\t\ta?.token === b?.token &&\n\t\ta?.sessionId === b?.sessionId &&\n\t\ta?.source === b?.source\n\t);\n}\n\nfunction createState(\n\tconfig: ResolvedConfig,\n\tmetadata?: Record<string, unknown>,\n): WidgetState {\n\tconst sessionId = config.sessionId ?? crypto.randomUUID();\n\tconst traceId = crypto.randomUUID();\n\n\tconst client = createFrontendClient({\n\t\tendpoint: config.endpoint,\n\t\ttoken: config.token,\n\t\tsource: config.source,\n\t\tidentity: () => ({ sessionId, traceId }),\n\t\tmetadata,\n\t});\n\n\t// The top-of-widget-funnel signal: emitted once per mount with resolved\n\t// config, so \"widget shown\" exists even when nothing is tracked manually.\n\tvoid client.track({ event: \"widget_render\" });\n\n\treturn {\n\t\tconfig,\n\t\twidget: {\n\t\t\tsessionId,\n\t\t\ttrack: client.track,\n\t\t\tidentify: (userId, traits) => client.identify(userId, traits),\n\t\t\tflush: () => client.flush(),\n\t\t},\n\t\tcleanup: () => {\n\t\t\tvoid client.shutdown();\n\t\t},\n\t};\n}\n\n/**\n * React hook for tracking from inside an MCP-app widget. Returns the same\n * `track` surface as the server client, with session identity stamped\n * automatically from the config `withWaniwani` injects into tool responses.\n *\n * The hook never opens a host connection of its own. Supply config one of\n * two ways:\n * 1. Explicit `endpoint` + `source` (plus optional `token` / `sessionId`)\n * 2. `toolResponseMetadata` — the `_meta` object your host already exposes\n * (e.g. skybridge's `useToolInfo().responseMetadata`); the hook reads\n * `_meta[\"waniwani/widget\"]` from it\n *\n * No-op when neither resolves an `endpoint` and a `source`.\n *\n * @example\n * ```tsx\n * function MyWidget() {\n * const { responseMetadata } = useToolInfo();\n * const wani = useWaniwani({ toolResponseMetadata: responseMetadata });\n * // wani.sessionId correlates with server-side tracking\n * return (\n * <button\n * onClick={() =>\n * wani.track.optionSelected({ id: \"pro\", amount: 49, currency: \"EUR\" })\n * }\n * >\n * Choose Pro\n * </button>\n * );\n * }\n * ```\n */\nexport function useWaniwani(options: UseWaniwaniOptions = {}): WaniwaniWidget {\n\tconst explicitEndpoint = normalizeString(options.endpoint);\n\tconst explicitToken = normalizeString(options.token);\n\tconst explicitSessionId = normalizeString(options.sessionId);\n\tconst explicitSource = normalizeString(options.source);\n\n\t// Config from the host metadata passed in. Ignored in explicit-endpoint\n\t// mode, where every field comes from the options directly.\n\tconst suppliedConfig = explicitEndpoint\n\t\t? null\n\t\t: extractConfigFromMetadata(options.toolResponseMetadata ?? null);\n\tconst ctxEndpoint = suppliedConfig?.endpoint;\n\tconst ctxToken = suppliedConfig?.token;\n\tconst ctxSessionId = suppliedConfig?.sessionId;\n\tconst ctxSource = suppliedConfig?.source;\n\n\t// Stabilize config identity — only changes when the resolved primitives do,\n\t// so a new `toolResponseMetadata` object of identical content is a no-op.\n\tconst config = useMemo<ResolvedConfig | null>(() => {\n\t\tconst source = explicitSource ?? ctxSource;\n\t\tif (!source) {\n\t\t\treturn null;\n\t\t}\n\t\tif (explicitEndpoint) {\n\t\t\treturn {\n\t\t\t\tendpoint: explicitEndpoint,\n\t\t\t\ttoken: explicitToken,\n\t\t\t\tsessionId: explicitSessionId,\n\t\t\t\tsource,\n\t\t\t};\n\t\t}\n\t\tif (!ctxEndpoint) {\n\t\t\treturn null;\n\t\t}\n\t\treturn {\n\t\t\tendpoint: ctxEndpoint,\n\t\t\ttoken: explicitToken ?? ctxToken,\n\t\t\tsessionId: explicitSessionId ?? ctxSessionId,\n\t\t\tsource,\n\t\t};\n\t}, [\n\t\texplicitEndpoint,\n\t\texplicitToken,\n\t\texplicitSessionId,\n\t\texplicitSource,\n\t\tctxEndpoint,\n\t\tctxToken,\n\t\tctxSessionId,\n\t\tctxSource,\n\t]);\n\n\tconst [widget, setWidget] = useState<WaniwaniWidget>(NOOP_WIDGET);\n\t// Ref, not a dependency: metadata is captured when the singleton is\n\t// created and must not force a transport restart on every render.\n\tconst metadataRef = useRef(options.metadata);\n\tmetadataRef.current = options.metadata;\n\n\t// Create/swap singleton state when config changes.\n\t// All side effects (timers, network) happen here in useEffect, making\n\t// this safe in Strict Mode and concurrent rendering.\n\t//\n\t// Only consumers with a resolved config hold a stake in the singleton.\n\t// A consumer with `config === null` becomes a local no-op without\n\t// touching the singleton (other consumers may still be driving it),\n\t// and the singleton is torn down only when the last stake is released.\n\tuseEffect(() => {\n\t\tif (typeof window === \"undefined\") {\n\t\t\treturn;\n\t\t}\n\n\t\tif (!config) {\n\t\t\tsetWidget(NOOP_WIDGET);\n\t\t\treturn;\n\t\t}\n\n\t\tlet current = state;\n\t\tif (!current || !isSameConfig(current.config, config)) {\n\t\t\tcurrent?.cleanup();\n\t\t\tcurrent = createState(config, metadataRef.current);\n\t\t\tstate = current;\n\t\t}\n\t\tsetWidget(current.widget);\n\t\tconsumerCount++;\n\n\t\treturn () => {\n\t\t\tconsumerCount = Math.max(consumerCount - 1, 0);\n\t\t\tif (consumerCount === 0) {\n\t\t\t\tstate?.cleanup();\n\t\t\t\tstate = null;\n\t\t\t}\n\t\t};\n\t}, [config]);\n\n\treturn widget;\n}\n\n/**\n * Reset the singleton (for testing only).\n * @internal\n */\nexport function _resetWidgetInstance(): void {\n\tstate?.cleanup();\n\tstate = null;\n}\n","// ============================================================================\n// Meta key extraction helpers\n// ============================================================================\n\n/** Pick the first non-empty string value from `meta` matching the given keys. */\nfunction pickFirst(\n\tmeta: Record<string, unknown>,\n\tkeys: readonly string[],\n): string | undefined {\n\tfor (const key of keys) {\n\t\tconst value = meta[key];\n\t\tif (typeof value === \"string\" && value.length > 0) {\n\t\t\treturn value;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n// --- Key lists (ordered by priority) ---\n\nconst SESSION_ID_KEYS = [\n\t\"waniwani/sessionId\",\n\t\"openai/sessionId\",\n\t\"openai/session\",\n\t\"sessionId\",\n\t\"conversationId\",\n\t\"mcp-session-id\",\n] as const;\n\nconst REQUEST_ID_KEYS = [\n\t\"waniwani/requestId\",\n\t\"openai/requestId\",\n\t\"requestId\",\n\t\"mcp/requestId\",\n] as const;\n\nconst TRACE_ID_KEYS = [\n\t\"waniwani/traceId\",\n\t\"openai/traceId\",\n\t\"traceId\",\n\t\"mcp/traceId\",\n\t\"openai/requestId\",\n\t\"requestId\",\n] as const;\n\nconst EXTERNAL_USER_ID_KEYS = [\n\t\"waniwani/userId\",\n\t\"openai/userId\",\n\t\"externalUserId\",\n\t\"userId\",\n\t\"actorId\",\n] as const;\n\nconst VISITOR_ID_KEYS = [\"waniwani/visitorId\", \"visitorId\"] as const;\n\nconst CORRELATION_ID_KEYS = [\"correlationId\", \"openai/requestId\"] as const;\n\nconst TURN_COUNT_KEYS = [\"waniwani/turnCount\"] as const;\n\n/** Meta key for flow execution path (nodesVisited, flowId). */\nexport const FLOW_META_KEY = \"waniwani/flow\" as const;\n\n/**\n * Meta key under which `withWaniwani` injects the widget tracking config\n * (`{ endpoint, token, sessionId, source, geoLocation }`) into tool response\n * `_meta`. The frontend client reads it to track without any manual wiring.\n * Older SDKs wrote the same object under the bare `waniwani` key.\n */\nexport const WIDGET_CONFIG_META_KEY = \"waniwani/widget\" as const;\n\n// --- Extractors ---\n\nexport function extractSessionId(\n\tmeta: Record<string, unknown> | undefined,\n): string | undefined {\n\treturn meta ? pickFirst(meta, SESSION_ID_KEYS) : undefined;\n}\n\nexport function extractRequestId(\n\tmeta: Record<string, unknown> | undefined,\n): string | undefined {\n\treturn meta ? pickFirst(meta, REQUEST_ID_KEYS) : undefined;\n}\n\nexport function extractTraceId(\n\tmeta: Record<string, unknown> | undefined,\n): string | undefined {\n\treturn meta ? pickFirst(meta, TRACE_ID_KEYS) : undefined;\n}\n\nexport function extractExternalUserId(\n\tmeta: Record<string, unknown> | undefined,\n): string | undefined {\n\treturn meta ? pickFirst(meta, EXTERNAL_USER_ID_KEYS) : undefined;\n}\n\nexport function extractVisitorId(\n\tmeta: Record<string, unknown> | undefined,\n): string | undefined {\n\treturn meta ? pickFirst(meta, VISITOR_ID_KEYS) : undefined;\n}\n\nexport function extractCorrelationId(\n\tmeta: Record<string, unknown> | undefined,\n): string | undefined {\n\treturn meta ? pickFirst(meta, CORRELATION_ID_KEYS) : undefined;\n}\n\n/**\n * Number of user messages in the current chat session, as counted by the\n * Waniwani app before dispatching the MCP request. Useful for MCPs that\n * gate behavior on conversation length (e.g. compulsory email verification\n * after N turns) without each MCP having to track turn state itself.\n *\n * Forwarded as `waniwani/turnCount` (non-negative integer).\n */\nexport function extractTurnCount(\n\tmeta: Record<string, unknown> | undefined,\n): number | undefined {\n\tif (!meta) {\n\t\treturn undefined;\n\t}\n\tfor (const key of TURN_COUNT_KEYS) {\n\t\tconst value = meta[key];\n\t\tif (typeof value === \"number\" && Number.isFinite(value) && value >= 0) {\n\t\t\treturn value;\n\t\t}\n\t}\n\treturn undefined;\n}\n\nconst SOURCE_SESSION_KEYS = [\n\t{ key: \"openai/sessionId\", source: \"chatgpt\" },\n\t{ key: \"openai/session\", source: \"chatgpt\" },\n] as const;\n\n/**\n * Client identifiers advertised via MCP `initialize` → `clientInfo.name` that\n * the SDK maps to a known source. Match is case-insensitive substring on the\n * advertised name, so this catches \"Claude\", \"Claude Code\", \"claude-ai\", etc.\n * without having to enumerate every surface.\n *\n * ChatGPT is primarily identified by its `openai/*` session key (see\n * SOURCE_SESSION_KEYS), but a session without that key still resolves here from\n * the advertised client name so assistant traffic is attributed consistently.\n * First match wins, so order the needles most-specific first.\n */\nconst CLIENT_INFO_NAME_SOURCES: ReadonlyArray<{\n\tneedle: string;\n\tsource: string;\n}> = [\n\t{ needle: \"claude\", source: \"claude\" },\n\t{ needle: \"chatgpt\", source: \"chatgpt\" },\n\t{ needle: \"openai\", source: \"chatgpt\" },\n\t{ needle: \"gemini\", source: \"gemini\" },\n];\n\nexport type ExtractSourceClientInfo = {\n\tname?: string;\n\tversion?: string;\n};\n\nexport function extractSource(\n\tmeta: Record<string, unknown> | undefined,\n\tclientInfo?: ExtractSourceClientInfo,\n): string | undefined {\n\tif (meta) {\n\t\tconst explicit = meta[\"waniwani/source\"];\n\t\tif (typeof explicit === \"string\" && explicit.length > 0) {\n\t\t\treturn explicit;\n\t\t}\n\t\tfor (const { key, source } of SOURCE_SESSION_KEYS) {\n\t\t\tconst value = meta[key];\n\t\t\tif (typeof value === \"string\" && value.length > 0) {\n\t\t\t\treturn source;\n\t\t\t}\n\t\t}\n\t}\n\t// Fall back to MCP `initialize` clientInfo.name when no _meta key matches.\n\t// Claude surfaces (Code, Desktop, claude.ai connector, Anthropic API MCP\n\t// connector) don't expose a namespaced session id in _meta but do advertise\n\t// themselves in the handshake. Per-MCP-session clientInfo is captured by the\n\t// MCP transport and exposed via server.getClientVersion().\n\tconst name = clientInfo?.name;\n\tif (typeof name === \"string\" && name.length > 0) {\n\t\tconst lower = name.toLowerCase();\n\t\tfor (const { needle, source } of CLIENT_INFO_NAME_SOURCES) {\n\t\t\tif (lower.includes(needle)) {\n\t\t\t\treturn source;\n\t\t\t}\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * HTTP request headers that identify a known caller when neither `_meta` nor\n * `clientInfo` carries a source. This is the most robust Claude signal: every\n * Claude surface sends `User-Agent: Claude-User` and `x-anthropic-client` on\n * MCP HTTP requests, whereas the MCP transport carries no session id and the\n * `initialize` `clientInfo` may not be surfaced on stateless deployments.\n */\nfunction headerValue(\n\theaders: Record<string, unknown>,\n\tkey: string,\n): string | undefined {\n\t// HTTP header names are case-insensitive; the transport may preserve the\n\t// original casing, so match the key case-insensitively rather than assuming\n\t// it was lowercased.\n\tconst lowerKey = key.toLowerCase();\n\tlet raw = headers[key] ?? headers[lowerKey];\n\tif (raw === undefined) {\n\t\tfor (const headerKey of Object.keys(headers)) {\n\t\t\tif (headerKey.toLowerCase() === lowerKey) {\n\t\t\t\traw = headers[headerKey];\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\tconst value = Array.isArray(raw) ? raw[0] : raw;\n\treturn typeof value === \"string\" && value.length > 0 ? value : undefined;\n}\n\nexport function extractSourceFromHeaders(\n\theaders: Record<string, unknown> | undefined,\n): string | undefined {\n\tif (!headers) {\n\t\treturn undefined;\n\t}\n\tconst userAgent = headerValue(headers, \"user-agent\");\n\tconst anthropicClient = headerValue(headers, \"x-anthropic-client\");\n\tif (\n\t\t(userAgent && /claude/i.test(userAgent)) ||\n\t\t(anthropicClient && /claude|anthropic/i.test(anthropicClient))\n\t) {\n\t\treturn \"claude\";\n\t}\n\t// ChatGPT / OpenAI surfaces without an `openai/*` session key still send an\n\t// OpenAI user-agent (e.g. `ChatGPT-User`), so attribute them here too.\n\tif (userAgent && /chatgpt|openai/i.test(userAgent)) {\n\t\treturn \"chatgpt\";\n\t}\n\treturn undefined;\n}\n","import {\n\textractCorrelationId,\n\textractExternalUserId,\n\textractRequestId,\n\textractSessionId,\n\textractSource,\n\textractTraceId,\n\textractVisitorId,\n} from \"../mcp/server/utils.js\";\nimport type { EventType, LegacyTrackEvent, TrackInput } from \"./@types.js\";\nimport type { V2CorrelationIds, V2EventEnvelope } from \"./v2-types.js\";\n\nconst DEFAULT_SOURCE = \"@waniwani/sdk\";\n\nexport interface MapTrackEventOptions {\n\tnow?: () => Date;\n\tgenerateId?: () => string;\n\tsource?: string;\n}\n\nexport function mapTrackEventToV2(\n\tinput: TrackInput,\n\toptions: MapTrackEventOptions = {},\n): V2EventEnvelope {\n\tconst now = options.now ?? (() => new Date());\n\tconst generateId = options.generateId ?? createEventId;\n\tconst eventName = resolveEventName(input);\n\tconst meta = toRecord(input.meta);\n\tconst metadata = toRecord(input.metadata);\n\tconst correlation = resolveCorrelationIds(input, meta);\n\tconst eventId = takeNonEmptyString(input.eventId) ?? generateId();\n\tconst timestamp = normalizeTimestamp(input.timestamp, now);\n\tconst source =\n\t\ttakeNonEmptyString(input.source) ??\n\t\textractSource(meta) ??\n\t\toptions.source ??\n\t\tDEFAULT_SOURCE;\n\tconst rawLegacy = isLegacyTrackEvent(input) ? { ...input } : undefined;\n\n\tconst mappedMetadata: Record<string, unknown> = {\n\t\t...metadata,\n\t};\n\tif (Object.keys(meta).length > 0) {\n\t\tmappedMetadata.meta = meta;\n\t}\n\tif (rawLegacy) {\n\t\tmappedMetadata.rawLegacy = rawLegacy;\n\t}\n\n\treturn {\n\t\tid: eventId,\n\t\ttype: \"mcp.event\",\n\t\tname: eventName,\n\t\tsource,\n\t\ttimestamp,\n\t\tcorrelation,\n\t\tproperties: mapProperties(input, eventName),\n\t\tmetadata: mappedMetadata,\n\t\trawLegacy,\n\t};\n}\n\nexport function createEventId(): string {\n\tif (\n\t\ttypeof crypto !== \"undefined\" &&\n\t\ttypeof crypto.randomUUID === \"function\"\n\t) {\n\t\treturn `evt_${crypto.randomUUID()}`;\n\t}\n\n\treturn `evt_${Math.random().toString(36).slice(2, 10)}_${Date.now().toString(36)}`;\n}\n\nfunction mapProperties(\n\tinput: TrackInput,\n\teventName: EventType,\n): Record<string, unknown> {\n\tif (!isLegacyTrackEvent(input)) {\n\t\treturn toRecord(input.properties);\n\t}\n\n\tconst legacyProperties = mapLegacyProperties(input, eventName);\n\tconst explicitProperties = toRecord(input.properties);\n\treturn {\n\t\t...legacyProperties,\n\t\t...explicitProperties,\n\t};\n}\n\nfunction mapLegacyProperties(\n\tinput: LegacyTrackEvent,\n\teventName: EventType,\n): Record<string, unknown> {\n\tswitch (eventName) {\n\t\tcase \"tool.called\": {\n\t\t\tconst properties: Record<string, unknown> = {};\n\t\t\tif (takeNonEmptyString(input.toolName)) {\n\t\t\t\tproperties.name = input.toolName;\n\t\t\t}\n\t\t\tif (takeNonEmptyString(input.toolType)) {\n\t\t\t\tproperties.type = input.toolType;\n\t\t\t}\n\t\t\treturn properties;\n\t\t}\n\t\tcase \"quote.succeeded\": {\n\t\t\tconst properties: Record<string, unknown> = {};\n\t\t\tif (typeof input.quoteAmount === \"number\") {\n\t\t\t\tproperties.amount = input.quoteAmount;\n\t\t\t}\n\t\t\tif (takeNonEmptyString(input.quoteCurrency)) {\n\t\t\t\tproperties.currency = input.quoteCurrency;\n\t\t\t}\n\t\t\treturn properties;\n\t\t}\n\t\tcase \"link.clicked\": {\n\t\t\tconst properties: Record<string, unknown> = {};\n\t\t\tif (takeNonEmptyString(input.linkUrl)) {\n\t\t\t\tproperties.url = input.linkUrl;\n\t\t\t}\n\t\t\treturn properties;\n\t\t}\n\t\tcase \"purchase.completed\": {\n\t\t\tconst properties: Record<string, unknown> = {};\n\t\t\tif (typeof input.purchaseAmount === \"number\") {\n\t\t\t\tproperties.amount = input.purchaseAmount;\n\t\t\t}\n\t\t\tif (takeNonEmptyString(input.purchaseCurrency)) {\n\t\t\t\tproperties.currency = input.purchaseCurrency;\n\t\t\t}\n\t\t\treturn properties;\n\t\t}\n\t\tdefault:\n\t\t\treturn {};\n\t}\n}\n\nfunction resolveEventName(input: TrackInput): EventType {\n\treturn isLegacyTrackEvent(input) ? input.eventType : input.event;\n}\n\nfunction resolveCorrelationIds(\n\tinput: TrackInput,\n\tmeta: Record<string, unknown>,\n): V2CorrelationIds {\n\tconst requestId =\n\t\ttakeNonEmptyString(input.requestId) ?? extractRequestId(meta);\n\n\tconst sessionId =\n\t\ttakeNonEmptyString(input.sessionId) ?? extractSessionId(meta);\n\n\tconst traceId = takeNonEmptyString(input.traceId) ?? extractTraceId(meta);\n\n\tconst externalUserId =\n\t\ttakeNonEmptyString(input.externalUserId) ?? extractExternalUserId(meta);\n\n\tconst visitorId =\n\t\ttakeNonEmptyString(input.visitorId) ?? extractVisitorId(meta);\n\n\tconst correlationId =\n\t\ttakeNonEmptyString(input.correlationId) ??\n\t\textractCorrelationId(meta) ??\n\t\trequestId;\n\n\tconst correlation: V2CorrelationIds = {};\n\tif (sessionId) {\n\t\tcorrelation.sessionId = sessionId;\n\t}\n\tif (traceId) {\n\t\tcorrelation.traceId = traceId;\n\t}\n\tif (requestId) {\n\t\tcorrelation.requestId = requestId;\n\t}\n\tif (correlationId) {\n\t\tcorrelation.correlationId = correlationId;\n\t}\n\tif (externalUserId) {\n\t\tcorrelation.externalUserId = externalUserId;\n\t}\n\tif (visitorId) {\n\t\tcorrelation.visitorId = visitorId;\n\t}\n\treturn correlation;\n}\n\nfunction normalizeTimestamp(\n\tinput: string | Date | undefined,\n\tnow: () => Date,\n): string {\n\tif (input instanceof Date) {\n\t\treturn input.toISOString();\n\t}\n\tif (typeof input === \"string\") {\n\t\tconst date = new Date(input);\n\t\tif (!Number.isNaN(date.getTime())) {\n\t\t\treturn date.toISOString();\n\t\t}\n\t}\n\treturn now().toISOString();\n}\n\nfunction toRecord(value: unknown): Record<string, unknown> {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) {\n\t\treturn {};\n\t}\n\treturn value as Record<string, unknown>;\n}\n\nfunction takeNonEmptyString(value: unknown): string | undefined {\n\tif (typeof value !== \"string\") {\n\t\treturn undefined;\n\t}\n\tif (value.trim().length === 0) {\n\t\treturn undefined;\n\t}\n\treturn value;\n}\n\nfunction isLegacyTrackEvent(input: TrackInput): input is LegacyTrackEvent {\n\treturn \"eventType\" in input;\n}\n","import type { RevenueTrackingApi, TrackInput } from \"./@types.js\";\n\n/** Enqueue one event and resolve to its id. */\ntype Emit = (event: TrackInput) => Promise<{ eventId: string }>;\n\n/**\n * Build the flat `track.*` revenue helpers over an `emit` function. Shared\n * by the top-level client and the request-scoped client (which injects meta\n * into `emit`) so both expose the same revenue API (WAN-386).\n *\n * Each helper maps its flat input to a typed first-class revenue event; the\n * remaining tracking context (sessionId / externalUserId / meta) is forwarded\n * as-is. Identity (one of those, or meta-derived) is required by the ingest\n * API — see the warning emitted from the client's `emit`.\n */\nexport function createRevenueApi(emit: Emit): RevenueTrackingApi {\n\treturn {\n\t\tpriceShown: ({ amount, currency, itemId, label, ...context }) =>\n\t\t\temit({\n\t\t\t\tevent: \"price_shown\",\n\t\t\t\tproperties: { amount, currency, itemId, label },\n\t\t\t\t...context,\n\t\t\t}),\n\t\tpricesCompared: ({ options, ...context }) =>\n\t\t\temit({ event: \"prices_compared\", properties: { options }, ...context }),\n\t\toptionSelected: ({ id, amount, currency, ...context }) =>\n\t\t\temit({\n\t\t\t\tevent: \"option_selected\",\n\t\t\t\tproperties: { id, amount, currency },\n\t\t\t\t...context,\n\t\t\t}),\n\t\tleadQualified: (input) => {\n\t\t\tconst { externalId, email, name, ...context } = input ?? {};\n\t\t\treturn emit({\n\t\t\t\tevent: \"lead_qualified\",\n\t\t\t\tproperties: { externalId, email, name },\n\t\t\t\t...context,\n\t\t\t});\n\t\t},\n\t\tconverted: ({ amount, currency, occurredAt, ...context }) =>\n\t\t\temit({\n\t\t\t\tevent: \"converted\",\n\t\t\t\tproperties: { amount, currency, occurredAt },\n\t\t\t\t...context,\n\t\t\t}),\n\t};\n}\n","import type {\n\tTrackingShutdownOptions,\n\tTrackingShutdownResult,\n} from \"./@types.js\";\nimport type {\n\tV2BatchRejectedEvent,\n\tV2BatchRequest,\n\tV2BatchResponse,\n\tV2EventEnvelope,\n} from \"./v2-types.js\";\n\ntype FetchFn = (\n\tinput: URL | RequestInfo,\n\tinit?: RequestInit,\n) => Promise<Response>;\n\nconst DEFAULT_ENDPOINT_PATH = \"/api/mcp/events/v2/batch\";\nconst DEFAULT_FLUSH_INTERVAL_MS = 1_000;\nconst DEFAULT_MAX_BATCH_SIZE = 20;\nconst DEFAULT_MAX_BUFFER_SIZE = 1_000;\nconst DEFAULT_MAX_RETRIES = 3;\nconst DEFAULT_RETRY_BASE_DELAY_MS = 200;\nconst DEFAULT_RETRY_MAX_DELAY_MS = 2_000;\nconst DEFAULT_SHUTDOWN_TIMEOUT_MS = 2_000;\nconst SDK_NAME = \"@waniwani/sdk\";\n// Keepalive requests are capped by browsers (64KiB in-flight quota); stay under it.\nconst TEARDOWN_MAX_BODY_BYTES = 60_000;\n\nconst AUTH_FAILURE_STATUS = new Set([401, 403]);\nconst RETRYABLE_STATUS = new Set([408, 425, 429, 500, 502, 503, 504]);\n\ninterface Logger {\n\twarn: (message: string, ...args: unknown[]) => void;\n\terror: (message: string, ...args: unknown[]) => void;\n}\n\nexport interface V2TransportOptions {\n\tapiUrl: string;\n\t/**\n\t * Bearer credential for the ingest endpoint: secret key (`wwk_`), public\n\t * token (`wwp_`), or widget JWT. Omit only for unauthenticated proxies.\n\t */\n\tapiKey?: string;\n\tendpointPath?: string;\n\tflushIntervalMs?: number;\n\tmaxBatchSize?: number;\n\tmaxBufferSize?: number;\n\tmaxRetries?: number;\n\tretryBaseDelayMs?: number;\n\tretryMaxDelayMs?: number;\n\tshutdownTimeoutMs?: number;\n\tsdkVersion?: string;\n\tfetchFn?: FetchFn;\n\tlogger?: Logger;\n\tnow?: () => Date;\n\tsleep?: (delayMs: number) => Promise<void>;\n}\n\nexport interface V2BatchTransport {\n\tenqueue: (event: V2EventEnvelope) => void;\n\tflush: () => Promise<void>;\n\tshutdown: (\n\t\toptions?: TrackingShutdownOptions,\n\t) => Promise<TrackingShutdownResult>;\n\tpendingEvents: () => number;\n}\n\ntype SendBatchResult =\n\t| { kind: \"success\" }\n\t| { kind: \"retryable\"; reason: string }\n\t| { kind: \"permanent\"; reason: string }\n\t| { kind: \"auth\"; status: number }\n\t| {\n\t\t\tkind: \"partial\";\n\t\t\tretryable: V2EventEnvelope[];\n\t\t\tpermanent: V2EventEnvelope[];\n\t };\n\nexport function createV2BatchTransport(\n\toptions: V2TransportOptions,\n): V2BatchTransport {\n\treturn new BatchingV2Transport(options);\n}\n\nclass BatchingV2Transport implements V2BatchTransport {\n\tprivate readonly endpointUrl: string;\n\tprivate readonly flushIntervalMs: number;\n\tprivate readonly maxBatchSize: number;\n\tprivate readonly maxBufferSize: number;\n\tprivate readonly maxRetries: number;\n\tprivate readonly retryBaseDelayMs: number;\n\tprivate readonly retryMaxDelayMs: number;\n\tprivate readonly shutdownTimeoutMs: number;\n\tprivate readonly sdkVersion?: string;\n\tprivate readonly fetchFn: FetchFn;\n\tprivate readonly logger: Logger;\n\tprivate readonly now: () => Date;\n\tprivate readonly sleep: (delayMs: number) => Promise<void>;\n\tprivate readonly apiKey?: string;\n\n\tprivate readonly buffer: V2EventEnvelope[] = [];\n\tprivate flushTimer: ReturnType<typeof setInterval> | undefined;\n\tprivate flushScheduled = false;\n\tprivate flushScheduledTimer: ReturnType<typeof setTimeout> | undefined;\n\tprivate flushInFlight: Promise<void> | undefined;\n\tprivate inFlightCount = 0;\n\tprivate isStopped = false;\n\tprivate isShuttingDown = false;\n\n\tconstructor(options: V2TransportOptions) {\n\t\tthis.endpointUrl = joinUrl(\n\t\t\toptions.apiUrl,\n\t\t\toptions.endpointPath ?? DEFAULT_ENDPOINT_PATH,\n\t\t);\n\t\tthis.flushIntervalMs = options.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;\n\t\tthis.maxBatchSize = options.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;\n\t\tthis.maxBufferSize = options.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE;\n\t\tthis.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;\n\t\tthis.retryBaseDelayMs =\n\t\t\toptions.retryBaseDelayMs ?? DEFAULT_RETRY_BASE_DELAY_MS;\n\t\tthis.retryMaxDelayMs =\n\t\t\toptions.retryMaxDelayMs ?? DEFAULT_RETRY_MAX_DELAY_MS;\n\t\tthis.shutdownTimeoutMs =\n\t\t\toptions.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;\n\t\tthis.fetchFn = options.fetchFn ?? fetch;\n\t\tthis.logger = options.logger ?? console;\n\t\tthis.now = options.now ?? (() => new Date());\n\t\tthis.sleep =\n\t\t\toptions.sleep ??\n\t\t\t((delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)));\n\t\tthis.apiKey = options.apiKey;\n\t\tthis.sdkVersion = options.sdkVersion;\n\n\t\tif (this.flushIntervalMs > 0) {\n\t\t\tthis.flushTimer = setInterval(() => {\n\t\t\t\tvoid this.flush();\n\t\t\t}, this.flushIntervalMs);\n\t\t}\n\n\t\tthis.registerBrowserTeardown();\n\t}\n\n\tenqueue(event: V2EventEnvelope): void {\n\t\tif (this.isStopped || this.isShuttingDown) {\n\t\t\tthis.logger.warn(\n\t\t\t\t\"[Waniwani] Tracking transport is stopped, dropping event %s\",\n\t\t\t\tevent.id,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.buffer.length >= this.maxBufferSize) {\n\t\t\tconst dropCount = this.buffer.length - this.maxBufferSize + 1;\n\t\t\tthis.buffer.splice(0, dropCount);\n\t\t\tthis.logger.warn(\n\t\t\t\t\"[Waniwani] Tracking buffer overflow, dropped %d oldest event(s)\",\n\t\t\t\tdropCount,\n\t\t\t);\n\t\t}\n\n\t\tthis.buffer.push(event);\n\n\t\tif (this.buffer.length >= this.maxBatchSize) {\n\t\t\tvoid this.flush();\n\t\t\treturn;\n\t\t}\n\n\t\tthis.scheduleMicroFlush();\n\t}\n\n\tpendingEvents(): number {\n\t\treturn this.buffer.length + this.inFlightCount;\n\t}\n\n\tasync flush(): Promise<void> {\n\t\tif (this.flushInFlight) {\n\t\t\treturn this.flushInFlight;\n\t\t}\n\t\tthis.flushInFlight = this.flushLoop().finally(() => {\n\t\t\tthis.flushInFlight = undefined;\n\t\t});\n\t\treturn this.flushInFlight;\n\t}\n\n\tasync shutdown(\n\t\toptions?: TrackingShutdownOptions,\n\t): Promise<TrackingShutdownResult> {\n\t\tthis.isShuttingDown = true;\n\t\tif (this.flushTimer) {\n\t\t\tclearInterval(this.flushTimer);\n\t\t\tthis.flushTimer = undefined;\n\t\t}\n\t\tif (this.flushScheduledTimer) {\n\t\t\tclearTimeout(this.flushScheduledTimer);\n\t\t\tthis.flushScheduledTimer = undefined;\n\t\t\tthis.flushScheduled = false;\n\t\t}\n\n\t\tconst timeoutMs = options?.timeoutMs ?? this.shutdownTimeoutMs;\n\t\tconst flushPromise = this.flush();\n\n\t\tif (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {\n\t\t\tawait flushPromise;\n\t\t\tthis.isStopped = true;\n\t\t\treturn { timedOut: false, pendingEvents: this.pendingEvents() };\n\t\t}\n\n\t\tconst timeoutSignal = Symbol(\"shutdown-timeout\");\n\t\tconst result = await Promise.race([\n\t\t\tflushPromise.then(() => \"flushed\" as const),\n\t\t\tthis.sleep(timeoutMs).then(() => timeoutSignal),\n\t\t]);\n\n\t\tif (result === timeoutSignal) {\n\t\t\tthis.isStopped = true;\n\t\t\treturn { timedOut: true, pendingEvents: this.pendingEvents() };\n\t\t}\n\n\t\tthis.isStopped = true;\n\t\treturn { timedOut: false, pendingEvents: this.pendingEvents() };\n\t}\n\n\t/**\n\t * In browsers, drain the buffer with keepalive fetches when the page is\n\t * hidden or unloading. The regular async flush loop does not survive\n\t * navigation; `keepalive` requests do. No-op outside a DOM environment.\n\t */\n\tprivate registerBrowserTeardown(): void {\n\t\tif (typeof document === \"undefined\" || typeof window === \"undefined\") {\n\t\t\treturn;\n\t\t}\n\n\t\tconst teardownFlush = () => {\n\t\t\tif (document.visibilityState === \"hidden\") {\n\t\t\t\tthis.keepaliveFlush();\n\t\t\t}\n\t\t};\n\t\tdocument.addEventListener(\"visibilitychange\", teardownFlush);\n\t\twindow.addEventListener(\"pagehide\", () => this.keepaliveFlush());\n\t}\n\n\t/** Fire-and-forget flush of everything buffered, chunked under the keepalive body cap. */\n\tprivate keepaliveFlush(): void {\n\t\tif (this.isStopped || this.buffer.length === 0) {\n\t\t\treturn;\n\t\t}\n\t\tconst events = this.buffer.splice(0, this.buffer.length);\n\t\tthis.sendKeepaliveChunk(events);\n\t}\n\n\tprivate sendKeepaliveChunk(events: V2EventEnvelope[]): void {\n\t\tconst body = JSON.stringify(this.makeBatchRequest(events));\n\t\tif (body.length > TEARDOWN_MAX_BODY_BYTES && events.length > 1) {\n\t\t\tconst mid = Math.ceil(events.length / 2);\n\t\t\tthis.sendKeepaliveChunk(events.slice(0, mid));\n\t\t\tthis.sendKeepaliveChunk(events.slice(mid));\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tvoid this.fetchFn(this.endpointUrl, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: this.requestHeaders(),\n\t\t\t\tbody,\n\t\t\t\tkeepalive: true,\n\t\t\t}).catch(() => {});\n\t\t} catch {\n\t\t\t// Teardown is best-effort; never throw during page unload.\n\t\t}\n\t}\n\n\tprivate requestHeaders(): Record<string, string> {\n\t\tconst headers: Record<string, string> = {\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\"X-WaniWani-SDK\": SDK_NAME,\n\t\t};\n\t\tif (this.apiKey) {\n\t\t\theaders.Authorization = `Bearer ${this.apiKey}`;\n\t\t}\n\t\treturn headers;\n\t}\n\n\tprivate scheduleMicroFlush(): void {\n\t\tif (this.flushScheduled) {\n\t\t\treturn;\n\t\t}\n\t\tthis.flushScheduled = true;\n\t\tthis.flushScheduledTimer = setTimeout(() => {\n\t\t\tthis.flushScheduledTimer = undefined;\n\t\t\tthis.flushScheduled = false;\n\t\t\tvoid this.flush();\n\t\t}, 0);\n\t}\n\n\tprivate async flushLoop(): Promise<void> {\n\t\twhile (this.buffer.length > 0 && !this.isStopped) {\n\t\t\tconst batch = this.buffer.splice(0, this.maxBatchSize);\n\t\t\tawait this.sendBatchWithRetry(batch);\n\t\t}\n\t}\n\n\tprivate async sendBatchWithRetry(batch: V2EventEnvelope[]): Promise<void> {\n\t\tlet attempt = 0;\n\t\tlet pendingBatch = batch;\n\n\t\twhile (pendingBatch.length > 0 && !this.isStopped) {\n\t\t\tthis.inFlightCount = pendingBatch.length;\n\t\t\tconst result = await this.sendBatchOnce(pendingBatch);\n\t\t\tthis.inFlightCount = 0;\n\n\t\t\tswitch (result.kind) {\n\t\t\t\tcase \"success\":\n\t\t\t\t\treturn;\n\t\t\t\tcase \"auth\":\n\t\t\t\t\tthis.stopTransportForAuthFailure(result.status, pendingBatch.length);\n\t\t\t\t\treturn;\n\t\t\t\tcase \"permanent\":\n\t\t\t\t\tthis.logger.error(\n\t\t\t\t\t\t\"[Waniwani] Dropping %d event(s) after permanent failure: %s\",\n\t\t\t\t\t\tpendingBatch.length,\n\t\t\t\t\t\tresult.reason,\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t\tcase \"retryable\":\n\t\t\t\t\tif (attempt >= this.maxRetries) {\n\t\t\t\t\t\tthis.logger.error(\n\t\t\t\t\t\t\t\"[Waniwani] Dropping %d event(s) after retry exhaustion: %s\",\n\t\t\t\t\t\t\tpendingBatch.length,\n\t\t\t\t\t\t\tresult.reason,\n\t\t\t\t\t\t);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tawait this.sleep(this.backoffDelayMs(attempt));\n\t\t\t\t\tattempt += 1;\n\t\t\t\t\tcontinue;\n\t\t\t\tcase \"partial\":\n\t\t\t\t\tif (result.permanent.length > 0) {\n\t\t\t\t\t\tthis.logger.error(\n\t\t\t\t\t\t\t\"[Waniwani] Dropping %d event(s) rejected as permanent\",\n\t\t\t\t\t\t\tresult.permanent.length,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tif (result.retryable.length === 0) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tif (attempt >= this.maxRetries) {\n\t\t\t\t\t\tthis.logger.error(\n\t\t\t\t\t\t\t\"[Waniwani] Dropping %d retryable event(s) after retry exhaustion\",\n\t\t\t\t\t\t\tresult.retryable.length,\n\t\t\t\t\t\t);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tpendingBatch = result.retryable;\n\t\t\t\t\tawait this.sleep(this.backoffDelayMs(attempt));\n\t\t\t\t\tattempt += 1;\n\t\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async sendBatchOnce(\n\t\tevents: V2EventEnvelope[],\n\t): Promise<SendBatchResult> {\n\t\tlet response: Response;\n\n\t\ttry {\n\t\t\tresponse = await this.fetchFn(this.endpointUrl, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: this.requestHeaders(),\n\t\t\t\tbody: JSON.stringify(this.makeBatchRequest(events)),\n\t\t\t});\n\t\t} catch (error) {\n\t\t\treturn {\n\t\t\t\tkind: \"retryable\",\n\t\t\t\treason: getErrorMessage(error),\n\t\t\t};\n\t\t}\n\n\t\tif (AUTH_FAILURE_STATUS.has(response.status)) {\n\t\t\treturn { kind: \"auth\", status: response.status };\n\t\t}\n\n\t\tif (RETRYABLE_STATUS.has(response.status)) {\n\t\t\treturn {\n\t\t\t\tkind: \"retryable\",\n\t\t\t\treason: `HTTP ${response.status}`,\n\t\t\t};\n\t\t}\n\n\t\tif (!response.ok) {\n\t\t\treturn {\n\t\t\t\tkind: \"permanent\",\n\t\t\t\treason: `HTTP ${response.status}`,\n\t\t\t};\n\t\t}\n\n\t\tconst data = await parseJsonResponse<V2BatchResponse>(response);\n\t\tif (!data?.rejected || data.rejected.length === 0) {\n\t\t\treturn { kind: \"success\" };\n\t\t}\n\n\t\tconst partial = this.classifyRejectedEvents(events, data.rejected);\n\t\tif (partial.retryable.length === 0 && partial.permanent.length === 0) {\n\t\t\treturn { kind: \"success\" };\n\t\t}\n\n\t\treturn {\n\t\t\tkind: \"partial\",\n\t\t\tretryable: partial.retryable,\n\t\t\tpermanent: partial.permanent,\n\t\t};\n\t}\n\n\tprivate makeBatchRequest(events: V2EventEnvelope[]): V2BatchRequest {\n\t\treturn {\n\t\t\tsentAt: this.now().toISOString(),\n\t\t\tsource: {\n\t\t\t\tsdk: SDK_NAME,\n\t\t\t\tversion: this.sdkVersion ?? \"0.0.0\",\n\t\t\t},\n\t\t\tevents,\n\t\t};\n\t}\n\n\tprivate classifyRejectedEvents(\n\t\tevents: V2EventEnvelope[],\n\t\trejected: V2BatchRejectedEvent[],\n\t): {\n\t\tretryable: V2EventEnvelope[];\n\t\tpermanent: V2EventEnvelope[];\n\t} {\n\t\tconst byId = new Map(events.map((event) => [event.id, event]));\n\t\tconst retryable: V2EventEnvelope[] = [];\n\t\tconst permanent: V2EventEnvelope[] = [];\n\n\t\tfor (const rejectedEvent of rejected) {\n\t\t\tconst event = byId.get(rejectedEvent.eventId);\n\t\t\tif (!event) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (isRetryableRejectedEvent(rejectedEvent)) {\n\t\t\t\tretryable.push(event);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tpermanent.push(event);\n\t\t}\n\n\t\treturn { retryable, permanent };\n\t}\n\n\tprivate backoffDelayMs(attempt: number): number {\n\t\tconst rawDelay = this.retryBaseDelayMs * 2 ** attempt;\n\t\treturn Math.min(rawDelay, this.retryMaxDelayMs);\n\t}\n\n\tprivate stopTransportForAuthFailure(\n\t\tstatus: number,\n\t\trejectedCount: number,\n\t): void {\n\t\tthis.isStopped = true;\n\t\tconst buffered = this.buffer.length;\n\t\tthis.buffer.splice(0, buffered);\n\t\tthis.logger.error(\n\t\t\t\"[Waniwani] Auth failure (HTTP %d). Stopping tracking transport and dropping %d queued event(s)\",\n\t\t\tstatus,\n\t\t\trejectedCount + buffered,\n\t\t);\n\t}\n}\n\nfunction isRetryableRejectedEvent(\n\trejectedEvent: V2BatchRejectedEvent,\n): boolean {\n\tif (rejectedEvent.retryable === true) {\n\t\treturn true;\n\t}\n\tconst code = rejectedEvent.code.toLowerCase();\n\treturn (\n\t\tcode.includes(\"timeout\") ||\n\t\tcode.includes(\"temporary\") ||\n\t\tcode.includes(\"unavailable\") ||\n\t\tcode.includes(\"rate_limit\") ||\n\t\tcode.includes(\"transient\") ||\n\t\tcode.includes(\"server\")\n\t);\n}\n\nasync function parseJsonResponse<T>(\n\tresponse: Response,\n): Promise<T | undefined> {\n\tconst body = await response.text();\n\tif (!body) {\n\t\treturn undefined;\n\t}\n\ttry {\n\t\treturn JSON.parse(body) as T;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction joinUrl(apiUrl: string, endpointPath: string): string {\n\tconst normalizedBase = apiUrl.endsWith(\"/\") ? apiUrl : `${apiUrl}/`;\n\tconst normalizedPath = endpointPath.startsWith(\"/\")\n\t\t? endpointPath.slice(1)\n\t\t: endpointPath;\n\treturn `${normalizedBase}${normalizedPath}`;\n}\n\nfunction getErrorMessage(error: unknown): string {\n\tif (error instanceof Error) {\n\t\treturn error.message;\n\t}\n\treturn String(error);\n}\n","import type {\n\tTrackFn,\n\tTrackInput,\n\tTrackingShutdownOptions,\n\tTrackingShutdownResult,\n} from \"./@types.js\";\nimport { mapTrackEventToV2 } from \"./mapper.js\";\nimport { createRevenueApi } from \"./revenue.js\";\nimport { createV2BatchTransport } from \"./transport.js\";\n\n/**\n * Correlation identity read at emit time, so identifiers that only exist\n * later (a server-assigned chat session id, for example) are picked up on\n * every event without re-creating the client.\n */\nexport interface FrontendIdentity {\n\tsessionId?: string;\n\tvisitorId?: string;\n\texternalUserId?: string;\n\ttraceId?: string;\n}\n\nexport interface FrontendClientOptions {\n\t/**\n\t * Full URL of the events ingest endpoint\n\t * (e.g. `https://app.waniwani.ai/api/mcp/events/v2/batch`).\n\t */\n\tendpoint: string;\n\t/**\n\t * Browser-safe credential: an environment public token (`wwp_...`) or a\n\t * widget JWT injected by `withWaniwani` into tool response `_meta`. Omit\n\t * only when the endpoint is your own unauthenticated proxy route.\n\t */\n\ttoken?: string;\n\t/**\n\t * Channel attribution stamped on every event (e.g. `\"chatgpt\"`, `\"web\"`).\n\t * The ingest API rejects events it cannot attribute to a channel, so set\n\t * this (or `channelId`) unless every event carries its own. Accepts a\n\t * getter for values that resolve after client creation.\n\t */\n\tsource?: string | (() => string | undefined);\n\t/**\n\t * Exact channel attribution, stamped as `properties.channelId` on every\n\t * event that does not set its own. Accepts a getter for values that\n\t * resolve after client creation.\n\t */\n\tchannelId?: string | (() => string | undefined);\n\t/** Live identity, called on every emit. */\n\tidentity?: () => FrontendIdentity;\n\t/** Extra fields merged into every event's envelope metadata. */\n\tmetadata?: Record<string, unknown>;\n\t/** Periodic flush interval for buffered events. */\n\tflushIntervalMs?: number;\n}\n\n/**\n * The browser counterpart of the server tracking client: the exact same\n * `track` surface (callable plus the flat revenue helpers) over the same\n * batching transport, with identity stamped automatically.\n */\nexport interface FrontendTrackingClient {\n\ttrack: TrackFn;\n\t/**\n\t * Attach a stable external user id. Emits `user.identified` and stamps\n\t * `externalUserId` on every subsequent event from this client.\n\t */\n\tidentify(\n\t\tuserId: string,\n\t\tproperties?: Record<string, unknown>,\n\t): Promise<{ eventId: string }>;\n\tflush(): Promise<void>;\n\tshutdown(options?: TrackingShutdownOptions): Promise<TrackingShutdownResult>;\n}\n\n/**\n * Create a tracking client for browser surfaces (MCP-app widgets, the chat\n * embed, or any page holding a `wwp_` public token). Events flush in batches,\n * survive page navigation via keepalive teardown, and never throw into the\n * host page.\n */\nexport function createFrontendClient(\n\toptions: FrontendClientOptions,\n): FrontendTrackingClient {\n\tconst { apiUrl, endpointPath } = splitEndpoint(options.endpoint);\n\tconst transport = createV2BatchTransport({\n\t\tapiUrl,\n\t\tapiKey: options.token,\n\t\tendpointPath,\n\t\tflushIntervalMs: options.flushIntervalMs,\n\t});\n\n\tlet identifiedUserId: string | undefined;\n\n\tconst resolve = (\n\t\tvalue: string | (() => string | undefined) | undefined,\n\t): string | undefined => (typeof value === \"function\" ? value() : value);\n\n\tconst emit = async (event: TrackInput): Promise<{ eventId: string }> => {\n\t\tconst identity = options.identity?.() ?? {};\n\t\tconst channelId = resolve(options.channelId);\n\t\tconst properties =\n\t\t\tchannelId === undefined\n\t\t\t\t? event.properties\n\t\t\t\t: { channelId, ...event.properties };\n\t\tconst mapped = mapTrackEventToV2(\n\t\t\t{\n\t\t\t\tsessionId: identity.sessionId,\n\t\t\t\tvisitorId: identity.visitorId,\n\t\t\t\texternalUserId: identity.externalUserId ?? identifiedUserId,\n\t\t\t\ttraceId: identity.traceId,\n\t\t\t\t...event,\n\t\t\t\tproperties,\n\t\t\t\tmetadata: { ...options.metadata, ...event.metadata },\n\t\t\t} as TrackInput,\n\t\t\t{ source: resolve(options.source) },\n\t\t);\n\t\ttransport.enqueue(mapped);\n\t\treturn { eventId: mapped.id };\n\t};\n\n\treturn {\n\t\ttrack: Object.assign(emit, createRevenueApi(emit)),\n\t\tidentify(userId, properties) {\n\t\t\tidentifiedUserId = userId;\n\t\t\treturn emit({\n\t\t\t\tevent: \"user.identified\",\n\t\t\t\texternalUserId: userId,\n\t\t\t\tproperties,\n\t\t\t});\n\t\t},\n\t\tflush: () => transport.flush(),\n\t\tshutdown: (shutdownOptions) => transport.shutdown(shutdownOptions),\n\t};\n}\n\nfunction splitEndpoint(endpoint: string): {\n\tapiUrl: string;\n\tendpointPath: string;\n} {\n\tconst url = new URL(endpoint);\n\treturn { apiUrl: url.origin, endpointPath: `${url.pathname}${url.search}` };\n}\n"],"mappings":"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","names":["useToolInfo","useEffect","useMemo","useRef","useState","pickFirst","meta","keys","key","value","SESSION_ID_KEYS","REQUEST_ID_KEYS","TRACE_ID_KEYS","EXTERNAL_USER_ID_KEYS","VISITOR_ID_KEYS","CORRELATION_ID_KEYS","WIDGET_CONFIG_META_KEY","extractSessionId","meta","pickFirst","SESSION_ID_KEYS","extractRequestId","REQUEST_ID_KEYS","extractTraceId","TRACE_ID_KEYS","extractExternalUserId","EXTERNAL_USER_ID_KEYS","extractVisitorId","VISITOR_ID_KEYS","extractCorrelationId","CORRELATION_ID_KEYS","SOURCE_SESSION_KEYS","CLIENT_INFO_NAME_SOURCES","extractSource","meta","clientInfo","explicit","key","source","value","name","lower","needle","DEFAULT_SOURCE","mapTrackEventToV2","input","options","now","generateId","createEventId","eventName","resolveEventName","meta","toRecord","metadata","correlation","resolveCorrelationIds","eventId","takeNonEmptyString","timestamp","normalizeTimestamp","source","extractSource","rawLegacy","isLegacyTrackEvent","mappedMetadata","mapProperties","legacyProperties","mapLegacyProperties","explicitProperties","properties","requestId","extractRequestId","sessionId","extractSessionId","traceId","extractTraceId","externalUserId","extractExternalUserId","visitorId","extractVisitorId","correlationId","extractCorrelationId","date","value","createRevenueApi","emit","amount","currency","itemId","label","context","options","id","input","externalId","email","name","occurredAt","DEFAULT_ENDPOINT_PATH","SDK_NAME","AUTH_FAILURE_STATUS","RETRYABLE_STATUS","createV2BatchTransport","options","BatchingV2Transport","joinUrl","DEFAULT_ENDPOINT_PATH","delayMs","resolve","event","dropCount","timeoutMs","flushPromise","timeoutSignal","teardownFlush","events","body","mid","headers","SDK_NAME","batch","attempt","pendingBatch","result","response","error","getErrorMessage","data","parseJsonResponse","partial","rejected","byId","retryable","permanent","rejectedEvent","isRetryableRejectedEvent","rawDelay","status","rejectedCount","buffered","code","apiUrl","endpointPath","normalizedBase","normalizedPath","createFrontendClient","options","apiUrl","endpointPath","splitEndpoint","transport","createV2BatchTransport","identifiedUserId","resolve","value","emit","event","identity","channelId","properties","mapped","mapTrackEventToV2","createRevenueApi","userId","shutdownOptions","endpoint","url","NOOP_EMIT","createNoopTrack","NOOP_WIDGET","state","consumerCount","normalizeString","value","trimmed","extractConfigFromMetadata","meta","nestedMeta","waniwani","WIDGET_CONFIG_META_KEY","endpoint","isSameConfig","a","b","createState","config","metadata","sessionId","traceId","client","createFrontendClient","userId","traits","useWaniwani","options","explicitEndpoint","explicitToken","explicitSessionId","explicitSource","suppliedConfig","ctxEndpoint","ctxToken","ctxSessionId","ctxSource","useMemo","source","widget","setWidget","useState","metadataRef","useRef","useEffect","current","useWaniwani","options","responseMetadata","useToolInfo"]}
1
+ {"version":3,"sources":["../../../src/mcp/react/skybridge.ts","../../../src/mcp/react/hooks/use-waniwani.ts","../../../src/mcp/server/utils.ts","../../../src/tracking/mapper.ts","../../../src/tracking/revenue.ts","../../../src/tracking/transport.ts","../../../src/tracking/frontend.ts"],"sourcesContent":["\"use client\";\n\n// `@waniwani/sdk/mcp/react/skybridge` — the skybridge-host adapter for\n// `useWaniwani`.\n//\n// The pure `@waniwani/sdk/mcp/react` hook is host-agnostic: it takes the tool\n// response `_meta` as data and never opens a host connection of its own. That\n// keeps the core dependency-free, but it means the widget author has to hand\n// the metadata in.\n//\n// On an MCP-Apps host the tool result `_meta` is delivered once, as the\n// `ui/notifications/tool-result` notification, to whichever `App` is connected\n// and listening at that moment. In a skybridge widget that App is skybridge —\n// it captures the result and exposes it as `useToolInfo().responseMetadata`.\n// This adapter reads that value and feeds it to the core hook, so widgets get\n// the full tracking surface from a bare call:\n//\n// ```tsx\n// import { useWaniwani } from \"@waniwani/sdk/mcp/react/skybridge\";\n//\n// function BookCallWidget() {\n// const wani = useWaniwani();\n// wani.track.leadQualified({ email, name });\n// }\n// ```\n//\n// skybridge is an optional peer dependency, resolved in the consumer app. The\n// SDK depends only on the tiny slice declared in `skybridge-web.d.ts`, so it\n// need not install skybridge to typecheck or build.\n\nimport { useToolInfo } from \"skybridge/web\";\nimport {\n\ttype UseWaniwaniOptions,\n\tuseWaniwani as useWaniwaniCore,\n\ttype WaniwaniWidget,\n} from \"./hooks/use-waniwani\";\n\n/**\n * Options for the skybridge `useWaniwani` adapter. Derived from the core hook's\n * metadata-driven options (the `endpoint?: undefined` branch of\n * {@link UseWaniwaniOptions}) minus the two fields the adapter supplies itself:\n * `endpoint`/`source` and `token`/`sessionId` are resolved from skybridge's\n * `responseMetadata` (`_meta[\"waniwani/widget\"]`), and `toolResponseMetadata` is\n * read from `useToolInfo()`. What remains — `source`/`token`/`sessionId`\n * overrides and `metadata` passthrough — keeps its field docs from the core.\n */\nexport type SkybridgeWaniwaniOptions = Omit<\n\tExtract<UseWaniwaniOptions, { endpoint?: undefined }>,\n\t\"endpoint\" | \"toolResponseMetadata\"\n>;\n\nexport type { WaniwaniWidget } from \"./hooks/use-waniwani\";\n\n/**\n * `useWaniwani` for skybridge-hosted MCP-app widgets. Resolves the Waniwani\n * config from skybridge's `useToolInfo().responseMetadata` and returns the same\n * `track` surface as the core hook, with session identity stamped\n * automatically. Call it bare — no config threading, no provider.\n *\n * @example\n * ```tsx\n * const wani = useWaniwani();\n * wani.track.optionSelected({ id: \"pro\", amount: 49, currency: \"EUR\" });\n * ```\n */\nexport function useWaniwani(\n\toptions: SkybridgeWaniwaniOptions = {},\n): WaniwaniWidget {\n\tconst { responseMetadata } = useToolInfo();\n\treturn useWaniwaniCore({\n\t\t...options,\n\t\ttoolResponseMetadata: responseMetadata,\n\t});\n}\n","\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport type { TrackFn } from \"../../../tracking/@types\";\nimport { createFrontendClient } from \"../../../tracking/frontend\";\nimport { WIDGET_CONFIG_META_KEY } from \"../../server/utils\";\n\n/**\n * Waniwani widget config injected into tool response `_meta` by\n * `withWaniwani` on the server side, under `waniwani/widget` (canonical)\n * or the legacy bare `waniwani` key.\n */\ninterface WaniwaniMeta {\n\ttoken?: string;\n\tendpoint?: string;\n\tsessionId?: string;\n\tsource?: string;\n}\n\ninterface WaniwaniConfig {\n\ttoken?: string;\n\tendpoint: string;\n\tsessionId?: string;\n\tsource?: string;\n}\n\ninterface BaseUseWaniwaniOptions {\n\t/**\n\t * JWT widget token for authenticating directly with the Waniwani backend.\n\t * If omitted, the hook resolves from tool response metadata\n\t * (`_meta[\"waniwani/widget\"].token`).\n\t */\n\ttoken?: string;\n\t/**\n\t * Session ID to use for event correlation.\n\t * If omitted, the hook resolves from tool response metadata\n\t * (`_meta[\"waniwani/widget\"].sessionId`), then falls back to a random UUID\n\t * so the widget's own events still group together.\n\t */\n\tsessionId?: string;\n\t/**\n\t * Additional fields merged into every tracked event's envelope metadata.\n\t */\n\tmetadata?: Record<string, unknown>;\n\t/**\n\t * Tool response metadata from a host you already have a connection to\n\t * (e.g. an MCP-Apps / skybridge host exposing `useToolInfo().responseMetadata`).\n\t *\n\t * The hook resolves its config (`endpoint`, `source`, `token`, `sessionId`)\n\t * from `_meta[\"waniwani/widget\"]` in this object. The hook never opens a host\n\t * connection of its own — your app owns the host bridge and passes the\n\t * metadata in. Ignored when an explicit `endpoint` is passed.\n\t */\n\ttoolResponseMetadata?: Record<string, unknown> | null;\n}\n\n/**\n * Metadata-driven options: `endpoint` and `source` are resolved from the\n * `toolResponseMetadata` you pass (`_meta[\"waniwani/widget\"]`). `source` may\n * be overridden explicitly.\n */\ninterface ContextDrivenOptions extends BaseUseWaniwaniOptions {\n\tendpoint?: undefined;\n\t/** Optional override; otherwise resolved from `toolResponseMetadata`. */\n\tsource?: string;\n}\n\n/**\n * Explicit-endpoint options: when `endpoint` is passed directly, `source`\n * is required so events never get stamped with a placeholder.\n */\ninterface ExplicitEndpointOptions extends BaseUseWaniwaniOptions {\n\t/** V2 batch endpoint URL to POST tracking events to. */\n\tendpoint: string;\n\t/** Required when `endpoint` is explicit (e.g. `\"chatgpt\"`, `\"chatbar\"`). */\n\tsource: string;\n}\n\n/**\n * Options for the useWaniwani hook. Either rely on the widget host\n * (omit `endpoint`) or pass `endpoint` + `source` explicitly.\n */\nexport type UseWaniwaniOptions = ContextDrivenOptions | ExplicitEndpointOptions;\n\n/**\n * The tracking API returned by `useWaniwani()`.\n */\nexport interface WaniwaniWidget {\n\t/**\n\t * The session ID stamped on every event this widget emits, so hosts can\n\t * correlate widget activity with server-side tracking.\n\t *\n\t * Resolved from (1) the explicit `sessionId` option, (2) the widget config\n\t * injected by `withWaniwani` (`_meta[\"waniwani/widget\"].sessionId`), else a\n\t * random UUID generated on mount. `undefined` until the widget initializes\n\t * and when no config resolves (no-op widget).\n\t */\n\treadonly sessionId?: string;\n\t/**\n\t * Track a typed event. The exact same surface as the server client:\n\t * `track({ event: \"session.started\" })`,\n\t * `track.priceShown({ amount, currency })`, `track.converted({ ... })`.\n\t * Identity (session, trace, user) is stamped automatically.\n\t */\n\ttrack: TrackFn;\n\t/** Tie all subsequent widget events to this user (emits `user.identified`). */\n\tidentify(\n\t\tuserId: string,\n\t\ttraits?: Record<string, unknown>,\n\t): Promise<{ eventId: string }>;\n\t/** Flush buffered events immediately instead of waiting for the timer. */\n\tflush(): Promise<void>;\n}\n\nconst NOOP_EMIT = async (): Promise<{ eventId: string }> => ({ eventId: \"\" });\n\nfunction createNoopTrack(): TrackFn {\n\treturn Object.assign(NOOP_EMIT, {\n\t\tpriceShown: NOOP_EMIT,\n\t\tpricesCompared: NOOP_EMIT,\n\t\toptionSelected: NOOP_EMIT,\n\t\tleadQualified: NOOP_EMIT,\n\t\tconverted: NOOP_EMIT,\n\t}) as TrackFn;\n}\n\n/** No-op widget that silently discards all calls. */\nconst NOOP_WIDGET: WaniwaniWidget = {\n\tsessionId: undefined,\n\ttrack: createNoopTrack(),\n\tidentify: NOOP_EMIT,\n\tflush: async () => {},\n};\n\ninterface ResolvedConfig extends WaniwaniConfig {\n\tsource: string;\n}\n\ninterface WidgetState {\n\twidget: WaniwaniWidget;\n\tcleanup: () => void;\n\tconfig: ResolvedConfig | null;\n}\n\n/** Module-level singleton — shared across all hook consumers. */\nlet state: WidgetState | null = null;\nlet consumerCount = 0;\n\nfunction normalizeString(value: unknown): string | undefined {\n\tif (typeof value !== \"string\") {\n\t\treturn undefined;\n\t}\n\tconst trimmed = value.trim();\n\treturn trimmed.length > 0 ? trimmed : undefined;\n}\n\n/**\n * Extract the Waniwani widget config from a tool response metadata object.\n * Reads the `waniwani/widget` key, on both the metadata root and its nested\n * `_meta`. Returns `null` when no endpoint can be resolved.\n *\n * @internal exported for unit testing.\n */\nexport function extractConfigFromMetadata(\n\tmeta: Record<string, unknown> | null,\n): WaniwaniConfig | null {\n\tif (!meta) {\n\t\treturn null;\n\t}\n\n\tconst nestedMeta = meta._meta as Record<string, unknown> | undefined;\n\tconst waniwani = (meta[WIDGET_CONFIG_META_KEY] ??\n\t\tnestedMeta?.[WIDGET_CONFIG_META_KEY]) as WaniwaniMeta | undefined;\n\tconst endpoint = normalizeString(waniwani?.endpoint);\n\tif (!endpoint) {\n\t\treturn null;\n\t}\n\n\treturn {\n\t\tendpoint,\n\t\ttoken: normalizeString(waniwani?.token),\n\t\tsessionId: normalizeString(waniwani?.sessionId),\n\t\tsource: normalizeString(waniwani?.source),\n\t};\n}\n\nfunction isSameConfig(\n\ta: WaniwaniConfig | null | undefined,\n\tb: WaniwaniConfig | null | undefined,\n): boolean {\n\treturn (\n\t\ta?.endpoint === b?.endpoint &&\n\t\ta?.token === b?.token &&\n\t\ta?.sessionId === b?.sessionId &&\n\t\ta?.source === b?.source\n\t);\n}\n\nfunction createState(\n\tconfig: ResolvedConfig,\n\tmetadata?: Record<string, unknown>,\n): WidgetState {\n\tconst sessionId = config.sessionId ?? crypto.randomUUID();\n\tconst traceId = crypto.randomUUID();\n\n\tconst client = createFrontendClient({\n\t\tendpoint: config.endpoint,\n\t\ttoken: config.token,\n\t\tsource: config.source,\n\t\tidentity: () => ({ sessionId, traceId }),\n\t\tmetadata,\n\t});\n\n\t// The top-of-widget-funnel signal: emitted once per mount with resolved\n\t// config, so \"widget shown\" exists even when nothing is tracked manually.\n\tvoid client.track({ event: \"widget_render\" });\n\n\treturn {\n\t\tconfig,\n\t\twidget: {\n\t\t\tsessionId,\n\t\t\ttrack: client.track,\n\t\t\tidentify: (userId, traits) => client.identify(userId, traits),\n\t\t\tflush: () => client.flush(),\n\t\t},\n\t\tcleanup: () => {\n\t\t\tvoid client.shutdown();\n\t\t},\n\t};\n}\n\n/**\n * React hook for tracking from inside an MCP-app widget. Returns the same\n * `track` surface as the server client, with session identity stamped\n * automatically from the config `withWaniwani` injects into tool responses.\n *\n * The hook never opens a host connection of its own. Supply config one of\n * two ways:\n * 1. Explicit `endpoint` + `source` (plus optional `token` / `sessionId`)\n * 2. `toolResponseMetadata` — the `_meta` object your host already exposes\n * (e.g. skybridge's `useToolInfo().responseMetadata`); the hook reads\n * `_meta[\"waniwani/widget\"]` from it\n *\n * No-op when neither resolves an `endpoint` and a `source`.\n *\n * @example\n * ```tsx\n * function MyWidget() {\n * const { responseMetadata } = useToolInfo();\n * const wani = useWaniwani({ toolResponseMetadata: responseMetadata });\n * // wani.sessionId correlates with server-side tracking\n * return (\n * <button\n * onClick={() =>\n * wani.track.optionSelected({ id: \"pro\", amount: 49, currency: \"EUR\" })\n * }\n * >\n * Choose Pro\n * </button>\n * );\n * }\n * ```\n */\nexport function useWaniwani(options: UseWaniwaniOptions = {}): WaniwaniWidget {\n\tconst explicitEndpoint = normalizeString(options.endpoint);\n\tconst explicitToken = normalizeString(options.token);\n\tconst explicitSessionId = normalizeString(options.sessionId);\n\tconst explicitSource = normalizeString(options.source);\n\n\t// Config from the host metadata passed in. Ignored in explicit-endpoint\n\t// mode, where every field comes from the options directly.\n\tconst suppliedConfig = explicitEndpoint\n\t\t? null\n\t\t: extractConfigFromMetadata(options.toolResponseMetadata ?? null);\n\tconst ctxEndpoint = suppliedConfig?.endpoint;\n\tconst ctxToken = suppliedConfig?.token;\n\tconst ctxSessionId = suppliedConfig?.sessionId;\n\tconst ctxSource = suppliedConfig?.source;\n\n\t// Stabilize config identity — only changes when the resolved primitives do,\n\t// so a new `toolResponseMetadata` object of identical content is a no-op.\n\tconst config = useMemo<ResolvedConfig | null>(() => {\n\t\tconst source = explicitSource ?? ctxSource;\n\t\tif (!source) {\n\t\t\treturn null;\n\t\t}\n\t\tif (explicitEndpoint) {\n\t\t\treturn {\n\t\t\t\tendpoint: explicitEndpoint,\n\t\t\t\ttoken: explicitToken,\n\t\t\t\tsessionId: explicitSessionId,\n\t\t\t\tsource,\n\t\t\t};\n\t\t}\n\t\tif (!ctxEndpoint) {\n\t\t\treturn null;\n\t\t}\n\t\treturn {\n\t\t\tendpoint: ctxEndpoint,\n\t\t\ttoken: explicitToken ?? ctxToken,\n\t\t\tsessionId: explicitSessionId ?? ctxSessionId,\n\t\t\tsource,\n\t\t};\n\t}, [\n\t\texplicitEndpoint,\n\t\texplicitToken,\n\t\texplicitSessionId,\n\t\texplicitSource,\n\t\tctxEndpoint,\n\t\tctxToken,\n\t\tctxSessionId,\n\t\tctxSource,\n\t]);\n\n\tconst [widget, setWidget] = useState<WaniwaniWidget>(NOOP_WIDGET);\n\t// Ref, not a dependency: metadata is captured when the singleton is\n\t// created and must not force a transport restart on every render.\n\tconst metadataRef = useRef(options.metadata);\n\tmetadataRef.current = options.metadata;\n\n\t// Create/swap singleton state when config changes.\n\t// All side effects (timers, network) happen here in useEffect, making\n\t// this safe in Strict Mode and concurrent rendering.\n\t//\n\t// Only consumers with a resolved config hold a stake in the singleton.\n\t// A consumer with `config === null` becomes a local no-op without\n\t// touching the singleton (other consumers may still be driving it),\n\t// and the singleton is torn down only when the last stake is released.\n\tuseEffect(() => {\n\t\tif (typeof window === \"undefined\") {\n\t\t\treturn;\n\t\t}\n\n\t\tif (!config) {\n\t\t\tsetWidget(NOOP_WIDGET);\n\t\t\treturn;\n\t\t}\n\n\t\tlet current = state;\n\t\tif (!current || !isSameConfig(current.config, config)) {\n\t\t\tcurrent?.cleanup();\n\t\t\tcurrent = createState(config, metadataRef.current);\n\t\t\tstate = current;\n\t\t}\n\t\tsetWidget(current.widget);\n\t\tconsumerCount++;\n\n\t\treturn () => {\n\t\t\tconsumerCount = Math.max(consumerCount - 1, 0);\n\t\t\tif (consumerCount === 0) {\n\t\t\t\tstate?.cleanup();\n\t\t\t\tstate = null;\n\t\t\t}\n\t\t};\n\t}, [config]);\n\n\treturn widget;\n}\n\n/**\n * Reset the singleton (for testing only).\n * @internal\n */\nexport function _resetWidgetInstance(): void {\n\tstate?.cleanup();\n\tstate = null;\n}\n","// ============================================================================\n// Meta key extraction helpers\n// ============================================================================\n\n/** Pick the first non-empty string value from `meta` matching the given keys. */\nfunction pickFirst(\n\tmeta: Record<string, unknown>,\n\tkeys: readonly string[],\n): string | undefined {\n\tfor (const key of keys) {\n\t\tconst value = meta[key];\n\t\tif (typeof value === \"string\" && value.length > 0) {\n\t\t\treturn value;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n// --- Key lists (ordered by priority) ---\n\nconst SESSION_ID_KEYS = [\n\t\"waniwani/sessionId\",\n\t\"openai/sessionId\",\n\t\"openai/session\",\n\t\"sessionId\",\n\t\"conversationId\",\n\t\"mcp-session-id\",\n] as const;\n\nconst REQUEST_ID_KEYS = [\n\t\"waniwani/requestId\",\n\t\"openai/requestId\",\n\t\"requestId\",\n\t\"mcp/requestId\",\n] as const;\n\nconst TRACE_ID_KEYS = [\n\t\"waniwani/traceId\",\n\t\"openai/traceId\",\n\t\"traceId\",\n\t\"mcp/traceId\",\n\t\"openai/requestId\",\n\t\"requestId\",\n] as const;\n\nconst EXTERNAL_USER_ID_KEYS = [\n\t\"waniwani/userId\",\n\t\"openai/userId\",\n\t\"externalUserId\",\n\t\"userId\",\n\t\"actorId\",\n] as const;\n\nconst VISITOR_ID_KEYS = [\"waniwani/visitorId\", \"visitorId\"] as const;\n\nconst CORRELATION_ID_KEYS = [\"correlationId\", \"openai/requestId\"] as const;\n\nconst TURN_COUNT_KEYS = [\"waniwani/turnCount\"] as const;\n\n/** Meta key for flow execution path (nodesVisited, flowId). */\nexport const FLOW_META_KEY = \"waniwani/flow\" as const;\n\n/**\n * Meta key under which `withWaniwani` injects the widget tracking config\n * (`{ endpoint, token, sessionId, source, geoLocation }`) into tool response\n * `_meta`. The frontend client reads it to track without any manual wiring.\n * Older SDKs wrote the same object under the bare `waniwani` key.\n */\nexport const WIDGET_CONFIG_META_KEY = \"waniwani/widget\" as const;\n\n// --- Extractors ---\n\nexport function extractSessionId(\n\tmeta: Record<string, unknown> | undefined,\n): string | undefined {\n\treturn meta ? pickFirst(meta, SESSION_ID_KEYS) : undefined;\n}\n\nexport function extractRequestId(\n\tmeta: Record<string, unknown> | undefined,\n): string | undefined {\n\treturn meta ? pickFirst(meta, REQUEST_ID_KEYS) : undefined;\n}\n\nexport function extractTraceId(\n\tmeta: Record<string, unknown> | undefined,\n): string | undefined {\n\treturn meta ? pickFirst(meta, TRACE_ID_KEYS) : undefined;\n}\n\nexport function extractExternalUserId(\n\tmeta: Record<string, unknown> | undefined,\n): string | undefined {\n\treturn meta ? pickFirst(meta, EXTERNAL_USER_ID_KEYS) : undefined;\n}\n\nexport function extractVisitorId(\n\tmeta: Record<string, unknown> | undefined,\n): string | undefined {\n\treturn meta ? pickFirst(meta, VISITOR_ID_KEYS) : undefined;\n}\n\nexport function extractCorrelationId(\n\tmeta: Record<string, unknown> | undefined,\n): string | undefined {\n\treturn meta ? pickFirst(meta, CORRELATION_ID_KEYS) : undefined;\n}\n\n/**\n * Number of user messages in the current chat session, as counted by the\n * Waniwani app before dispatching the MCP request. Useful for MCPs that\n * gate behavior on conversation length (e.g. compulsory email verification\n * after N turns) without each MCP having to track turn state itself.\n *\n * Forwarded as `waniwani/turnCount` (non-negative integer).\n */\nexport function extractTurnCount(\n\tmeta: Record<string, unknown> | undefined,\n): number | undefined {\n\tif (!meta) {\n\t\treturn undefined;\n\t}\n\tfor (const key of TURN_COUNT_KEYS) {\n\t\tconst value = meta[key];\n\t\tif (typeof value === \"number\" && Number.isFinite(value) && value >= 0) {\n\t\t\treturn value;\n\t\t}\n\t}\n\treturn undefined;\n}\n\nconst SOURCE_SESSION_KEYS = [\n\t{ key: \"openai/sessionId\", source: \"chatgpt\" },\n\t{ key: \"openai/session\", source: \"chatgpt\" },\n] as const;\n\n/**\n * Client identifiers advertised via MCP `initialize` → `clientInfo.name` that\n * the SDK maps to a known source. Match is case-insensitive substring on the\n * advertised name, so this catches \"Claude\", \"Claude Code\", \"claude-ai\", etc.\n * without having to enumerate every surface.\n *\n * ChatGPT is primarily identified by its `openai/*` session key (see\n * SOURCE_SESSION_KEYS), but a session without that key still resolves here from\n * the advertised client name so assistant traffic is attributed consistently.\n * First match wins, so order the needles most-specific first.\n */\nconst CLIENT_INFO_NAME_SOURCES: ReadonlyArray<{\n\tneedle: string;\n\tsource: string;\n}> = [\n\t{ needle: \"claude\", source: \"claude\" },\n\t{ needle: \"chatgpt\", source: \"chatgpt\" },\n\t{ needle: \"openai\", source: \"chatgpt\" },\n\t{ needle: \"gemini\", source: \"gemini\" },\n];\n\nexport type ExtractSourceClientInfo = {\n\tname?: string;\n\tversion?: string;\n};\n\nexport function extractSource(\n\tmeta: Record<string, unknown> | undefined,\n\tclientInfo?: ExtractSourceClientInfo,\n): string | undefined {\n\tif (meta) {\n\t\tconst explicit = meta[\"waniwani/source\"];\n\t\tif (typeof explicit === \"string\" && explicit.length > 0) {\n\t\t\treturn explicit;\n\t\t}\n\t\tfor (const { key, source } of SOURCE_SESSION_KEYS) {\n\t\t\tconst value = meta[key];\n\t\t\tif (typeof value === \"string\" && value.length > 0) {\n\t\t\t\treturn source;\n\t\t\t}\n\t\t}\n\t}\n\t// Fall back to MCP `initialize` clientInfo.name when no _meta key matches.\n\t// Claude surfaces (Code, Desktop, claude.ai connector, Anthropic API MCP\n\t// connector) don't expose a namespaced session id in _meta but do advertise\n\t// themselves in the handshake. Per-MCP-session clientInfo is captured by the\n\t// MCP transport and exposed via server.getClientVersion().\n\tconst name = clientInfo?.name;\n\tif (typeof name === \"string\" && name.length > 0) {\n\t\tconst lower = name.toLowerCase();\n\t\tfor (const { needle, source } of CLIENT_INFO_NAME_SOURCES) {\n\t\t\tif (lower.includes(needle)) {\n\t\t\t\treturn source;\n\t\t\t}\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * HTTP request headers that identify a known caller when neither `_meta` nor\n * `clientInfo` carries a source. This is the most robust Claude signal: every\n * Claude surface sends `User-Agent: Claude-User` and `x-anthropic-client` on\n * MCP HTTP requests, whereas the MCP transport carries no session id and the\n * `initialize` `clientInfo` may not be surfaced on stateless deployments.\n */\nfunction headerValue(\n\theaders: Record<string, unknown>,\n\tkey: string,\n): string | undefined {\n\t// HTTP header names are case-insensitive; the transport may preserve the\n\t// original casing, so match the key case-insensitively rather than assuming\n\t// it was lowercased.\n\tconst lowerKey = key.toLowerCase();\n\tlet raw = headers[key] ?? headers[lowerKey];\n\tif (raw === undefined) {\n\t\tfor (const headerKey of Object.keys(headers)) {\n\t\t\tif (headerKey.toLowerCase() === lowerKey) {\n\t\t\t\traw = headers[headerKey];\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\tconst value = Array.isArray(raw) ? raw[0] : raw;\n\treturn typeof value === \"string\" && value.length > 0 ? value : undefined;\n}\n\nexport function extractSourceFromHeaders(\n\theaders: Record<string, unknown> | undefined,\n): string | undefined {\n\tif (!headers) {\n\t\treturn undefined;\n\t}\n\tconst userAgent = headerValue(headers, \"user-agent\");\n\tconst anthropicClient = headerValue(headers, \"x-anthropic-client\");\n\tif (\n\t\t(userAgent && /claude/i.test(userAgent)) ||\n\t\t(anthropicClient && /claude|anthropic/i.test(anthropicClient))\n\t) {\n\t\treturn \"claude\";\n\t}\n\t// ChatGPT / OpenAI surfaces without an `openai/*` session key still send an\n\t// OpenAI user-agent (e.g. `ChatGPT-User`), so attribute them here too.\n\tif (userAgent && /chatgpt|openai/i.test(userAgent)) {\n\t\treturn \"chatgpt\";\n\t}\n\treturn undefined;\n}\n","import {\n\textractCorrelationId,\n\textractExternalUserId,\n\textractRequestId,\n\textractSessionId,\n\textractSource,\n\textractTraceId,\n\textractVisitorId,\n} from \"../mcp/server/utils.js\";\nimport type { EventType, LegacyTrackEvent, TrackInput } from \"./@types.js\";\nimport type { V2CorrelationIds, V2EventEnvelope } from \"./v2-types.js\";\n\nconst DEFAULT_SOURCE = \"@waniwani/sdk\";\n\nexport interface MapTrackEventOptions {\n\tnow?: () => Date;\n\tgenerateId?: () => string;\n\tsource?: string;\n}\n\nexport function mapTrackEventToV2(\n\tinput: TrackInput,\n\toptions: MapTrackEventOptions = {},\n): V2EventEnvelope {\n\tconst now = options.now ?? (() => new Date());\n\tconst generateId = options.generateId ?? createEventId;\n\tconst eventName = resolveEventName(input);\n\tconst meta = toRecord(input.meta);\n\tconst metadata = toRecord(input.metadata);\n\tconst correlation = resolveCorrelationIds(input, meta);\n\tconst eventId = takeNonEmptyString(input.eventId) ?? generateId();\n\tconst timestamp = normalizeTimestamp(input.timestamp, now);\n\tconst source =\n\t\ttakeNonEmptyString(input.source) ??\n\t\textractSource(meta) ??\n\t\toptions.source ??\n\t\tDEFAULT_SOURCE;\n\tconst rawLegacy = isLegacyTrackEvent(input) ? { ...input } : undefined;\n\n\tconst mappedMetadata: Record<string, unknown> = {\n\t\t...metadata,\n\t};\n\tif (Object.keys(meta).length > 0) {\n\t\tmappedMetadata.meta = meta;\n\t}\n\tif (rawLegacy) {\n\t\tmappedMetadata.rawLegacy = rawLegacy;\n\t}\n\n\treturn {\n\t\tid: eventId,\n\t\ttype: \"mcp.event\",\n\t\tname: eventName,\n\t\tsource,\n\t\ttimestamp,\n\t\tcorrelation,\n\t\tproperties: mapProperties(input, eventName),\n\t\tmetadata: mappedMetadata,\n\t\trawLegacy,\n\t};\n}\n\nexport function createEventId(): string {\n\tif (\n\t\ttypeof crypto !== \"undefined\" &&\n\t\ttypeof crypto.randomUUID === \"function\"\n\t) {\n\t\treturn `evt_${crypto.randomUUID()}`;\n\t}\n\n\treturn `evt_${Math.random().toString(36).slice(2, 10)}_${Date.now().toString(36)}`;\n}\n\nfunction mapProperties(\n\tinput: TrackInput,\n\teventName: EventType,\n): Record<string, unknown> {\n\tif (!isLegacyTrackEvent(input)) {\n\t\treturn toRecord(input.properties);\n\t}\n\n\tconst legacyProperties = mapLegacyProperties(input, eventName);\n\tconst explicitProperties = toRecord(input.properties);\n\treturn {\n\t\t...legacyProperties,\n\t\t...explicitProperties,\n\t};\n}\n\nfunction mapLegacyProperties(\n\tinput: LegacyTrackEvent,\n\teventName: EventType,\n): Record<string, unknown> {\n\tswitch (eventName) {\n\t\tcase \"tool.called\": {\n\t\t\tconst properties: Record<string, unknown> = {};\n\t\t\tif (takeNonEmptyString(input.toolName)) {\n\t\t\t\tproperties.name = input.toolName;\n\t\t\t}\n\t\t\tif (takeNonEmptyString(input.toolType)) {\n\t\t\t\tproperties.type = input.toolType;\n\t\t\t}\n\t\t\treturn properties;\n\t\t}\n\t\tcase \"link.clicked\": {\n\t\t\tconst properties: Record<string, unknown> = {};\n\t\t\tif (takeNonEmptyString(input.linkUrl)) {\n\t\t\t\tproperties.url = input.linkUrl;\n\t\t\t}\n\t\t\treturn properties;\n\t\t}\n\t\tdefault:\n\t\t\treturn {};\n\t}\n}\n\nfunction resolveEventName(input: TrackInput): EventType {\n\treturn isLegacyTrackEvent(input) ? input.eventType : input.event;\n}\n\nfunction resolveCorrelationIds(\n\tinput: TrackInput,\n\tmeta: Record<string, unknown>,\n): V2CorrelationIds {\n\tconst requestId =\n\t\ttakeNonEmptyString(input.requestId) ?? extractRequestId(meta);\n\n\tconst sessionId =\n\t\ttakeNonEmptyString(input.sessionId) ?? extractSessionId(meta);\n\n\tconst traceId = takeNonEmptyString(input.traceId) ?? extractTraceId(meta);\n\n\tconst externalUserId =\n\t\ttakeNonEmptyString(input.externalUserId) ?? extractExternalUserId(meta);\n\n\tconst visitorId =\n\t\ttakeNonEmptyString(input.visitorId) ?? extractVisitorId(meta);\n\n\tconst correlationId =\n\t\ttakeNonEmptyString(input.correlationId) ??\n\t\textractCorrelationId(meta) ??\n\t\trequestId;\n\n\tconst correlation: V2CorrelationIds = {};\n\tif (sessionId) {\n\t\tcorrelation.sessionId = sessionId;\n\t}\n\tif (traceId) {\n\t\tcorrelation.traceId = traceId;\n\t}\n\tif (requestId) {\n\t\tcorrelation.requestId = requestId;\n\t}\n\tif (correlationId) {\n\t\tcorrelation.correlationId = correlationId;\n\t}\n\tif (externalUserId) {\n\t\tcorrelation.externalUserId = externalUserId;\n\t}\n\tif (visitorId) {\n\t\tcorrelation.visitorId = visitorId;\n\t}\n\treturn correlation;\n}\n\nfunction normalizeTimestamp(\n\tinput: string | Date | undefined,\n\tnow: () => Date,\n): string {\n\tif (input instanceof Date) {\n\t\treturn input.toISOString();\n\t}\n\tif (typeof input === \"string\") {\n\t\tconst date = new Date(input);\n\t\tif (!Number.isNaN(date.getTime())) {\n\t\t\treturn date.toISOString();\n\t\t}\n\t}\n\treturn now().toISOString();\n}\n\nfunction toRecord(value: unknown): Record<string, unknown> {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) {\n\t\treturn {};\n\t}\n\treturn value as Record<string, unknown>;\n}\n\nfunction takeNonEmptyString(value: unknown): string | undefined {\n\tif (typeof value !== \"string\") {\n\t\treturn undefined;\n\t}\n\tif (value.trim().length === 0) {\n\t\treturn undefined;\n\t}\n\treturn value;\n}\n\nfunction isLegacyTrackEvent(input: TrackInput): input is LegacyTrackEvent {\n\treturn \"eventType\" in input;\n}\n","import type { RevenueTrackingApi, TrackInput } from \"./@types.js\";\n\n/** Enqueue one event and resolve to its id. */\ntype Emit = (event: TrackInput) => Promise<{ eventId: string }>;\n\n/**\n * Build the flat `track.*` revenue helpers over an `emit` function. Shared\n * by the top-level client and the request-scoped client (which injects meta\n * into `emit`) so both expose the same revenue API (WAN-386).\n *\n * Each helper maps its flat input to a typed first-class revenue event; the\n * remaining tracking context (sessionId / externalUserId / meta) is forwarded\n * as-is. Identity (one of those, or meta-derived) is required by the ingest\n * API — see the warning emitted from the client's `emit`.\n */\nexport function createRevenueApi(emit: Emit): RevenueTrackingApi {\n\treturn {\n\t\tpriceShown: ({ amount, currency, itemId, label, ...context }) =>\n\t\t\temit({\n\t\t\t\tevent: \"price_shown\",\n\t\t\t\tproperties: { amount, currency, itemId, label },\n\t\t\t\t...context,\n\t\t\t}),\n\t\tpricesCompared: ({ options, ...context }) =>\n\t\t\temit({ event: \"prices_compared\", properties: { options }, ...context }),\n\t\toptionSelected: ({ id, amount, currency, ...context }) =>\n\t\t\temit({\n\t\t\t\tevent: \"option_selected\",\n\t\t\t\tproperties: { id, amount, currency },\n\t\t\t\t...context,\n\t\t\t}),\n\t\tleadQualified: (input) => {\n\t\t\tconst { externalId, email, name, ...context } = input ?? {};\n\t\t\treturn emit({\n\t\t\t\tevent: \"lead_qualified\",\n\t\t\t\tproperties: { externalId, email, name },\n\t\t\t\t...context,\n\t\t\t});\n\t\t},\n\t\tconverted: ({ amount, currency, occurredAt, ...context }) =>\n\t\t\temit({\n\t\t\t\tevent: \"converted\",\n\t\t\t\tproperties: { amount, currency, occurredAt },\n\t\t\t\t...context,\n\t\t\t}),\n\t};\n}\n","import type {\n\tTrackingShutdownOptions,\n\tTrackingShutdownResult,\n} from \"./@types.js\";\nimport type {\n\tV2BatchRejectedEvent,\n\tV2BatchRequest,\n\tV2BatchResponse,\n\tV2EventEnvelope,\n} from \"./v2-types.js\";\n\ntype FetchFn = (\n\tinput: URL | RequestInfo,\n\tinit?: RequestInit,\n) => Promise<Response>;\n\nconst DEFAULT_ENDPOINT_PATH = \"/api/mcp/events/v2/batch\";\nconst DEFAULT_FLUSH_INTERVAL_MS = 1_000;\nconst DEFAULT_MAX_BATCH_SIZE = 20;\nconst DEFAULT_MAX_BUFFER_SIZE = 1_000;\nconst DEFAULT_MAX_RETRIES = 3;\nconst DEFAULT_RETRY_BASE_DELAY_MS = 200;\nconst DEFAULT_RETRY_MAX_DELAY_MS = 2_000;\nconst DEFAULT_SHUTDOWN_TIMEOUT_MS = 2_000;\nconst SDK_NAME = \"@waniwani/sdk\";\n// Keepalive requests are capped by browsers (64KiB in-flight quota); stay under it.\nconst TEARDOWN_MAX_BODY_BYTES = 60_000;\n\nconst AUTH_FAILURE_STATUS = new Set([401, 403]);\nconst RETRYABLE_STATUS = new Set([408, 425, 429, 500, 502, 503, 504]);\n\ninterface Logger {\n\twarn: (message: string, ...args: unknown[]) => void;\n\terror: (message: string, ...args: unknown[]) => void;\n}\n\nexport interface V2TransportOptions {\n\tapiUrl: string;\n\t/**\n\t * Bearer credential for the ingest endpoint: secret key (`wwk_`), public\n\t * token (`wwp_`), or widget JWT. Omit only for unauthenticated proxies.\n\t */\n\tapiKey?: string;\n\tendpointPath?: string;\n\tflushIntervalMs?: number;\n\tmaxBatchSize?: number;\n\tmaxBufferSize?: number;\n\tmaxRetries?: number;\n\tretryBaseDelayMs?: number;\n\tretryMaxDelayMs?: number;\n\tshutdownTimeoutMs?: number;\n\tsdkVersion?: string;\n\tfetchFn?: FetchFn;\n\tlogger?: Logger;\n\tnow?: () => Date;\n\tsleep?: (delayMs: number) => Promise<void>;\n}\n\nexport interface V2BatchTransport {\n\tenqueue: (event: V2EventEnvelope) => void;\n\tflush: () => Promise<void>;\n\tshutdown: (\n\t\toptions?: TrackingShutdownOptions,\n\t) => Promise<TrackingShutdownResult>;\n\tpendingEvents: () => number;\n}\n\ntype SendBatchResult =\n\t| { kind: \"success\" }\n\t| { kind: \"retryable\"; reason: string }\n\t| { kind: \"permanent\"; reason: string }\n\t| { kind: \"auth\"; status: number }\n\t| {\n\t\t\tkind: \"partial\";\n\t\t\tretryable: V2EventEnvelope[];\n\t\t\tpermanent: V2EventEnvelope[];\n\t };\n\nexport function createV2BatchTransport(\n\toptions: V2TransportOptions,\n): V2BatchTransport {\n\treturn new BatchingV2Transport(options);\n}\n\nclass BatchingV2Transport implements V2BatchTransport {\n\tprivate readonly endpointUrl: string;\n\tprivate readonly flushIntervalMs: number;\n\tprivate readonly maxBatchSize: number;\n\tprivate readonly maxBufferSize: number;\n\tprivate readonly maxRetries: number;\n\tprivate readonly retryBaseDelayMs: number;\n\tprivate readonly retryMaxDelayMs: number;\n\tprivate readonly shutdownTimeoutMs: number;\n\tprivate readonly sdkVersion?: string;\n\tprivate readonly fetchFn: FetchFn;\n\tprivate readonly logger: Logger;\n\tprivate readonly now: () => Date;\n\tprivate readonly sleep: (delayMs: number) => Promise<void>;\n\tprivate readonly apiKey?: string;\n\n\tprivate readonly buffer: V2EventEnvelope[] = [];\n\tprivate flushTimer: ReturnType<typeof setInterval> | undefined;\n\tprivate flushScheduled = false;\n\tprivate flushScheduledTimer: ReturnType<typeof setTimeout> | undefined;\n\tprivate flushInFlight: Promise<void> | undefined;\n\tprivate inFlightCount = 0;\n\tprivate isStopped = false;\n\tprivate isShuttingDown = false;\n\n\tconstructor(options: V2TransportOptions) {\n\t\tthis.endpointUrl = joinUrl(\n\t\t\toptions.apiUrl,\n\t\t\toptions.endpointPath ?? DEFAULT_ENDPOINT_PATH,\n\t\t);\n\t\tthis.flushIntervalMs = options.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;\n\t\tthis.maxBatchSize = options.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;\n\t\tthis.maxBufferSize = options.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE;\n\t\tthis.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;\n\t\tthis.retryBaseDelayMs =\n\t\t\toptions.retryBaseDelayMs ?? DEFAULT_RETRY_BASE_DELAY_MS;\n\t\tthis.retryMaxDelayMs =\n\t\t\toptions.retryMaxDelayMs ?? DEFAULT_RETRY_MAX_DELAY_MS;\n\t\tthis.shutdownTimeoutMs =\n\t\t\toptions.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;\n\t\tthis.fetchFn = options.fetchFn ?? fetch;\n\t\tthis.logger = options.logger ?? console;\n\t\tthis.now = options.now ?? (() => new Date());\n\t\tthis.sleep =\n\t\t\toptions.sleep ??\n\t\t\t((delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)));\n\t\tthis.apiKey = options.apiKey;\n\t\tthis.sdkVersion = options.sdkVersion;\n\n\t\tif (this.flushIntervalMs > 0) {\n\t\t\tthis.flushTimer = setInterval(() => {\n\t\t\t\tvoid this.flush();\n\t\t\t}, this.flushIntervalMs);\n\t\t}\n\n\t\tthis.registerBrowserTeardown();\n\t}\n\n\tenqueue(event: V2EventEnvelope): void {\n\t\tif (this.isStopped || this.isShuttingDown) {\n\t\t\tthis.logger.warn(\n\t\t\t\t\"[Waniwani] Tracking transport is stopped, dropping event %s\",\n\t\t\t\tevent.id,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.buffer.length >= this.maxBufferSize) {\n\t\t\tconst dropCount = this.buffer.length - this.maxBufferSize + 1;\n\t\t\tthis.buffer.splice(0, dropCount);\n\t\t\tthis.logger.warn(\n\t\t\t\t\"[Waniwani] Tracking buffer overflow, dropped %d oldest event(s)\",\n\t\t\t\tdropCount,\n\t\t\t);\n\t\t}\n\n\t\tthis.buffer.push(event);\n\n\t\tif (this.buffer.length >= this.maxBatchSize) {\n\t\t\tvoid this.flush();\n\t\t\treturn;\n\t\t}\n\n\t\tthis.scheduleMicroFlush();\n\t}\n\n\tpendingEvents(): number {\n\t\treturn this.buffer.length + this.inFlightCount;\n\t}\n\n\tasync flush(): Promise<void> {\n\t\tif (this.flushInFlight) {\n\t\t\treturn this.flushInFlight;\n\t\t}\n\t\tthis.flushInFlight = this.flushLoop().finally(() => {\n\t\t\tthis.flushInFlight = undefined;\n\t\t});\n\t\treturn this.flushInFlight;\n\t}\n\n\tasync shutdown(\n\t\toptions?: TrackingShutdownOptions,\n\t): Promise<TrackingShutdownResult> {\n\t\tthis.isShuttingDown = true;\n\t\tif (this.flushTimer) {\n\t\t\tclearInterval(this.flushTimer);\n\t\t\tthis.flushTimer = undefined;\n\t\t}\n\t\tif (this.flushScheduledTimer) {\n\t\t\tclearTimeout(this.flushScheduledTimer);\n\t\t\tthis.flushScheduledTimer = undefined;\n\t\t\tthis.flushScheduled = false;\n\t\t}\n\n\t\tconst timeoutMs = options?.timeoutMs ?? this.shutdownTimeoutMs;\n\t\tconst flushPromise = this.flush();\n\n\t\tif (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {\n\t\t\tawait flushPromise;\n\t\t\tthis.isStopped = true;\n\t\t\treturn { timedOut: false, pendingEvents: this.pendingEvents() };\n\t\t}\n\n\t\tconst timeoutSignal = Symbol(\"shutdown-timeout\");\n\t\tconst result = await Promise.race([\n\t\t\tflushPromise.then(() => \"flushed\" as const),\n\t\t\tthis.sleep(timeoutMs).then(() => timeoutSignal),\n\t\t]);\n\n\t\tif (result === timeoutSignal) {\n\t\t\tthis.isStopped = true;\n\t\t\treturn { timedOut: true, pendingEvents: this.pendingEvents() };\n\t\t}\n\n\t\tthis.isStopped = true;\n\t\treturn { timedOut: false, pendingEvents: this.pendingEvents() };\n\t}\n\n\t/**\n\t * In browsers, drain the buffer with keepalive fetches when the page is\n\t * hidden or unloading. The regular async flush loop does not survive\n\t * navigation; `keepalive` requests do. No-op outside a DOM environment.\n\t */\n\tprivate registerBrowserTeardown(): void {\n\t\tif (typeof document === \"undefined\" || typeof window === \"undefined\") {\n\t\t\treturn;\n\t\t}\n\n\t\tconst teardownFlush = () => {\n\t\t\tif (document.visibilityState === \"hidden\") {\n\t\t\t\tthis.keepaliveFlush();\n\t\t\t}\n\t\t};\n\t\tdocument.addEventListener(\"visibilitychange\", teardownFlush);\n\t\twindow.addEventListener(\"pagehide\", () => this.keepaliveFlush());\n\t}\n\n\t/** Fire-and-forget flush of everything buffered, chunked under the keepalive body cap. */\n\tprivate keepaliveFlush(): void {\n\t\tif (this.isStopped || this.buffer.length === 0) {\n\t\t\treturn;\n\t\t}\n\t\tconst events = this.buffer.splice(0, this.buffer.length);\n\t\tthis.sendKeepaliveChunk(events);\n\t}\n\n\tprivate sendKeepaliveChunk(events: V2EventEnvelope[]): void {\n\t\tconst body = JSON.stringify(this.makeBatchRequest(events));\n\t\tif (body.length > TEARDOWN_MAX_BODY_BYTES && events.length > 1) {\n\t\t\tconst mid = Math.ceil(events.length / 2);\n\t\t\tthis.sendKeepaliveChunk(events.slice(0, mid));\n\t\t\tthis.sendKeepaliveChunk(events.slice(mid));\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tvoid this.fetchFn(this.endpointUrl, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: this.requestHeaders(),\n\t\t\t\tbody,\n\t\t\t\tkeepalive: true,\n\t\t\t}).catch(() => {});\n\t\t} catch {\n\t\t\t// Teardown is best-effort; never throw during page unload.\n\t\t}\n\t}\n\n\tprivate requestHeaders(): Record<string, string> {\n\t\tconst headers: Record<string, string> = {\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\"X-WaniWani-SDK\": SDK_NAME,\n\t\t};\n\t\tif (this.apiKey) {\n\t\t\theaders.Authorization = `Bearer ${this.apiKey}`;\n\t\t}\n\t\treturn headers;\n\t}\n\n\tprivate scheduleMicroFlush(): void {\n\t\tif (this.flushScheduled) {\n\t\t\treturn;\n\t\t}\n\t\tthis.flushScheduled = true;\n\t\tthis.flushScheduledTimer = setTimeout(() => {\n\t\t\tthis.flushScheduledTimer = undefined;\n\t\t\tthis.flushScheduled = false;\n\t\t\tvoid this.flush();\n\t\t}, 0);\n\t}\n\n\tprivate async flushLoop(): Promise<void> {\n\t\twhile (this.buffer.length > 0 && !this.isStopped) {\n\t\t\tconst batch = this.buffer.splice(0, this.maxBatchSize);\n\t\t\tawait this.sendBatchWithRetry(batch);\n\t\t}\n\t}\n\n\tprivate async sendBatchWithRetry(batch: V2EventEnvelope[]): Promise<void> {\n\t\tlet attempt = 0;\n\t\tlet pendingBatch = batch;\n\n\t\twhile (pendingBatch.length > 0 && !this.isStopped) {\n\t\t\tthis.inFlightCount = pendingBatch.length;\n\t\t\tconst result = await this.sendBatchOnce(pendingBatch);\n\t\t\tthis.inFlightCount = 0;\n\n\t\t\tswitch (result.kind) {\n\t\t\t\tcase \"success\":\n\t\t\t\t\treturn;\n\t\t\t\tcase \"auth\":\n\t\t\t\t\tthis.stopTransportForAuthFailure(result.status, pendingBatch.length);\n\t\t\t\t\treturn;\n\t\t\t\tcase \"permanent\":\n\t\t\t\t\tthis.logger.error(\n\t\t\t\t\t\t\"[Waniwani] Dropping %d event(s) after permanent failure: %s\",\n\t\t\t\t\t\tpendingBatch.length,\n\t\t\t\t\t\tresult.reason,\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t\tcase \"retryable\":\n\t\t\t\t\tif (attempt >= this.maxRetries) {\n\t\t\t\t\t\tthis.logger.error(\n\t\t\t\t\t\t\t\"[Waniwani] Dropping %d event(s) after retry exhaustion: %s\",\n\t\t\t\t\t\t\tpendingBatch.length,\n\t\t\t\t\t\t\tresult.reason,\n\t\t\t\t\t\t);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tawait this.sleep(this.backoffDelayMs(attempt));\n\t\t\t\t\tattempt += 1;\n\t\t\t\t\tcontinue;\n\t\t\t\tcase \"partial\":\n\t\t\t\t\tif (result.permanent.length > 0) {\n\t\t\t\t\t\tthis.logger.error(\n\t\t\t\t\t\t\t\"[Waniwani] Dropping %d event(s) rejected as permanent\",\n\t\t\t\t\t\t\tresult.permanent.length,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tif (result.retryable.length === 0) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tif (attempt >= this.maxRetries) {\n\t\t\t\t\t\tthis.logger.error(\n\t\t\t\t\t\t\t\"[Waniwani] Dropping %d retryable event(s) after retry exhaustion\",\n\t\t\t\t\t\t\tresult.retryable.length,\n\t\t\t\t\t\t);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tpendingBatch = result.retryable;\n\t\t\t\t\tawait this.sleep(this.backoffDelayMs(attempt));\n\t\t\t\t\tattempt += 1;\n\t\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async sendBatchOnce(\n\t\tevents: V2EventEnvelope[],\n\t): Promise<SendBatchResult> {\n\t\tlet response: Response;\n\n\t\ttry {\n\t\t\tresponse = await this.fetchFn(this.endpointUrl, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: this.requestHeaders(),\n\t\t\t\tbody: JSON.stringify(this.makeBatchRequest(events)),\n\t\t\t});\n\t\t} catch (error) {\n\t\t\treturn {\n\t\t\t\tkind: \"retryable\",\n\t\t\t\treason: getErrorMessage(error),\n\t\t\t};\n\t\t}\n\n\t\tif (AUTH_FAILURE_STATUS.has(response.status)) {\n\t\t\treturn { kind: \"auth\", status: response.status };\n\t\t}\n\n\t\tif (RETRYABLE_STATUS.has(response.status)) {\n\t\t\treturn {\n\t\t\t\tkind: \"retryable\",\n\t\t\t\treason: `HTTP ${response.status}`,\n\t\t\t};\n\t\t}\n\n\t\tif (!response.ok) {\n\t\t\treturn {\n\t\t\t\tkind: \"permanent\",\n\t\t\t\treason: `HTTP ${response.status}`,\n\t\t\t};\n\t\t}\n\n\t\tconst data = await parseJsonResponse<V2BatchResponse>(response);\n\t\tif (!data?.rejected || data.rejected.length === 0) {\n\t\t\treturn { kind: \"success\" };\n\t\t}\n\n\t\tconst partial = this.classifyRejectedEvents(events, data.rejected);\n\t\tif (partial.retryable.length === 0 && partial.permanent.length === 0) {\n\t\t\treturn { kind: \"success\" };\n\t\t}\n\n\t\treturn {\n\t\t\tkind: \"partial\",\n\t\t\tretryable: partial.retryable,\n\t\t\tpermanent: partial.permanent,\n\t\t};\n\t}\n\n\tprivate makeBatchRequest(events: V2EventEnvelope[]): V2BatchRequest {\n\t\treturn {\n\t\t\tsentAt: this.now().toISOString(),\n\t\t\tsource: {\n\t\t\t\tsdk: SDK_NAME,\n\t\t\t\tversion: this.sdkVersion ?? \"0.0.0\",\n\t\t\t},\n\t\t\tevents,\n\t\t};\n\t}\n\n\tprivate classifyRejectedEvents(\n\t\tevents: V2EventEnvelope[],\n\t\trejected: V2BatchRejectedEvent[],\n\t): {\n\t\tretryable: V2EventEnvelope[];\n\t\tpermanent: V2EventEnvelope[];\n\t} {\n\t\tconst byId = new Map(events.map((event) => [event.id, event]));\n\t\tconst retryable: V2EventEnvelope[] = [];\n\t\tconst permanent: V2EventEnvelope[] = [];\n\n\t\tfor (const rejectedEvent of rejected) {\n\t\t\tconst event = byId.get(rejectedEvent.eventId);\n\t\t\tif (!event) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (isRetryableRejectedEvent(rejectedEvent)) {\n\t\t\t\tretryable.push(event);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tpermanent.push(event);\n\t\t}\n\n\t\treturn { retryable, permanent };\n\t}\n\n\tprivate backoffDelayMs(attempt: number): number {\n\t\tconst rawDelay = this.retryBaseDelayMs * 2 ** attempt;\n\t\treturn Math.min(rawDelay, this.retryMaxDelayMs);\n\t}\n\n\tprivate stopTransportForAuthFailure(\n\t\tstatus: number,\n\t\trejectedCount: number,\n\t): void {\n\t\tthis.isStopped = true;\n\t\tconst buffered = this.buffer.length;\n\t\tthis.buffer.splice(0, buffered);\n\t\tthis.logger.error(\n\t\t\t\"[Waniwani] Auth failure (HTTP %d). Stopping tracking transport and dropping %d queued event(s)\",\n\t\t\tstatus,\n\t\t\trejectedCount + buffered,\n\t\t);\n\t}\n}\n\nfunction isRetryableRejectedEvent(\n\trejectedEvent: V2BatchRejectedEvent,\n): boolean {\n\tif (rejectedEvent.retryable === true) {\n\t\treturn true;\n\t}\n\tconst code = rejectedEvent.code.toLowerCase();\n\treturn (\n\t\tcode.includes(\"timeout\") ||\n\t\tcode.includes(\"temporary\") ||\n\t\tcode.includes(\"unavailable\") ||\n\t\tcode.includes(\"rate_limit\") ||\n\t\tcode.includes(\"transient\") ||\n\t\tcode.includes(\"server\")\n\t);\n}\n\nasync function parseJsonResponse<T>(\n\tresponse: Response,\n): Promise<T | undefined> {\n\tconst body = await response.text();\n\tif (!body) {\n\t\treturn undefined;\n\t}\n\ttry {\n\t\treturn JSON.parse(body) as T;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction joinUrl(apiUrl: string, endpointPath: string): string {\n\tconst normalizedBase = apiUrl.endsWith(\"/\") ? apiUrl : `${apiUrl}/`;\n\tconst normalizedPath = endpointPath.startsWith(\"/\")\n\t\t? endpointPath.slice(1)\n\t\t: endpointPath;\n\treturn `${normalizedBase}${normalizedPath}`;\n}\n\nfunction getErrorMessage(error: unknown): string {\n\tif (error instanceof Error) {\n\t\treturn error.message;\n\t}\n\treturn String(error);\n}\n","import type {\n\tTrackFn,\n\tTrackInput,\n\tTrackingShutdownOptions,\n\tTrackingShutdownResult,\n} from \"./@types.js\";\nimport { mapTrackEventToV2 } from \"./mapper.js\";\nimport { createRevenueApi } from \"./revenue.js\";\nimport { createV2BatchTransport } from \"./transport.js\";\n\n/**\n * Correlation identity read at emit time, so identifiers that only exist\n * later (a server-assigned chat session id, for example) are picked up on\n * every event without re-creating the client.\n */\nexport interface FrontendIdentity {\n\tsessionId?: string;\n\tvisitorId?: string;\n\texternalUserId?: string;\n\ttraceId?: string;\n}\n\nexport interface FrontendClientOptions {\n\t/**\n\t * Full URL of the events ingest endpoint\n\t * (e.g. `https://app.waniwani.ai/api/mcp/events/v2/batch`).\n\t */\n\tendpoint: string;\n\t/**\n\t * Browser-safe credential: an environment public token (`wwp_...`) or a\n\t * widget JWT injected by `withWaniwani` into tool response `_meta`. Omit\n\t * only when the endpoint is your own unauthenticated proxy route.\n\t */\n\ttoken?: string;\n\t/**\n\t * Channel attribution stamped on every event (e.g. `\"chatgpt\"`, `\"web\"`).\n\t * The ingest API rejects events it cannot attribute to a channel, so set\n\t * this (or `channelId`) unless every event carries its own. Accepts a\n\t * getter for values that resolve after client creation.\n\t */\n\tsource?: string | (() => string | undefined);\n\t/**\n\t * Exact channel attribution, stamped as `properties.channelId` on every\n\t * event that does not set its own. Accepts a getter for values that\n\t * resolve after client creation.\n\t */\n\tchannelId?: string | (() => string | undefined);\n\t/** Live identity, called on every emit. */\n\tidentity?: () => FrontendIdentity;\n\t/** Extra fields merged into every event's envelope metadata. */\n\tmetadata?: Record<string, unknown>;\n\t/** Periodic flush interval for buffered events. */\n\tflushIntervalMs?: number;\n}\n\n/**\n * The browser counterpart of the server tracking client: the exact same\n * `track` surface (callable plus the flat revenue helpers) over the same\n * batching transport, with identity stamped automatically.\n */\nexport interface FrontendTrackingClient {\n\ttrack: TrackFn;\n\t/**\n\t * Attach a stable external user id. Emits `user.identified` and stamps\n\t * `externalUserId` on every subsequent event from this client.\n\t */\n\tidentify(\n\t\tuserId: string,\n\t\tproperties?: Record<string, unknown>,\n\t): Promise<{ eventId: string }>;\n\tflush(): Promise<void>;\n\tshutdown(options?: TrackingShutdownOptions): Promise<TrackingShutdownResult>;\n}\n\n/**\n * Create a tracking client for browser surfaces (MCP-app widgets, the chat\n * embed, or any page holding a `wwp_` public token). Events flush in batches,\n * survive page navigation via keepalive teardown, and never throw into the\n * host page.\n */\nexport function createFrontendClient(\n\toptions: FrontendClientOptions,\n): FrontendTrackingClient {\n\tconst { apiUrl, endpointPath } = splitEndpoint(options.endpoint);\n\tconst transport = createV2BatchTransport({\n\t\tapiUrl,\n\t\tapiKey: options.token,\n\t\tendpointPath,\n\t\tflushIntervalMs: options.flushIntervalMs,\n\t});\n\n\tlet identifiedUserId: string | undefined;\n\n\tconst resolve = (\n\t\tvalue: string | (() => string | undefined) | undefined,\n\t): string | undefined => (typeof value === \"function\" ? value() : value);\n\n\tconst emit = async (event: TrackInput): Promise<{ eventId: string }> => {\n\t\tconst identity = options.identity?.() ?? {};\n\t\tconst channelId = resolve(options.channelId);\n\t\tconst properties =\n\t\t\tchannelId === undefined\n\t\t\t\t? event.properties\n\t\t\t\t: { channelId, ...event.properties };\n\t\tconst mapped = mapTrackEventToV2(\n\t\t\t{\n\t\t\t\tsessionId: identity.sessionId,\n\t\t\t\tvisitorId: identity.visitorId,\n\t\t\t\texternalUserId: identity.externalUserId ?? identifiedUserId,\n\t\t\t\ttraceId: identity.traceId,\n\t\t\t\t...event,\n\t\t\t\tproperties,\n\t\t\t\tmetadata: { ...options.metadata, ...event.metadata },\n\t\t\t} as TrackInput,\n\t\t\t{ source: resolve(options.source) },\n\t\t);\n\t\ttransport.enqueue(mapped);\n\t\treturn { eventId: mapped.id };\n\t};\n\n\treturn {\n\t\ttrack: Object.assign(emit, createRevenueApi(emit)),\n\t\tidentify(userId, properties) {\n\t\t\tidentifiedUserId = userId;\n\t\t\treturn emit({\n\t\t\t\tevent: \"user.identified\",\n\t\t\t\texternalUserId: userId,\n\t\t\t\tproperties,\n\t\t\t});\n\t\t},\n\t\tflush: () => transport.flush(),\n\t\tshutdown: (shutdownOptions) => transport.shutdown(shutdownOptions),\n\t};\n}\n\nfunction splitEndpoint(endpoint: string): {\n\tapiUrl: string;\n\tendpointPath: string;\n} {\n\tconst url = new URL(endpoint);\n\treturn { apiUrl: url.origin, endpointPath: `${url.pathname}${url.search}` };\n}\n"],"mappings":"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","names":["useToolInfo","useEffect","useMemo","useRef","useState","pickFirst","meta","keys","key","value","SESSION_ID_KEYS","REQUEST_ID_KEYS","TRACE_ID_KEYS","EXTERNAL_USER_ID_KEYS","VISITOR_ID_KEYS","CORRELATION_ID_KEYS","WIDGET_CONFIG_META_KEY","extractSessionId","meta","pickFirst","SESSION_ID_KEYS","extractRequestId","REQUEST_ID_KEYS","extractTraceId","TRACE_ID_KEYS","extractExternalUserId","EXTERNAL_USER_ID_KEYS","extractVisitorId","VISITOR_ID_KEYS","extractCorrelationId","CORRELATION_ID_KEYS","SOURCE_SESSION_KEYS","CLIENT_INFO_NAME_SOURCES","extractSource","meta","clientInfo","explicit","key","source","value","name","lower","needle","DEFAULT_SOURCE","mapTrackEventToV2","input","options","now","generateId","createEventId","eventName","resolveEventName","meta","toRecord","metadata","correlation","resolveCorrelationIds","eventId","takeNonEmptyString","timestamp","normalizeTimestamp","source","extractSource","rawLegacy","isLegacyTrackEvent","mappedMetadata","mapProperties","legacyProperties","mapLegacyProperties","explicitProperties","properties","requestId","extractRequestId","sessionId","extractSessionId","traceId","extractTraceId","externalUserId","extractExternalUserId","visitorId","extractVisitorId","correlationId","extractCorrelationId","date","value","createRevenueApi","emit","amount","currency","itemId","label","context","options","id","input","externalId","email","name","occurredAt","DEFAULT_ENDPOINT_PATH","SDK_NAME","AUTH_FAILURE_STATUS","RETRYABLE_STATUS","createV2BatchTransport","options","BatchingV2Transport","joinUrl","DEFAULT_ENDPOINT_PATH","delayMs","resolve","event","dropCount","timeoutMs","flushPromise","timeoutSignal","teardownFlush","events","body","mid","headers","SDK_NAME","batch","attempt","pendingBatch","result","response","error","getErrorMessage","data","parseJsonResponse","partial","rejected","byId","retryable","permanent","rejectedEvent","isRetryableRejectedEvent","rawDelay","status","rejectedCount","buffered","code","apiUrl","endpointPath","normalizedBase","normalizedPath","createFrontendClient","options","apiUrl","endpointPath","splitEndpoint","transport","createV2BatchTransport","identifiedUserId","resolve","value","emit","event","identity","channelId","properties","mapped","mapTrackEventToV2","createRevenueApi","userId","shutdownOptions","endpoint","url","NOOP_EMIT","createNoopTrack","NOOP_WIDGET","state","consumerCount","normalizeString","value","trimmed","extractConfigFromMetadata","meta","nestedMeta","waniwani","WIDGET_CONFIG_META_KEY","endpoint","isSameConfig","a","b","createState","config","metadata","sessionId","traceId","client","createFrontendClient","userId","traits","useWaniwani","options","explicitEndpoint","explicitToken","explicitSessionId","explicitSource","suppliedConfig","ctxEndpoint","ctxToken","ctxSessionId","ctxSource","useMemo","source","widget","setWidget","useState","metadataRef","useRef","useEffect","current","useWaniwani","options","responseMetadata","useToolInfo"]}
@@ -661,7 +661,7 @@ interface KbSearchTrace {
661
661
  * first-class names at runtime (the widget transport passes these through
662
662
  * verbatim instead of namespacing them as custom widget events).
663
663
  */
664
- declare const EVENT_TYPES: readonly ["session.started", "page.viewed", "tool.called", "quote.requested", "quote.succeeded", "quote.failed", "link.clicked", "purchase.completed", "widget_render", "user.identified", "price_shown", "prices_compared", "option_selected", "lead_qualified", "converted"];
664
+ declare const EVENT_TYPES: readonly ["session.started", "page.viewed", "tool.called", "link.clicked", "widget_render", "user.identified", "price_shown", "prices_compared", "option_selected", "lead_qualified", "converted"];
665
665
  type EventType = (typeof EVENT_TYPES)[number];
666
666
  /**
667
667
  * Properties for `page.viewed` — emitted once when the chat widget initializes
@@ -690,17 +690,9 @@ interface ToolCalledProperties {
690
690
  /** Retrieval traces for kb.search() calls made inside this tool handler. */
691
691
  kbSearch?: KbSearchTrace[];
692
692
  }
693
- interface QuoteSucceededProperties {
694
- amount?: number;
695
- currency?: string;
696
- }
697
693
  interface LinkClickedProperties {
698
694
  url?: string;
699
695
  }
700
- interface PurchaseCompletedProperties {
701
- amount?: number;
702
- currency?: string;
703
- }
704
696
  interface PriceShownProperties {
705
697
  amount: number;
706
698
  currency: string;
@@ -787,19 +779,9 @@ type TrackEvent = ({
787
779
  } & BaseTrackEvent) | ({
788
780
  event: "tool.called";
789
781
  properties?: ToolCalledProperties;
790
- } & BaseTrackEvent) | ({
791
- event: "quote.requested";
792
- } & BaseTrackEvent) | ({
793
- event: "quote.succeeded";
794
- properties?: QuoteSucceededProperties;
795
- } & BaseTrackEvent) | ({
796
- event: "quote.failed";
797
782
  } & BaseTrackEvent) | ({
798
783
  event: "link.clicked";
799
784
  properties?: LinkClickedProperties;
800
- } & BaseTrackEvent) | ({
801
- event: "purchase.completed";
802
- properties?: PurchaseCompletedProperties;
803
785
  } & BaseTrackEvent) | ({
804
786
  event: "user.identified";
805
787
  } & BaseTrackEvent) | ({
@@ -828,11 +810,7 @@ interface LegacyTrackEvent extends TrackingContext {
828
810
  properties?: Record<string, unknown>;
829
811
  toolName?: string;
830
812
  toolType?: ToolCalledProperties["type"];
831
- quoteAmount?: number;
832
- quoteCurrency?: string;
833
813
  linkUrl?: string;
834
- purchaseAmount?: number;
835
- purchaseCurrency?: string;
836
814
  }
837
815
  /**
838
816
  * Public track input accepted by `client.track()`.
@@ -956,7 +934,7 @@ interface WaniwaniWidget {
956
934
  readonly sessionId?: string;
957
935
  /**
958
936
  * Track a typed event. The exact same surface as the server client:
959
- * `track({ event: "quote.succeeded", properties })`,
937
+ * `track({ event: "session.started" })`,
960
938
  * `track.priceShown({ amount, currency })`, `track.converted({ ... })`.
961
939
  * Identity (session, trace, user) is stamped automatically.
962
940
  */