@openattribution/telemetry 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -166,6 +166,49 @@ await ucpClient.completeCheckout({
166
166
  });
167
167
  ```
168
168
 
169
+ ## Tracking the full funnel
170
+
171
+ The four tracker methods cover the complete content attribution chain:
172
+
173
+ ```ts
174
+ // 1. Content fetched — what the agent retrieved to answer the query
175
+ await tracker.trackRetrieved(sessionId, productUrls);
176
+
177
+ // 2. Content cited — what the agent explicitly recommended
178
+ await tracker.trackCited(sessionId, citedUrls, { citationType: "reference" });
179
+
180
+ // 3. Content engaged — what the user clicked or viewed
181
+ await tracker.trackEngaged(sessionId, [clickedUrl], { interactionType: "click" });
182
+
183
+ // 4. Checkout — purchase completed (ends the session)
184
+ await tracker.trackCheckout(sessionId, { type: "completed", valueAmount: 4999, currency: "USD" });
185
+ ```
186
+
187
+ ### Click tracking with a redirect endpoint
188
+
189
+ The most reliable way to track clicks is a server-side redirect. Use `createTrackingUrl` to build proxied links, then implement a lightweight redirect handler:
190
+
191
+ ```ts
192
+ import { createTrackingUrl, MCPSessionTracker } from "@openattribution/telemetry";
193
+
194
+ // Build tracked links for your products
195
+ const trackedUrl = createTrackingUrl("https://shop.example.com/product/123", {
196
+ endpoint: "https://myagent.com/api/track",
197
+ sessionId: "conv-abc123",
198
+ });
199
+ // → "https://myagent.com/api/track?url=https%3A%2F%2F...&session_id=conv-abc123"
200
+
201
+ // Next.js redirect handler (app/api/track/route.ts)
202
+ export async function GET(req: Request) {
203
+ const { searchParams } = new URL(req.url);
204
+ const url = searchParams.get("url");
205
+ const sessionId = searchParams.get("session_id") ?? undefined;
206
+ if (!url) return new Response("Missing url", { status: 400 });
207
+ void tracker.trackEngaged(sessionId, [url], { interactionType: "click" });
208
+ return Response.redirect(url, 302);
209
+ }
210
+ ```
211
+
169
212
  ## API reference
170
213
 
171
214
  ### `TelemetryClient`
@@ -184,9 +227,15 @@ await ucpClient.completeCheckout({
184
227
  |--------|-------------|
185
228
  | `trackRetrieved(externalId, urls[])` | Emit `content_retrieved` events. Creates session if needed. |
186
229
  | `trackCited(externalId, urls[], options?)` | Emit `content_cited` events. |
187
- | `endSession(externalId, outcome)` | End the session and remove from registry. |
230
+ | `trackEngaged(externalId, urls[], options?)` | Emit `content_engaged` events (clicks, views). |
231
+ | `trackCheckout(externalId, outcome)` | Emit checkout event and end session. |
232
+ | `endSession(externalId, outcome)` | End session with explicit outcome. |
188
233
  | `getOrCreateSession(externalId)` | Get or create an OA session ID. |
189
234
 
235
+ ### `createTrackingUrl(contentUrl, options)`
236
+
237
+ Build a server-side redirect URL for reliable click tracking. Pass to your product links instead of the raw destination URL. See the redirect endpoint example above.
238
+
190
239
  ### `extractCitationUrls(text)`
191
240
 
192
241
  Extract HTTP/HTTPS URLs from Markdown-formatted text. Finds both `[anchor](url)` links and bare URLs. Returns deduplicated array.
package/dist/index.cjs CHANGED
@@ -22,6 +22,7 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  MCPSessionTracker: () => MCPSessionTracker,
24
24
  TelemetryClient: () => TelemetryClient,
25
+ createTrackingUrl: () => createTrackingUrl,
25
26
  extractCitationUrls: () => extractCitationUrls,
26
27
  extractResultUrls: () => extractResultUrls,
27
28
  sessionToAttribution: () => sessionToAttribution,
@@ -307,10 +308,89 @@ var MCPSessionTracker = class {
307
308
  await this.client.recordEvents(sessionId, events);
308
309
  }
309
310
  /**
310
- * End a session with an outcome.
311
+ * Emit `content_engaged` events when a user interacts with content.
312
+ *
313
+ * Call this when a user clicks a link, views an embedded product, or
314
+ * otherwise actively engages with retrieved content. This is the
315
+ * strongest attribution signal before a purchase event.
311
316
  *
312
- * Call this when the conversation concludes — at checkout, after
313
- * the user clicks a link, or when the session times out.
317
+ * @param externalSessionId - Caller-supplied conversation identifier.
318
+ * @param urls - URLs the user engaged with.
319
+ * @param options - Optional engagement metadata.
320
+ *
321
+ * @example
322
+ * ```ts
323
+ * // In a redirect/tracking endpoint
324
+ * await tracker.trackEngaged(sessionId, [productUrl], {
325
+ * interactionType: "click",
326
+ * });
327
+ * ```
328
+ */
329
+ async trackEngaged(externalSessionId, urls, options = {}) {
330
+ if (urls.length === 0) return;
331
+ const sessionId = await this.getOrCreateSession(externalSessionId);
332
+ if (sessionId == null) return;
333
+ const now = (/* @__PURE__ */ new Date()).toISOString();
334
+ const events = urls.map((url) => ({
335
+ id: crypto.randomUUID(),
336
+ type: "content_engaged",
337
+ timestamp: now,
338
+ contentUrl: url,
339
+ data: {
340
+ ...options.interactionType != null && {
341
+ interaction_type: options.interactionType
342
+ }
343
+ }
344
+ }));
345
+ await this.client.recordEvents(sessionId, events);
346
+ }
347
+ /**
348
+ * Record a checkout outcome and end the session.
349
+ *
350
+ * Call this when a user completes a purchase, abandons checkout, or
351
+ * the conversation concludes with a clear commerce outcome.
352
+ * Emits a `checkout_completed` or `checkout_abandoned` event then
353
+ * ends the session with the appropriate outcome type.
354
+ *
355
+ * @param externalSessionId - Caller-supplied conversation identifier.
356
+ * @param outcome - Purchase details.
357
+ *
358
+ * @example
359
+ * ```ts
360
+ * // User completed a purchase
361
+ * await tracker.trackCheckout(sessionId, {
362
+ * type: "completed",
363
+ * valueAmount: 4999, // $49.99 in minor units (cents)
364
+ * currency: "USD",
365
+ * });
366
+ *
367
+ * // User abandoned checkout
368
+ * await tracker.trackCheckout(sessionId, { type: "abandoned" });
369
+ * ```
370
+ */
371
+ async trackCheckout(externalSessionId, outcome) {
372
+ const sessionId = externalSessionId != null ? this.registry.get(externalSessionId) ?? null : null;
373
+ if (sessionId == null) return;
374
+ const eventType = outcome.type === "completed" ? "checkout_completed" : outcome.type === "abandoned" ? "checkout_abandoned" : "checkout_started";
375
+ await this.client.recordEvent(sessionId, eventType);
376
+ if (outcome.type === "completed" || outcome.type === "abandoned") {
377
+ const outcomeType = outcome.type === "completed" ? "conversion" : "abandonment";
378
+ await this.client.endSession(sessionId, {
379
+ type: outcomeType,
380
+ ...outcome.valueAmount != null && { valueAmount: outcome.valueAmount },
381
+ ...outcome.currency != null && { currency: outcome.currency },
382
+ ...outcome.products != null && { products: outcome.products }
383
+ });
384
+ if (externalSessionId != null) {
385
+ this.registry.delete(externalSessionId);
386
+ }
387
+ }
388
+ }
389
+ /**
390
+ * End a session with an explicit outcome.
391
+ *
392
+ * Use `trackCheckout` for commerce outcomes. Use this for non-commerce
393
+ * session endings (browse sessions, timeouts, explicit abandonment).
314
394
  */
315
395
  async endSession(externalSessionId, outcome) {
316
396
  const sessionId = externalSessionId != null ? this.registry.get(externalSessionId) ?? null : null;
@@ -352,6 +432,14 @@ function extractResultUrls(results) {
352
432
  }
353
433
  return [...new Set(urls)];
354
434
  }
435
+ function createTrackingUrl(contentUrl, options) {
436
+ const url = new URL(options.endpoint);
437
+ url.searchParams.set("url", contentUrl);
438
+ if (options.sessionId != null) {
439
+ url.searchParams.set("session_id", options.sessionId);
440
+ }
441
+ return url.toString();
442
+ }
355
443
  function cleanUrl(url) {
356
444
  return url.replace(/[.,;:!?]+$/, "");
357
445
  }
@@ -434,6 +522,7 @@ function sessionToAttribution(session) {
434
522
  0 && (module.exports = {
435
523
  MCPSessionTracker,
436
524
  TelemetryClient,
525
+ createTrackingUrl,
437
526
  extractCitationUrls,
438
527
  extractResultUrls,
439
528
  sessionToAttribution,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/client.ts","../src/mcp.ts","../src/extract.ts","../src/acp.ts","../src/ucp.ts"],"sourcesContent":["/**\n * @openattribution/telemetry\n *\n * OpenAttribution Telemetry SDK for TypeScript/JavaScript.\n * Track content attribution in AI agent interactions.\n *\n * Specification: https://openattribution.org/telemetry\n *\n * @example\n * ```ts\n * import { TelemetryClient, MCPSessionTracker, extractCitationUrls } from \"@openattribution/telemetry\";\n *\n * // Direct client usage\n * const client = new TelemetryClient({\n * endpoint: \"https://telemetry.example.com\",\n * apiKey: process.env.TELEMETRY_API_KEY,\n * failSilently: true,\n * });\n *\n * // MCP agent usage\n * const tracker = new MCPSessionTracker(client, \"my-shopping-agent\");\n * await tracker.trackRetrieved(sessionId, productUrls);\n *\n * // Extract citation URLs from AI response text\n * const urls = extractCitationUrls(assistantMessage);\n * await tracker.trackCited(sessionId, urls);\n * ```\n */\n\nexport { TelemetryClient } from \"./client.js\";\nexport { MCPSessionTracker } from \"./mcp.js\";\nexport { extractCitationUrls, extractResultUrls } from \"./extract.js\";\nexport { sessionToContentAttribution } from \"./acp.js\";\nexport { sessionToAttribution } from \"./ucp.js\";\n\nexport type {\n // Types\n TelemetryClientOptions,\n TelemetrySession,\n TelemetryEvent,\n SessionOutcome,\n StartSessionOptions,\n ConversationTurn,\n UserContext,\n Initiator,\n // Enumerations\n EventType,\n OutcomeType,\n PrivacyLevel,\n IntentCategory,\n InitiatorType,\n CitationType,\n CitationPosition,\n} from \"./types.js\";\n\nexport type { ContentAttribution, ContentAttributionRetrieved, ContentAttributionCited } from \"./acp.js\";\nexport type { UCPAttribution } from \"./ucp.js\";\n","/**\n * OpenAttribution Telemetry — HTTP client.\n *\n * Zero dependencies — uses native fetch (Node 18+, Deno, browsers, Edge).\n */\n\nimport type {\n ConversationTurn,\n EventType,\n Initiator,\n SessionOutcome,\n StartSessionOptions,\n TelemetryClientOptions,\n TelemetryEvent,\n TelemetrySession,\n UserContext,\n} from \"./types.js\";\n\nconst TRANSIENT_STATUS_CODES = new Set([429, 500, 502, 503, 504]);\n\n// ---------------------------------------------------------------------------\n// Wire format helpers (camelCase → snake_case for the JSON body)\n// ---------------------------------------------------------------------------\n\nfunction turnToWire(turn: ConversationTurn): Record<string, unknown> {\n return {\n privacy_level: turn.privacyLevel,\n query_text: turn.queryText,\n response_text: turn.responseText,\n query_intent: turn.queryIntent,\n response_type: turn.responseType,\n topics: turn.topics,\n content_urls_retrieved: turn.contentUrlsRetrieved,\n content_urls_cited: turn.contentUrlsCited,\n query_tokens: turn.queryTokens,\n response_tokens: turn.responseTokens,\n model_id: turn.modelId,\n };\n}\n\nfunction eventToWire(event: TelemetryEvent): Record<string, unknown> {\n return {\n id: event.id,\n type: event.type,\n timestamp: event.timestamp,\n content_url: event.contentUrl,\n product_id: event.productId,\n turn: event.turn != null ? turnToWire(event.turn) : undefined,\n data: event.data ?? {},\n };\n}\n\nfunction initiatorToWire(i: Initiator): Record<string, unknown> {\n return {\n agent_id: i.agentId,\n manifest_ref: i.manifestRef,\n operator_id: i.operatorId,\n };\n}\n\nfunction userContextToWire(uc: UserContext): Record<string, unknown> {\n return {\n external_id: uc.externalId,\n segments: uc.segments ?? [],\n attributes: uc.attributes ?? {},\n };\n}\n\nfunction outcomeToWire(o: SessionOutcome): Record<string, unknown> {\n return {\n type: o.type,\n value_amount: o.valueAmount ?? 0,\n currency: o.currency ?? \"USD\",\n products: o.products ?? [],\n metadata: o.metadata ?? {},\n };\n}\n\n// ---------------------------------------------------------------------------\n// TelemetryClient\n// ---------------------------------------------------------------------------\n\n/**\n * Async client for recording OpenAttribution telemetry.\n *\n * Works in Node.js ≥ 18, Deno, browsers, and Edge runtimes (Vercel, Cloudflare).\n *\n * @example\n * ```ts\n * const client = new TelemetryClient({\n * endpoint: \"https://telemetry.example.com\",\n * apiKey: \"your-api-key\",\n * failSilently: true,\n * });\n *\n * const sessionId = await client.startSession({ contentScope: \"my-mix\" });\n *\n * await client.recordEvents(sessionId, [\n * { id: crypto.randomUUID(), type: \"content_retrieved\",\n * timestamp: new Date().toISOString(), contentUrl: \"https://...\" }\n * ]);\n *\n * await client.endSession(sessionId, { type: \"browse\" });\n * ```\n */\nexport class TelemetryClient {\n private readonly endpoint: string;\n private readonly apiKey: string | undefined;\n private readonly failSilently: boolean;\n private readonly timeout: number;\n private readonly maxRetries: number;\n\n constructor(options: TelemetryClientOptions) {\n this.endpoint = options.endpoint.replace(/\\/$/, \"\");\n this.apiKey = options.apiKey;\n this.failSilently = options.failSilently ?? true;\n this.timeout = options.timeout ?? 30_000;\n this.maxRetries = options.maxRetries ?? 3;\n }\n\n private headers(): Record<string, string> {\n const h: Record<string, string> = { \"Content-Type\": \"application/json\" };\n if (this.apiKey != null) h[\"X-API-Key\"] = this.apiKey;\n return h;\n }\n\n private async post(path: string, body: unknown): Promise<unknown> {\n const url = `${this.endpoint}${path}`;\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= this.maxRetries; attempt++) {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const res = await fetch(url, {\n method: \"POST\",\n headers: this.headers(),\n body: JSON.stringify(body),\n signal: controller.signal,\n });\n\n if (TRANSIENT_STATUS_CODES.has(res.status) && attempt < this.maxRetries) {\n const wait = 2 ** attempt * 1000 + Math.random() * 500;\n await sleep(wait);\n continue;\n }\n\n if (!res.ok) {\n throw new Error(`HTTP ${res.status} ${res.statusText} from ${url}`);\n }\n\n return await res.json();\n } catch (err) {\n lastError = err;\n if (attempt < this.maxRetries && isTransientError(err)) {\n const wait = 2 ** attempt * 1000 + Math.random() * 500;\n await sleep(wait);\n } else {\n break;\n }\n } finally {\n clearTimeout(timer);\n }\n }\n\n if (this.failSilently) {\n return null;\n }\n throw lastError;\n }\n\n /**\n * Start a new telemetry session.\n *\n * @returns Session ID string, or null on silent failure.\n */\n async startSession(options: StartSessionOptions = {}): Promise<string | null> {\n const result = await this.post(\"/session/start\", {\n initiator_type: options.initiatorType ?? \"user\",\n initiator:\n options.initiator != null ? initiatorToWire(options.initiator) : null,\n content_scope: options.contentScope,\n agent_id: options.agentId,\n external_session_id: options.externalSessionId,\n user_context:\n options.userContext != null\n ? userContextToWire(options.userContext)\n : {},\n manifest_ref: options.manifestRef,\n prior_session_ids: options.priorSessionIds ?? [],\n }) as { session_id?: string } | null;\n\n return result?.session_id ?? null;\n }\n\n /**\n * Record a single telemetry event.\n */\n async recordEvent(\n sessionId: string | null,\n eventType: EventType,\n options: {\n contentUrl?: string;\n productId?: string;\n turn?: ConversationTurn;\n data?: Record<string, unknown>;\n } = {},\n ): Promise<void> {\n if (sessionId == null) return;\n await this.recordEvents(sessionId, [\n {\n id: crypto.randomUUID(),\n type: eventType,\n timestamp: new Date().toISOString(),\n ...options,\n },\n ]);\n }\n\n /**\n * Record a batch of telemetry events.\n */\n async recordEvents(\n sessionId: string | null,\n events: TelemetryEvent[],\n ): Promise<void> {\n if (sessionId == null || events.length === 0) return;\n await this.post(\"/events\", {\n session_id: sessionId,\n events: events.map(eventToWire),\n });\n }\n\n /**\n * End a session with an outcome.\n */\n async endSession(\n sessionId: string | null,\n outcome: SessionOutcome,\n ): Promise<void> {\n if (sessionId == null) return;\n await this.post(\"/session/end\", {\n session_id: sessionId,\n outcome: outcomeToWire(outcome),\n });\n }\n\n /**\n * Upload a complete session in one request (bulk path).\n *\n * Useful for post-hoc reporting or when you've built the session\n * locally and want to submit it in one shot.\n *\n * @returns Server-assigned session ID, or null on silent failure.\n */\n async uploadSession(session: TelemetrySession): Promise<string | null> {\n const result = await this.post(\"/session/bulk\", sessionToWire(session)) as\n | { session_id?: string }\n | null;\n return result?.session_id ?? null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction isTransientError(err: unknown): boolean {\n if (err instanceof Error) {\n // AbortError (timeout), network errors\n return (\n err.name === \"AbortError\" ||\n err.name === \"TypeError\" ||\n err.message.includes(\"fetch\")\n );\n }\n return false;\n}\n\nfunction sessionToWire(session: TelemetrySession): Record<string, unknown> {\n return {\n schema_version: session.schemaVersion ?? \"0.4\",\n session_id: session.sessionId,\n initiator_type: session.initiatorType ?? \"user\",\n initiator:\n session.initiator != null ? initiatorToWire(session.initiator) : null,\n agent_id: session.agentId,\n content_scope: session.contentScope,\n manifest_ref: session.manifestRef,\n prior_session_ids: session.priorSessionIds ?? [],\n started_at: session.startedAt,\n ended_at: session.endedAt,\n user_context:\n session.userContext != null\n ? userContextToWire(session.userContext)\n : {},\n events: session.events.map(eventToWire),\n outcome:\n session.outcome != null ? outcomeToWire(session.outcome) : undefined,\n };\n}\n","/**\n * OpenAttribution Telemetry — MCP session tracker.\n *\n * Provides session continuity across stateless MCP tool calls.\n * The calling agent passes a stable `sessionId` string; this module\n * maps it to an OA session UUID and reuses it across tool calls within\n * the same server process.\n *\n * Usage in an MCP tool:\n *\n * ```ts\n * import { TelemetryClient, MCPSessionTracker } from \"@openattribution/telemetry\";\n *\n * const client = new TelemetryClient({ endpoint: \"...\", apiKey: \"...\" });\n * const tracker = new MCPSessionTracker(client, \"my-agent\");\n *\n * // In your MCP tool handler:\n * server.tool(\"search_products\", { query: z.string(), sessionId: z.string().optional() },\n * async ({ query, sessionId }) => {\n * const results = await searchProducts(query);\n * await tracker.trackRetrieved(sessionId, results.map(r => r.url));\n * return formatResults(results);\n * }\n * );\n * ```\n */\n\nimport type { TelemetryEvent } from \"./types.js\";\nimport type { TelemetryClient } from \"./client.js\";\n\n/**\n * Session tracker for MCP agents.\n *\n * Maintains an in-process mapping of external session IDs to OA session UUIDs.\n * Sessions are created on first use and reused across subsequent tool calls.\n *\n * The in-process registry is intentionally simple — it works correctly for\n * single-process MCP servers. For distributed deployments, pass a shared\n * external store to the constructor.\n */\nexport class MCPSessionTracker {\n private readonly client: TelemetryClient;\n private readonly contentScope: string;\n private readonly registry = new Map<string, string | null>();\n\n /**\n * @param client - Configured TelemetryClient instance.\n * @param contentScope - Stable identifier for this agent's content scope\n * (e.g. mix ID, manifest reference, or descriptive slug like \"my-shopping-agent\").\n */\n constructor(client: TelemetryClient, contentScope = \"mcp-agent\") {\n this.client = client;\n this.contentScope = contentScope;\n }\n\n /**\n * Get or create an OA session for the given external session ID.\n *\n * If `externalSessionId` is undefined, a new anonymous session is created\n * on every call (no continuity across tool calls).\n *\n * @returns OA session ID string, or null on silent failure.\n */\n async getOrCreateSession(\n externalSessionId: string | undefined,\n ): Promise<string | null> {\n if (externalSessionId != null && this.registry.has(externalSessionId)) {\n return this.registry.get(externalSessionId) ?? null;\n }\n\n const sessionId = await this.client.startSession({\n contentScope: this.contentScope,\n ...(externalSessionId != null && { externalSessionId }),\n });\n\n if (externalSessionId != null) {\n this.registry.set(externalSessionId, sessionId);\n }\n\n return sessionId;\n }\n\n /**\n * Emit `content_retrieved` events for a list of URLs.\n *\n * Call this after fetching products, search results, or any content\n * that influenced the agent's response.\n *\n * @param externalSessionId - Caller-supplied conversation identifier.\n * @param urls - URLs of content retrieved during this tool call.\n */\n async trackRetrieved(\n externalSessionId: string | undefined,\n urls: string[],\n ): Promise<void> {\n if (urls.length === 0) return;\n const sessionId = await this.getOrCreateSession(externalSessionId);\n if (sessionId == null) return;\n\n const now = new Date().toISOString();\n const events: TelemetryEvent[] = urls.map((url) => ({\n id: crypto.randomUUID(),\n type: \"content_retrieved\" as const,\n timestamp: now,\n contentUrl: url,\n }));\n\n await this.client.recordEvents(sessionId, events);\n }\n\n /**\n * Emit `content_cited` events for content explicitly referenced in a response.\n *\n * Call this when you know which content the agent cited — e.g. the top\n * search result, an editorial quote, or a product recommendation.\n *\n * @param externalSessionId - Caller-supplied conversation identifier.\n * @param urls - URLs of content cited in the agent's response.\n * @param options - Optional citation metadata.\n */\n async trackCited(\n externalSessionId: string | undefined,\n urls: string[],\n options: {\n citationType?: \"direct_quote\" | \"paraphrase\" | \"reference\" | \"contradiction\";\n position?: \"primary\" | \"supporting\" | \"mentioned\";\n } = {},\n ): Promise<void> {\n if (urls.length === 0) return;\n const sessionId = await this.getOrCreateSession(externalSessionId);\n if (sessionId == null) return;\n\n const now = new Date().toISOString();\n const events: TelemetryEvent[] = urls.map((url) => ({\n id: crypto.randomUUID(),\n type: \"content_cited\" as const,\n timestamp: now,\n contentUrl: url,\n data: {\n ...(options.citationType != null && { citation_type: options.citationType }),\n ...(options.position != null && { position: options.position }),\n },\n }));\n\n await this.client.recordEvents(sessionId, events);\n }\n\n /**\n * End a session with an outcome.\n *\n * Call this when the conversation concludes — at checkout, after\n * the user clicks a link, or when the session times out.\n */\n async endSession(\n externalSessionId: string | undefined,\n outcome: { type: \"conversion\" | \"abandonment\" | \"browse\"; valueAmount?: number; currency?: string },\n ): Promise<void> {\n const sessionId = externalSessionId != null\n ? (this.registry.get(externalSessionId) ?? null)\n : null;\n\n if (sessionId == null) return;\n\n await this.client.endSession(sessionId, {\n type: outcome.type,\n ...(outcome.valueAmount != null && { valueAmount: outcome.valueAmount }),\n ...(outcome.currency != null && { currency: outcome.currency }),\n });\n\n this.registry.delete(externalSessionId!);\n }\n\n /** Number of active sessions in the registry. */\n get sessionCount(): number {\n return this.registry.size;\n }\n}\n","/**\n * OpenAttribution Telemetry — content URL extraction utilities.\n *\n * Helpers for extracting content URLs from AI-generated text, so they\n * can be recorded as telemetry events without manual instrumentation.\n */\n\n/** Matches `[text](url)` — standard Markdown links. */\nconst MARKDOWN_LINK_RE = /\\[([^\\]]+)\\]\\((https?:\\/\\/[^)]+)\\)/g;\n\n/** Matches bare URLs starting with http/https. */\nconst BARE_URL_RE = /https?:\\/\\/[^\\s<>\"')\\]]+/g;\n\n/**\n * Extract all HTTP/HTTPS URLs from Markdown-formatted text.\n *\n * Finds URLs in two forms:\n * - Markdown links: `[anchor text](https://...)`\n * - Bare URLs: `https://...`\n *\n * Results are deduplicated. Useful for extracting citation URLs from\n * AI model responses that include web search results.\n *\n * @example\n * ```ts\n * const text = \"According to [Wirecutter](https://nytimes.com/wirecutter/reviews/...), ...\";\n * const urls = extractCitationUrls(text);\n * // [\"https://nytimes.com/wirecutter/reviews/...\"]\n * ```\n */\nexport function extractCitationUrls(text: string): string[] {\n const urls = new Set<string>();\n\n for (const match of text.matchAll(MARKDOWN_LINK_RE)) {\n const url = match[2];\n if (url != null) urls.add(cleanUrl(url));\n }\n\n // Only look for bare URLs if Markdown links didn't cover everything\n for (const match of text.matchAll(BARE_URL_RE)) {\n urls.add(cleanUrl(match[0]));\n }\n\n return [...urls];\n}\n\n/**\n * Extract URLs from a list of search result objects.\n *\n * Accepts any object with a `url` string property — works with\n * Channel3, Exa, Tavily, and most search API responses.\n *\n * @example\n * ```ts\n * const products = await channel3.search({ query: \"headphones\" });\n * const urls = extractResultUrls(products);\n * await tracker.trackRetrieved(sessionId, urls);\n * ```\n */\nexport function extractResultUrls(\n results: Array<{ url?: string | null; link?: string | null }>,\n): string[] {\n const urls: string[] = [];\n for (const r of results) {\n const url = r.url ?? r.link;\n if (url != null && url.startsWith(\"http\")) {\n urls.push(url);\n }\n }\n return [...new Set(urls)];\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/** Remove trailing punctuation that commonly attaches to bare URLs. */\nfunction cleanUrl(url: string): string {\n return url.replace(/[.,;:!?]+$/, \"\");\n}\n","/**\n * OpenAttribution Telemetry — ACP (Agentic Commerce Protocol) bridge.\n *\n * Converts a TelemetrySession into the `content_attribution` object\n * defined in the ACP RFC. Include this in a CheckoutSessionCreateRequest\n * or CheckoutSessionCompleteRequest.\n *\n * Reference: acp/rfc.content_attribution.md\n */\n\nimport type { TelemetrySession } from \"./types.js\";\n\nexport interface ContentAttributionRetrieved {\n content_url: string;\n timestamp: string;\n}\n\nexport interface ContentAttributionCited {\n content_url: string;\n timestamp: string;\n citation_type?: string;\n excerpt_tokens?: number;\n position?: string;\n content_hash?: string;\n}\n\nexport interface ConversationSummary {\n turn_count: number;\n topics: string[];\n}\n\n/**\n * ACP `content_attribution` object.\n * Include in CheckoutSessionCreateRequest.content_attribution.\n */\nexport interface ContentAttribution {\n content_scope?: string;\n content_retrieved: ContentAttributionRetrieved[];\n content_cited?: ContentAttributionCited[];\n conversation_summary?: ConversationSummary;\n}\n\n/**\n * Convert a TelemetrySession into an ACP `content_attribution` object.\n *\n * @example\n * ```ts\n * import { sessionToContentAttribution } from \"@openattribution/telemetry/acp\";\n *\n * const attribution = sessionToContentAttribution(session);\n *\n * // Include in ACP checkout request:\n * await acp.createCheckout({\n * cart: { ... },\n * content_attribution: attribution,\n * });\n * ```\n */\nexport function sessionToContentAttribution(\n session: TelemetrySession,\n): ContentAttribution {\n const retrieved: ContentAttributionRetrieved[] = session.events\n .filter((e) => e.type === \"content_retrieved\" && e.contentUrl != null)\n .map((e) => ({\n content_url: e.contentUrl!,\n timestamp: e.timestamp,\n }));\n\n const cited: ContentAttributionCited[] = session.events\n .filter((e) => e.type === \"content_cited\" && e.contentUrl != null)\n .map((e) => ({\n content_url: e.contentUrl!,\n timestamp: e.timestamp,\n ...(e.data?.[\"citation_type\"] != null && {\n citation_type: String(e.data[\"citation_type\"]),\n }),\n ...(e.data?.[\"excerpt_tokens\"] != null && {\n excerpt_tokens: Number(e.data[\"excerpt_tokens\"]),\n }),\n ...(e.data?.[\"position\"] != null && {\n position: String(e.data[\"position\"]),\n }),\n ...(e.data?.[\"content_hash\"] != null && {\n content_hash: String(e.data[\"content_hash\"]),\n }),\n }));\n\n const turnEvents = session.events.filter(\n (e) => e.type === \"turn_completed\" || e.type === \"turn_started\",\n );\n const turnCount = Math.max(\n session.events.filter((e) => e.type === \"turn_completed\").length,\n 1,\n );\n\n const topics = [\n ...new Set(\n session.events.flatMap((e) => e.turn?.topics ?? []),\n ),\n ];\n\n const result: ContentAttribution = {\n ...(session.contentScope != null && { content_scope: session.contentScope }),\n content_retrieved: retrieved,\n ...(cited.length > 0 && { content_cited: cited }),\n ...((turnEvents.length > 0 || topics.length > 0) && {\n conversation_summary: { turn_count: turnCount, topics },\n }),\n };\n\n return result;\n}\n","/**\n * OpenAttribution Telemetry — UCP (Universal Checkout Protocol) bridge.\n *\n * Converts a TelemetrySession into the UCP attribution extension object.\n *\n * Reference: ucp/EXTENSION.md\n */\n\nimport type { TelemetrySession } from \"./types.js\";\n\nexport interface UCPAttribution {\n content_scope?: string;\n prior_session_ids?: string[];\n content_retrieved: Array<{ content_url: string; timestamp: string }>;\n content_cited?: Array<{\n content_url: string;\n timestamp: string;\n citation_type?: string;\n position?: string;\n }>;\n conversation_summary?: {\n turn_count: number;\n topics: string[];\n };\n}\n\n/**\n * Convert a TelemetrySession into a UCP attribution extension object.\n *\n * @example\n * ```ts\n * import { sessionToAttribution } from \"@openattribution/telemetry/ucp\";\n *\n * const attribution = sessionToAttribution(session);\n *\n * // Include in UCP checkout:\n * await ucp.completeCheckout({\n * order: { ... },\n * extensions: { \"org.openattribution.telemetry\": attribution },\n * });\n * ```\n */\nexport function sessionToAttribution(session: TelemetrySession): UCPAttribution {\n const retrieved = session.events\n .filter((e) => e.type === \"content_retrieved\" && e.contentUrl != null)\n .map((e) => ({ content_url: e.contentUrl!, timestamp: e.timestamp }));\n\n const cited = session.events\n .filter((e) => e.type === \"content_cited\" && e.contentUrl != null)\n .map((e) => ({\n content_url: e.contentUrl!,\n timestamp: e.timestamp,\n ...(e.data?.[\"citation_type\"] != null && {\n citation_type: String(e.data[\"citation_type\"]),\n }),\n ...(e.data?.[\"position\"] != null && {\n position: String(e.data[\"position\"]),\n }),\n }));\n\n const turnCount = Math.max(\n session.events.filter((e) => e.type === \"turn_completed\").length,\n 1,\n );\n const topics = [...new Set(session.events.flatMap((e) => e.turn?.topics ?? []))];\n const priorIds = (session.priorSessionIds ?? []).filter(Boolean);\n\n return {\n ...(session.contentScope != null && { content_scope: session.contentScope }),\n ...(priorIds.length > 0 && { prior_session_ids: priorIds }),\n content_retrieved: retrieved,\n ...(cited.length > 0 && { content_cited: cited }),\n ...((topics.length > 0 || turnCount > 0) && {\n conversation_summary: { turn_count: turnCount, topics },\n }),\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/client.ts","../src/mcp.ts","../src/extract.ts","../src/acp.ts","../src/ucp.ts"],"sourcesContent":["/**\n * @openattribution/telemetry\n *\n * OpenAttribution Telemetry SDK for TypeScript/JavaScript.\n * Track content attribution in AI agent interactions.\n *\n * Specification: https://openattribution.org/telemetry\n *\n * @example\n * ```ts\n * import { TelemetryClient, MCPSessionTracker, extractCitationUrls } from \"@openattribution/telemetry\";\n *\n * // Direct client usage\n * const client = new TelemetryClient({\n * endpoint: \"https://telemetry.example.com\",\n * apiKey: process.env.TELEMETRY_API_KEY,\n * failSilently: true,\n * });\n *\n * // MCP agent usage\n * const tracker = new MCPSessionTracker(client, \"my-shopping-agent\");\n * await tracker.trackRetrieved(sessionId, productUrls);\n *\n * // Extract citation URLs from AI response text\n * const urls = extractCitationUrls(assistantMessage);\n * await tracker.trackCited(sessionId, urls);\n * ```\n */\n\nexport { TelemetryClient } from \"./client.js\";\nexport { MCPSessionTracker } from \"./mcp.js\";\nexport { extractCitationUrls, extractResultUrls, createTrackingUrl } from \"./extract.js\";\nexport { sessionToContentAttribution } from \"./acp.js\";\nexport { sessionToAttribution } from \"./ucp.js\";\n\nexport type {\n // Types\n TelemetryClientOptions,\n TelemetrySession,\n TelemetryEvent,\n SessionOutcome,\n StartSessionOptions,\n ConversationTurn,\n UserContext,\n Initiator,\n // Enumerations\n EventType,\n OutcomeType,\n PrivacyLevel,\n IntentCategory,\n InitiatorType,\n CitationType,\n CitationPosition,\n} from \"./types.js\";\n\nexport type { ContentAttribution, ContentAttributionRetrieved, ContentAttributionCited } from \"./acp.js\";\nexport type { UCPAttribution } from \"./ucp.js\";\n","/**\n * OpenAttribution Telemetry — HTTP client.\n *\n * Zero dependencies — uses native fetch (Node 18+, Deno, browsers, Edge).\n */\n\nimport type {\n ConversationTurn,\n EventType,\n Initiator,\n SessionOutcome,\n StartSessionOptions,\n TelemetryClientOptions,\n TelemetryEvent,\n TelemetrySession,\n UserContext,\n} from \"./types.js\";\n\nconst TRANSIENT_STATUS_CODES = new Set([429, 500, 502, 503, 504]);\n\n// ---------------------------------------------------------------------------\n// Wire format helpers (camelCase → snake_case for the JSON body)\n// ---------------------------------------------------------------------------\n\nfunction turnToWire(turn: ConversationTurn): Record<string, unknown> {\n return {\n privacy_level: turn.privacyLevel,\n query_text: turn.queryText,\n response_text: turn.responseText,\n query_intent: turn.queryIntent,\n response_type: turn.responseType,\n topics: turn.topics,\n content_urls_retrieved: turn.contentUrlsRetrieved,\n content_urls_cited: turn.contentUrlsCited,\n query_tokens: turn.queryTokens,\n response_tokens: turn.responseTokens,\n model_id: turn.modelId,\n };\n}\n\nfunction eventToWire(event: TelemetryEvent): Record<string, unknown> {\n return {\n id: event.id,\n type: event.type,\n timestamp: event.timestamp,\n content_url: event.contentUrl,\n product_id: event.productId,\n turn: event.turn != null ? turnToWire(event.turn) : undefined,\n data: event.data ?? {},\n };\n}\n\nfunction initiatorToWire(i: Initiator): Record<string, unknown> {\n return {\n agent_id: i.agentId,\n manifest_ref: i.manifestRef,\n operator_id: i.operatorId,\n };\n}\n\nfunction userContextToWire(uc: UserContext): Record<string, unknown> {\n return {\n external_id: uc.externalId,\n segments: uc.segments ?? [],\n attributes: uc.attributes ?? {},\n };\n}\n\nfunction outcomeToWire(o: SessionOutcome): Record<string, unknown> {\n return {\n type: o.type,\n value_amount: o.valueAmount ?? 0,\n currency: o.currency ?? \"USD\",\n products: o.products ?? [],\n metadata: o.metadata ?? {},\n };\n}\n\n// ---------------------------------------------------------------------------\n// TelemetryClient\n// ---------------------------------------------------------------------------\n\n/**\n * Async client for recording OpenAttribution telemetry.\n *\n * Works in Node.js ≥ 18, Deno, browsers, and Edge runtimes (Vercel, Cloudflare).\n *\n * @example\n * ```ts\n * const client = new TelemetryClient({\n * endpoint: \"https://telemetry.example.com\",\n * apiKey: \"your-api-key\",\n * failSilently: true,\n * });\n *\n * const sessionId = await client.startSession({ contentScope: \"my-mix\" });\n *\n * await client.recordEvents(sessionId, [\n * { id: crypto.randomUUID(), type: \"content_retrieved\",\n * timestamp: new Date().toISOString(), contentUrl: \"https://...\" }\n * ]);\n *\n * await client.endSession(sessionId, { type: \"browse\" });\n * ```\n */\nexport class TelemetryClient {\n private readonly endpoint: string;\n private readonly apiKey: string | undefined;\n private readonly failSilently: boolean;\n private readonly timeout: number;\n private readonly maxRetries: number;\n\n constructor(options: TelemetryClientOptions) {\n this.endpoint = options.endpoint.replace(/\\/$/, \"\");\n this.apiKey = options.apiKey;\n this.failSilently = options.failSilently ?? true;\n this.timeout = options.timeout ?? 30_000;\n this.maxRetries = options.maxRetries ?? 3;\n }\n\n private headers(): Record<string, string> {\n const h: Record<string, string> = { \"Content-Type\": \"application/json\" };\n if (this.apiKey != null) h[\"X-API-Key\"] = this.apiKey;\n return h;\n }\n\n private async post(path: string, body: unknown): Promise<unknown> {\n const url = `${this.endpoint}${path}`;\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= this.maxRetries; attempt++) {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const res = await fetch(url, {\n method: \"POST\",\n headers: this.headers(),\n body: JSON.stringify(body),\n signal: controller.signal,\n });\n\n if (TRANSIENT_STATUS_CODES.has(res.status) && attempt < this.maxRetries) {\n const wait = 2 ** attempt * 1000 + Math.random() * 500;\n await sleep(wait);\n continue;\n }\n\n if (!res.ok) {\n throw new Error(`HTTP ${res.status} ${res.statusText} from ${url}`);\n }\n\n return await res.json();\n } catch (err) {\n lastError = err;\n if (attempt < this.maxRetries && isTransientError(err)) {\n const wait = 2 ** attempt * 1000 + Math.random() * 500;\n await sleep(wait);\n } else {\n break;\n }\n } finally {\n clearTimeout(timer);\n }\n }\n\n if (this.failSilently) {\n return null;\n }\n throw lastError;\n }\n\n /**\n * Start a new telemetry session.\n *\n * @returns Session ID string, or null on silent failure.\n */\n async startSession(options: StartSessionOptions = {}): Promise<string | null> {\n const result = await this.post(\"/session/start\", {\n initiator_type: options.initiatorType ?? \"user\",\n initiator:\n options.initiator != null ? initiatorToWire(options.initiator) : null,\n content_scope: options.contentScope,\n agent_id: options.agentId,\n external_session_id: options.externalSessionId,\n user_context:\n options.userContext != null\n ? userContextToWire(options.userContext)\n : {},\n manifest_ref: options.manifestRef,\n prior_session_ids: options.priorSessionIds ?? [],\n }) as { session_id?: string } | null;\n\n return result?.session_id ?? null;\n }\n\n /**\n * Record a single telemetry event.\n */\n async recordEvent(\n sessionId: string | null,\n eventType: EventType,\n options: {\n contentUrl?: string;\n productId?: string;\n turn?: ConversationTurn;\n data?: Record<string, unknown>;\n } = {},\n ): Promise<void> {\n if (sessionId == null) return;\n await this.recordEvents(sessionId, [\n {\n id: crypto.randomUUID(),\n type: eventType,\n timestamp: new Date().toISOString(),\n ...options,\n },\n ]);\n }\n\n /**\n * Record a batch of telemetry events.\n */\n async recordEvents(\n sessionId: string | null,\n events: TelemetryEvent[],\n ): Promise<void> {\n if (sessionId == null || events.length === 0) return;\n await this.post(\"/events\", {\n session_id: sessionId,\n events: events.map(eventToWire),\n });\n }\n\n /**\n * End a session with an outcome.\n */\n async endSession(\n sessionId: string | null,\n outcome: SessionOutcome,\n ): Promise<void> {\n if (sessionId == null) return;\n await this.post(\"/session/end\", {\n session_id: sessionId,\n outcome: outcomeToWire(outcome),\n });\n }\n\n /**\n * Upload a complete session in one request (bulk path).\n *\n * Useful for post-hoc reporting or when you've built the session\n * locally and want to submit it in one shot.\n *\n * @returns Server-assigned session ID, or null on silent failure.\n */\n async uploadSession(session: TelemetrySession): Promise<string | null> {\n const result = await this.post(\"/session/bulk\", sessionToWire(session)) as\n | { session_id?: string }\n | null;\n return result?.session_id ?? null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction isTransientError(err: unknown): boolean {\n if (err instanceof Error) {\n // AbortError (timeout), network errors\n return (\n err.name === \"AbortError\" ||\n err.name === \"TypeError\" ||\n err.message.includes(\"fetch\")\n );\n }\n return false;\n}\n\nfunction sessionToWire(session: TelemetrySession): Record<string, unknown> {\n return {\n schema_version: session.schemaVersion ?? \"0.4\",\n session_id: session.sessionId,\n initiator_type: session.initiatorType ?? \"user\",\n initiator:\n session.initiator != null ? initiatorToWire(session.initiator) : null,\n agent_id: session.agentId,\n content_scope: session.contentScope,\n manifest_ref: session.manifestRef,\n prior_session_ids: session.priorSessionIds ?? [],\n started_at: session.startedAt,\n ended_at: session.endedAt,\n user_context:\n session.userContext != null\n ? userContextToWire(session.userContext)\n : {},\n events: session.events.map(eventToWire),\n outcome:\n session.outcome != null ? outcomeToWire(session.outcome) : undefined,\n };\n}\n","/**\n * OpenAttribution Telemetry — MCP session tracker.\n *\n * Provides session continuity across stateless MCP tool calls.\n * The calling agent passes a stable `sessionId` string; this module\n * maps it to an OA session UUID and reuses it across tool calls within\n * the same server process.\n *\n * Usage in an MCP tool:\n *\n * ```ts\n * import { TelemetryClient, MCPSessionTracker } from \"@openattribution/telemetry\";\n *\n * const client = new TelemetryClient({ endpoint: \"...\", apiKey: \"...\" });\n * const tracker = new MCPSessionTracker(client, \"my-agent\");\n *\n * // In your MCP tool handler:\n * server.tool(\"search_products\", { query: z.string(), sessionId: z.string().optional() },\n * async ({ query, sessionId }) => {\n * const results = await searchProducts(query);\n * await tracker.trackRetrieved(sessionId, results.map(r => r.url));\n * return formatResults(results);\n * }\n * );\n * ```\n */\n\nimport type { TelemetryEvent } from \"./types.js\";\nimport type { TelemetryClient } from \"./client.js\";\n\n/**\n * Session tracker for MCP agents.\n *\n * Maintains an in-process mapping of external session IDs to OA session UUIDs.\n * Sessions are created on first use and reused across subsequent tool calls.\n *\n * The in-process registry is intentionally simple — it works correctly for\n * single-process MCP servers. For distributed deployments, pass a shared\n * external store to the constructor.\n */\nexport class MCPSessionTracker {\n private readonly client: TelemetryClient;\n private readonly contentScope: string;\n private readonly registry = new Map<string, string | null>();\n\n /**\n * @param client - Configured TelemetryClient instance.\n * @param contentScope - Stable identifier for this agent's content scope\n * (e.g. mix ID, manifest reference, or descriptive slug like \"my-shopping-agent\").\n */\n constructor(client: TelemetryClient, contentScope = \"mcp-agent\") {\n this.client = client;\n this.contentScope = contentScope;\n }\n\n /**\n * Get or create an OA session for the given external session ID.\n *\n * If `externalSessionId` is undefined, a new anonymous session is created\n * on every call (no continuity across tool calls).\n *\n * @returns OA session ID string, or null on silent failure.\n */\n async getOrCreateSession(\n externalSessionId: string | undefined,\n ): Promise<string | null> {\n if (externalSessionId != null && this.registry.has(externalSessionId)) {\n return this.registry.get(externalSessionId) ?? null;\n }\n\n const sessionId = await this.client.startSession({\n contentScope: this.contentScope,\n ...(externalSessionId != null && { externalSessionId }),\n });\n\n if (externalSessionId != null) {\n this.registry.set(externalSessionId, sessionId);\n }\n\n return sessionId;\n }\n\n /**\n * Emit `content_retrieved` events for a list of URLs.\n *\n * Call this after fetching products, search results, or any content\n * that influenced the agent's response.\n *\n * @param externalSessionId - Caller-supplied conversation identifier.\n * @param urls - URLs of content retrieved during this tool call.\n */\n async trackRetrieved(\n externalSessionId: string | undefined,\n urls: string[],\n ): Promise<void> {\n if (urls.length === 0) return;\n const sessionId = await this.getOrCreateSession(externalSessionId);\n if (sessionId == null) return;\n\n const now = new Date().toISOString();\n const events: TelemetryEvent[] = urls.map((url) => ({\n id: crypto.randomUUID(),\n type: \"content_retrieved\" as const,\n timestamp: now,\n contentUrl: url,\n }));\n\n await this.client.recordEvents(sessionId, events);\n }\n\n /**\n * Emit `content_cited` events for content explicitly referenced in a response.\n *\n * Call this when you know which content the agent cited — e.g. the top\n * search result, an editorial quote, or a product recommendation.\n *\n * @param externalSessionId - Caller-supplied conversation identifier.\n * @param urls - URLs of content cited in the agent's response.\n * @param options - Optional citation metadata.\n */\n async trackCited(\n externalSessionId: string | undefined,\n urls: string[],\n options: {\n citationType?: \"direct_quote\" | \"paraphrase\" | \"reference\" | \"contradiction\";\n position?: \"primary\" | \"supporting\" | \"mentioned\";\n } = {},\n ): Promise<void> {\n if (urls.length === 0) return;\n const sessionId = await this.getOrCreateSession(externalSessionId);\n if (sessionId == null) return;\n\n const now = new Date().toISOString();\n const events: TelemetryEvent[] = urls.map((url) => ({\n id: crypto.randomUUID(),\n type: \"content_cited\" as const,\n timestamp: now,\n contentUrl: url,\n data: {\n ...(options.citationType != null && { citation_type: options.citationType }),\n ...(options.position != null && { position: options.position }),\n },\n }));\n\n await this.client.recordEvents(sessionId, events);\n }\n\n /**\n * Emit `content_engaged` events when a user interacts with content.\n *\n * Call this when a user clicks a link, views an embedded product, or\n * otherwise actively engages with retrieved content. This is the\n * strongest attribution signal before a purchase event.\n *\n * @param externalSessionId - Caller-supplied conversation identifier.\n * @param urls - URLs the user engaged with.\n * @param options - Optional engagement metadata.\n *\n * @example\n * ```ts\n * // In a redirect/tracking endpoint\n * await tracker.trackEngaged(sessionId, [productUrl], {\n * interactionType: \"click\",\n * });\n * ```\n */\n async trackEngaged(\n externalSessionId: string | undefined,\n urls: string[],\n options: {\n interactionType?: \"click\" | \"view\" | \"expand\" | \"share\";\n } = {},\n ): Promise<void> {\n if (urls.length === 0) return;\n const sessionId = await this.getOrCreateSession(externalSessionId);\n if (sessionId == null) return;\n\n const now = new Date().toISOString();\n const events: TelemetryEvent[] = urls.map((url) => ({\n id: crypto.randomUUID(),\n type: \"content_engaged\" as const,\n timestamp: now,\n contentUrl: url,\n data: {\n ...(options.interactionType != null && {\n interaction_type: options.interactionType,\n }),\n },\n }));\n\n await this.client.recordEvents(sessionId, events);\n }\n\n /**\n * Record a checkout outcome and end the session.\n *\n * Call this when a user completes a purchase, abandons checkout, or\n * the conversation concludes with a clear commerce outcome.\n * Emits a `checkout_completed` or `checkout_abandoned` event then\n * ends the session with the appropriate outcome type.\n *\n * @param externalSessionId - Caller-supplied conversation identifier.\n * @param outcome - Purchase details.\n *\n * @example\n * ```ts\n * // User completed a purchase\n * await tracker.trackCheckout(sessionId, {\n * type: \"completed\",\n * valueAmount: 4999, // $49.99 in minor units (cents)\n * currency: \"USD\",\n * });\n *\n * // User abandoned checkout\n * await tracker.trackCheckout(sessionId, { type: \"abandoned\" });\n * ```\n */\n async trackCheckout(\n externalSessionId: string | undefined,\n outcome: {\n type: \"completed\" | \"abandoned\" | \"started\";\n valueAmount?: number;\n currency?: string;\n products?: string[];\n },\n ): Promise<void> {\n const sessionId = externalSessionId != null\n ? (this.registry.get(externalSessionId) ?? null)\n : null;\n\n if (sessionId == null) return;\n\n // Emit the checkout event\n const eventType = outcome.type === \"completed\"\n ? \"checkout_completed\" as const\n : outcome.type === \"abandoned\"\n ? \"checkout_abandoned\" as const\n : \"checkout_started\" as const;\n\n await this.client.recordEvent(sessionId, eventType);\n\n // End the session with an outcome for completed/abandoned\n if (outcome.type === \"completed\" || outcome.type === \"abandoned\") {\n const outcomeType = outcome.type === \"completed\" ? \"conversion\" as const : \"abandonment\" as const;\n await this.client.endSession(sessionId, {\n type: outcomeType,\n ...(outcome.valueAmount != null && { valueAmount: outcome.valueAmount }),\n ...(outcome.currency != null && { currency: outcome.currency }),\n ...(outcome.products != null && { products: outcome.products }),\n });\n if (externalSessionId != null) {\n this.registry.delete(externalSessionId);\n }\n }\n }\n\n /**\n * End a session with an explicit outcome.\n *\n * Use `trackCheckout` for commerce outcomes. Use this for non-commerce\n * session endings (browse sessions, timeouts, explicit abandonment).\n */\n async endSession(\n externalSessionId: string | undefined,\n outcome: { type: \"conversion\" | \"abandonment\" | \"browse\"; valueAmount?: number; currency?: string },\n ): Promise<void> {\n const sessionId = externalSessionId != null\n ? (this.registry.get(externalSessionId) ?? null)\n : null;\n\n if (sessionId == null) return;\n\n await this.client.endSession(sessionId, {\n type: outcome.type,\n ...(outcome.valueAmount != null && { valueAmount: outcome.valueAmount }),\n ...(outcome.currency != null && { currency: outcome.currency }),\n });\n\n this.registry.delete(externalSessionId!);\n }\n\n /** Number of active sessions in the registry. */\n get sessionCount(): number {\n return this.registry.size;\n }\n}\n","/**\n * OpenAttribution Telemetry — content URL extraction utilities.\n *\n * Helpers for extracting content URLs from AI-generated text, so they\n * can be recorded as telemetry events without manual instrumentation.\n */\n\n/** Matches `[text](url)` — standard Markdown links. */\nconst MARKDOWN_LINK_RE = /\\[([^\\]]+)\\]\\((https?:\\/\\/[^)]+)\\)/g;\n\n/** Matches bare URLs starting with http/https. */\nconst BARE_URL_RE = /https?:\\/\\/[^\\s<>\"')\\]]+/g;\n\n/**\n * Extract all HTTP/HTTPS URLs from Markdown-formatted text.\n *\n * Finds URLs in two forms:\n * - Markdown links: `[anchor text](https://...)`\n * - Bare URLs: `https://...`\n *\n * Results are deduplicated. Useful for extracting citation URLs from\n * AI model responses that include web search results.\n *\n * @example\n * ```ts\n * const text = \"According to [Wirecutter](https://nytimes.com/wirecutter/reviews/...), ...\";\n * const urls = extractCitationUrls(text);\n * // [\"https://nytimes.com/wirecutter/reviews/...\"]\n * ```\n */\nexport function extractCitationUrls(text: string): string[] {\n const urls = new Set<string>();\n\n for (const match of text.matchAll(MARKDOWN_LINK_RE)) {\n const url = match[2];\n if (url != null) urls.add(cleanUrl(url));\n }\n\n // Only look for bare URLs if Markdown links didn't cover everything\n for (const match of text.matchAll(BARE_URL_RE)) {\n urls.add(cleanUrl(match[0]));\n }\n\n return [...urls];\n}\n\n/**\n * Extract URLs from a list of search result objects.\n *\n * Accepts any object with a `url` string property — works with\n * Channel3, Exa, Tavily, and most search API responses.\n *\n * @example\n * ```ts\n * const products = await channel3.search({ query: \"headphones\" });\n * const urls = extractResultUrls(products);\n * await tracker.trackRetrieved(sessionId, urls);\n * ```\n */\nexport function extractResultUrls(\n results: Array<{ url?: string | null; link?: string | null }>,\n): string[] {\n const urls: string[] = [];\n for (const r of results) {\n const url = r.url ?? r.link;\n if (url != null && url.startsWith(\"http\")) {\n urls.push(url);\n }\n }\n return [...new Set(urls)];\n}\n\n/**\n * Build a tracking URL that routes through your server-side redirect endpoint.\n *\n * Your endpoint emits a `content_engaged` event then redirects the user to\n * the original URL. This is the most reliable pattern for click tracking —\n * it fires server-side before the redirect, so it works regardless of\n * browser JS state or navigation timing.\n *\n * @param contentUrl - The destination URL to redirect to after tracking.\n * @param options.endpoint - Your tracking endpoint base URL (e.g. `https://example.com/api/track`).\n * @param options.sessionId - Optional session ID to link the click to an existing session.\n *\n * @example\n * ```ts\n * // Build a tracked URL for a product link\n * const trackedUrl = createTrackingUrl(\"https://shop.example.com/product\", {\n * endpoint: \"https://myagent.com/api/track\",\n * sessionId: \"conv-abc123\",\n * });\n * // → \"https://myagent.com/api/track?url=https%3A%2F%2Fshop.example.com%2Fproduct&session_id=conv-abc123\"\n *\n * // Server-side handler (Next.js example):\n * export async function GET(req: Request) {\n * const { searchParams } = new URL(req.url);\n * const url = searchParams.get(\"url\");\n * const sessionId = searchParams.get(\"session_id\") ?? undefined;\n * if (!url) return new Response(\"Missing url\", { status: 400 });\n * void tracker.trackEngaged(sessionId, [url], { interactionType: \"click\" });\n * return Response.redirect(url, 302);\n * }\n * ```\n */\nexport function createTrackingUrl(\n contentUrl: string,\n options: {\n endpoint: string;\n sessionId?: string;\n },\n): string {\n const url = new URL(options.endpoint);\n url.searchParams.set(\"url\", contentUrl);\n if (options.sessionId != null) {\n url.searchParams.set(\"session_id\", options.sessionId);\n }\n return url.toString();\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/** Remove trailing punctuation that commonly attaches to bare URLs. */\nfunction cleanUrl(url: string): string {\n return url.replace(/[.,;:!?]+$/, \"\");\n}\n","/**\n * OpenAttribution Telemetry — ACP (Agentic Commerce Protocol) bridge.\n *\n * Converts a TelemetrySession into the `content_attribution` object\n * defined in the ACP RFC. Include this in a CheckoutSessionCreateRequest\n * or CheckoutSessionCompleteRequest.\n *\n * Reference: acp/rfc.content_attribution.md\n */\n\nimport type { TelemetrySession } from \"./types.js\";\n\nexport interface ContentAttributionRetrieved {\n content_url: string;\n timestamp: string;\n}\n\nexport interface ContentAttributionCited {\n content_url: string;\n timestamp: string;\n citation_type?: string;\n excerpt_tokens?: number;\n position?: string;\n content_hash?: string;\n}\n\nexport interface ConversationSummary {\n turn_count: number;\n topics: string[];\n}\n\n/**\n * ACP `content_attribution` object.\n * Include in CheckoutSessionCreateRequest.content_attribution.\n */\nexport interface ContentAttribution {\n content_scope?: string;\n content_retrieved: ContentAttributionRetrieved[];\n content_cited?: ContentAttributionCited[];\n conversation_summary?: ConversationSummary;\n}\n\n/**\n * Convert a TelemetrySession into an ACP `content_attribution` object.\n *\n * @example\n * ```ts\n * import { sessionToContentAttribution } from \"@openattribution/telemetry/acp\";\n *\n * const attribution = sessionToContentAttribution(session);\n *\n * // Include in ACP checkout request:\n * await acp.createCheckout({\n * cart: { ... },\n * content_attribution: attribution,\n * });\n * ```\n */\nexport function sessionToContentAttribution(\n session: TelemetrySession,\n): ContentAttribution {\n const retrieved: ContentAttributionRetrieved[] = session.events\n .filter((e) => e.type === \"content_retrieved\" && e.contentUrl != null)\n .map((e) => ({\n content_url: e.contentUrl!,\n timestamp: e.timestamp,\n }));\n\n const cited: ContentAttributionCited[] = session.events\n .filter((e) => e.type === \"content_cited\" && e.contentUrl != null)\n .map((e) => ({\n content_url: e.contentUrl!,\n timestamp: e.timestamp,\n ...(e.data?.[\"citation_type\"] != null && {\n citation_type: String(e.data[\"citation_type\"]),\n }),\n ...(e.data?.[\"excerpt_tokens\"] != null && {\n excerpt_tokens: Number(e.data[\"excerpt_tokens\"]),\n }),\n ...(e.data?.[\"position\"] != null && {\n position: String(e.data[\"position\"]),\n }),\n ...(e.data?.[\"content_hash\"] != null && {\n content_hash: String(e.data[\"content_hash\"]),\n }),\n }));\n\n const turnEvents = session.events.filter(\n (e) => e.type === \"turn_completed\" || e.type === \"turn_started\",\n );\n const turnCount = Math.max(\n session.events.filter((e) => e.type === \"turn_completed\").length,\n 1,\n );\n\n const topics = [\n ...new Set(\n session.events.flatMap((e) => e.turn?.topics ?? []),\n ),\n ];\n\n const result: ContentAttribution = {\n ...(session.contentScope != null && { content_scope: session.contentScope }),\n content_retrieved: retrieved,\n ...(cited.length > 0 && { content_cited: cited }),\n ...((turnEvents.length > 0 || topics.length > 0) && {\n conversation_summary: { turn_count: turnCount, topics },\n }),\n };\n\n return result;\n}\n","/**\n * OpenAttribution Telemetry — UCP (Universal Checkout Protocol) bridge.\n *\n * Converts a TelemetrySession into the UCP attribution extension object.\n *\n * Reference: ucp/EXTENSION.md\n */\n\nimport type { TelemetrySession } from \"./types.js\";\n\nexport interface UCPAttribution {\n content_scope?: string;\n prior_session_ids?: string[];\n content_retrieved: Array<{ content_url: string; timestamp: string }>;\n content_cited?: Array<{\n content_url: string;\n timestamp: string;\n citation_type?: string;\n position?: string;\n }>;\n conversation_summary?: {\n turn_count: number;\n topics: string[];\n };\n}\n\n/**\n * Convert a TelemetrySession into a UCP attribution extension object.\n *\n * @example\n * ```ts\n * import { sessionToAttribution } from \"@openattribution/telemetry/ucp\";\n *\n * const attribution = sessionToAttribution(session);\n *\n * // Include in UCP checkout:\n * await ucp.completeCheckout({\n * order: { ... },\n * extensions: { \"org.openattribution.telemetry\": attribution },\n * });\n * ```\n */\nexport function sessionToAttribution(session: TelemetrySession): UCPAttribution {\n const retrieved = session.events\n .filter((e) => e.type === \"content_retrieved\" && e.contentUrl != null)\n .map((e) => ({ content_url: e.contentUrl!, timestamp: e.timestamp }));\n\n const cited = session.events\n .filter((e) => e.type === \"content_cited\" && e.contentUrl != null)\n .map((e) => ({\n content_url: e.contentUrl!,\n timestamp: e.timestamp,\n ...(e.data?.[\"citation_type\"] != null && {\n citation_type: String(e.data[\"citation_type\"]),\n }),\n ...(e.data?.[\"position\"] != null && {\n position: String(e.data[\"position\"]),\n }),\n }));\n\n const turnCount = Math.max(\n session.events.filter((e) => e.type === \"turn_completed\").length,\n 1,\n );\n const topics = [...new Set(session.events.flatMap((e) => e.turn?.topics ?? []))];\n const priorIds = (session.priorSessionIds ?? []).filter(Boolean);\n\n return {\n ...(session.contentScope != null && { content_scope: session.contentScope }),\n ...(priorIds.length > 0 && { prior_session_ids: priorIds }),\n content_retrieved: retrieved,\n ...(cited.length > 0 && { content_cited: cited }),\n ...((topics.length > 0 || turnCount > 0) && {\n conversation_summary: { turn_count: turnCount, topics },\n }),\n };\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -283,10 +283,62 @@ declare class MCPSessionTracker {
283
283
  position?: "primary" | "supporting" | "mentioned";
284
284
  }): Promise<void>;
285
285
  /**
286
- * End a session with an outcome.
286
+ * Emit `content_engaged` events when a user interacts with content.
287
+ *
288
+ * Call this when a user clicks a link, views an embedded product, or
289
+ * otherwise actively engages with retrieved content. This is the
290
+ * strongest attribution signal before a purchase event.
291
+ *
292
+ * @param externalSessionId - Caller-supplied conversation identifier.
293
+ * @param urls - URLs the user engaged with.
294
+ * @param options - Optional engagement metadata.
287
295
  *
288
- * Call this when the conversation concludes — at checkout, after
289
- * the user clicks a link, or when the session times out.
296
+ * @example
297
+ * ```ts
298
+ * // In a redirect/tracking endpoint
299
+ * await tracker.trackEngaged(sessionId, [productUrl], {
300
+ * interactionType: "click",
301
+ * });
302
+ * ```
303
+ */
304
+ trackEngaged(externalSessionId: string | undefined, urls: string[], options?: {
305
+ interactionType?: "click" | "view" | "expand" | "share";
306
+ }): Promise<void>;
307
+ /**
308
+ * Record a checkout outcome and end the session.
309
+ *
310
+ * Call this when a user completes a purchase, abandons checkout, or
311
+ * the conversation concludes with a clear commerce outcome.
312
+ * Emits a `checkout_completed` or `checkout_abandoned` event then
313
+ * ends the session with the appropriate outcome type.
314
+ *
315
+ * @param externalSessionId - Caller-supplied conversation identifier.
316
+ * @param outcome - Purchase details.
317
+ *
318
+ * @example
319
+ * ```ts
320
+ * // User completed a purchase
321
+ * await tracker.trackCheckout(sessionId, {
322
+ * type: "completed",
323
+ * valueAmount: 4999, // $49.99 in minor units (cents)
324
+ * currency: "USD",
325
+ * });
326
+ *
327
+ * // User abandoned checkout
328
+ * await tracker.trackCheckout(sessionId, { type: "abandoned" });
329
+ * ```
330
+ */
331
+ trackCheckout(externalSessionId: string | undefined, outcome: {
332
+ type: "completed" | "abandoned" | "started";
333
+ valueAmount?: number;
334
+ currency?: string;
335
+ products?: string[];
336
+ }): Promise<void>;
337
+ /**
338
+ * End a session with an explicit outcome.
339
+ *
340
+ * Use `trackCheckout` for commerce outcomes. Use this for non-commerce
341
+ * session endings (browse sessions, timeouts, explicit abandonment).
290
342
  */
291
343
  endSession(externalSessionId: string | undefined, outcome: {
292
344
  type: "conversion" | "abandonment" | "browse";
@@ -338,6 +390,42 @@ declare function extractResultUrls(results: Array<{
338
390
  url?: string | null;
339
391
  link?: string | null;
340
392
  }>): string[];
393
+ /**
394
+ * Build a tracking URL that routes through your server-side redirect endpoint.
395
+ *
396
+ * Your endpoint emits a `content_engaged` event then redirects the user to
397
+ * the original URL. This is the most reliable pattern for click tracking —
398
+ * it fires server-side before the redirect, so it works regardless of
399
+ * browser JS state or navigation timing.
400
+ *
401
+ * @param contentUrl - The destination URL to redirect to after tracking.
402
+ * @param options.endpoint - Your tracking endpoint base URL (e.g. `https://example.com/api/track`).
403
+ * @param options.sessionId - Optional session ID to link the click to an existing session.
404
+ *
405
+ * @example
406
+ * ```ts
407
+ * // Build a tracked URL for a product link
408
+ * const trackedUrl = createTrackingUrl("https://shop.example.com/product", {
409
+ * endpoint: "https://myagent.com/api/track",
410
+ * sessionId: "conv-abc123",
411
+ * });
412
+ * // → "https://myagent.com/api/track?url=https%3A%2F%2Fshop.example.com%2Fproduct&session_id=conv-abc123"
413
+ *
414
+ * // Server-side handler (Next.js example):
415
+ * export async function GET(req: Request) {
416
+ * const { searchParams } = new URL(req.url);
417
+ * const url = searchParams.get("url");
418
+ * const sessionId = searchParams.get("session_id") ?? undefined;
419
+ * if (!url) return new Response("Missing url", { status: 400 });
420
+ * void tracker.trackEngaged(sessionId, [url], { interactionType: "click" });
421
+ * return Response.redirect(url, 302);
422
+ * }
423
+ * ```
424
+ */
425
+ declare function createTrackingUrl(contentUrl: string, options: {
426
+ endpoint: string;
427
+ sessionId?: string;
428
+ }): string;
341
429
 
342
430
  /**
343
431
  * OpenAttribution Telemetry — ACP (Agentic Commerce Protocol) bridge.
@@ -437,4 +525,4 @@ interface UCPAttribution {
437
525
  */
438
526
  declare function sessionToAttribution(session: TelemetrySession): UCPAttribution;
439
527
 
440
- export { type CitationPosition, type CitationType, type ContentAttribution, type ContentAttributionCited, type ContentAttributionRetrieved, type ConversationTurn, type EventType, type Initiator, type InitiatorType, type IntentCategory, MCPSessionTracker, type OutcomeType, type PrivacyLevel, type SessionOutcome, type StartSessionOptions, TelemetryClient, type TelemetryClientOptions, type TelemetryEvent, type TelemetrySession, type UCPAttribution, type UserContext, extractCitationUrls, extractResultUrls, sessionToAttribution, sessionToContentAttribution };
528
+ export { type CitationPosition, type CitationType, type ContentAttribution, type ContentAttributionCited, type ContentAttributionRetrieved, type ConversationTurn, type EventType, type Initiator, type InitiatorType, type IntentCategory, MCPSessionTracker, type OutcomeType, type PrivacyLevel, type SessionOutcome, type StartSessionOptions, TelemetryClient, type TelemetryClientOptions, type TelemetryEvent, type TelemetrySession, type UCPAttribution, type UserContext, createTrackingUrl, extractCitationUrls, extractResultUrls, sessionToAttribution, sessionToContentAttribution };
package/dist/index.d.ts CHANGED
@@ -283,10 +283,62 @@ declare class MCPSessionTracker {
283
283
  position?: "primary" | "supporting" | "mentioned";
284
284
  }): Promise<void>;
285
285
  /**
286
- * End a session with an outcome.
286
+ * Emit `content_engaged` events when a user interacts with content.
287
+ *
288
+ * Call this when a user clicks a link, views an embedded product, or
289
+ * otherwise actively engages with retrieved content. This is the
290
+ * strongest attribution signal before a purchase event.
291
+ *
292
+ * @param externalSessionId - Caller-supplied conversation identifier.
293
+ * @param urls - URLs the user engaged with.
294
+ * @param options - Optional engagement metadata.
287
295
  *
288
- * Call this when the conversation concludes — at checkout, after
289
- * the user clicks a link, or when the session times out.
296
+ * @example
297
+ * ```ts
298
+ * // In a redirect/tracking endpoint
299
+ * await tracker.trackEngaged(sessionId, [productUrl], {
300
+ * interactionType: "click",
301
+ * });
302
+ * ```
303
+ */
304
+ trackEngaged(externalSessionId: string | undefined, urls: string[], options?: {
305
+ interactionType?: "click" | "view" | "expand" | "share";
306
+ }): Promise<void>;
307
+ /**
308
+ * Record a checkout outcome and end the session.
309
+ *
310
+ * Call this when a user completes a purchase, abandons checkout, or
311
+ * the conversation concludes with a clear commerce outcome.
312
+ * Emits a `checkout_completed` or `checkout_abandoned` event then
313
+ * ends the session with the appropriate outcome type.
314
+ *
315
+ * @param externalSessionId - Caller-supplied conversation identifier.
316
+ * @param outcome - Purchase details.
317
+ *
318
+ * @example
319
+ * ```ts
320
+ * // User completed a purchase
321
+ * await tracker.trackCheckout(sessionId, {
322
+ * type: "completed",
323
+ * valueAmount: 4999, // $49.99 in minor units (cents)
324
+ * currency: "USD",
325
+ * });
326
+ *
327
+ * // User abandoned checkout
328
+ * await tracker.trackCheckout(sessionId, { type: "abandoned" });
329
+ * ```
330
+ */
331
+ trackCheckout(externalSessionId: string | undefined, outcome: {
332
+ type: "completed" | "abandoned" | "started";
333
+ valueAmount?: number;
334
+ currency?: string;
335
+ products?: string[];
336
+ }): Promise<void>;
337
+ /**
338
+ * End a session with an explicit outcome.
339
+ *
340
+ * Use `trackCheckout` for commerce outcomes. Use this for non-commerce
341
+ * session endings (browse sessions, timeouts, explicit abandonment).
290
342
  */
291
343
  endSession(externalSessionId: string | undefined, outcome: {
292
344
  type: "conversion" | "abandonment" | "browse";
@@ -338,6 +390,42 @@ declare function extractResultUrls(results: Array<{
338
390
  url?: string | null;
339
391
  link?: string | null;
340
392
  }>): string[];
393
+ /**
394
+ * Build a tracking URL that routes through your server-side redirect endpoint.
395
+ *
396
+ * Your endpoint emits a `content_engaged` event then redirects the user to
397
+ * the original URL. This is the most reliable pattern for click tracking —
398
+ * it fires server-side before the redirect, so it works regardless of
399
+ * browser JS state or navigation timing.
400
+ *
401
+ * @param contentUrl - The destination URL to redirect to after tracking.
402
+ * @param options.endpoint - Your tracking endpoint base URL (e.g. `https://example.com/api/track`).
403
+ * @param options.sessionId - Optional session ID to link the click to an existing session.
404
+ *
405
+ * @example
406
+ * ```ts
407
+ * // Build a tracked URL for a product link
408
+ * const trackedUrl = createTrackingUrl("https://shop.example.com/product", {
409
+ * endpoint: "https://myagent.com/api/track",
410
+ * sessionId: "conv-abc123",
411
+ * });
412
+ * // → "https://myagent.com/api/track?url=https%3A%2F%2Fshop.example.com%2Fproduct&session_id=conv-abc123"
413
+ *
414
+ * // Server-side handler (Next.js example):
415
+ * export async function GET(req: Request) {
416
+ * const { searchParams } = new URL(req.url);
417
+ * const url = searchParams.get("url");
418
+ * const sessionId = searchParams.get("session_id") ?? undefined;
419
+ * if (!url) return new Response("Missing url", { status: 400 });
420
+ * void tracker.trackEngaged(sessionId, [url], { interactionType: "click" });
421
+ * return Response.redirect(url, 302);
422
+ * }
423
+ * ```
424
+ */
425
+ declare function createTrackingUrl(contentUrl: string, options: {
426
+ endpoint: string;
427
+ sessionId?: string;
428
+ }): string;
341
429
 
342
430
  /**
343
431
  * OpenAttribution Telemetry — ACP (Agentic Commerce Protocol) bridge.
@@ -437,4 +525,4 @@ interface UCPAttribution {
437
525
  */
438
526
  declare function sessionToAttribution(session: TelemetrySession): UCPAttribution;
439
527
 
440
- export { type CitationPosition, type CitationType, type ContentAttribution, type ContentAttributionCited, type ContentAttributionRetrieved, type ConversationTurn, type EventType, type Initiator, type InitiatorType, type IntentCategory, MCPSessionTracker, type OutcomeType, type PrivacyLevel, type SessionOutcome, type StartSessionOptions, TelemetryClient, type TelemetryClientOptions, type TelemetryEvent, type TelemetrySession, type UCPAttribution, type UserContext, extractCitationUrls, extractResultUrls, sessionToAttribution, sessionToContentAttribution };
528
+ export { type CitationPosition, type CitationType, type ContentAttribution, type ContentAttributionCited, type ContentAttributionRetrieved, type ConversationTurn, type EventType, type Initiator, type InitiatorType, type IntentCategory, MCPSessionTracker, type OutcomeType, type PrivacyLevel, type SessionOutcome, type StartSessionOptions, TelemetryClient, type TelemetryClientOptions, type TelemetryEvent, type TelemetrySession, type UCPAttribution, type UserContext, createTrackingUrl, extractCitationUrls, extractResultUrls, sessionToAttribution, sessionToContentAttribution };
package/dist/index.js CHANGED
@@ -276,10 +276,89 @@ var MCPSessionTracker = class {
276
276
  await this.client.recordEvents(sessionId, events);
277
277
  }
278
278
  /**
279
- * End a session with an outcome.
279
+ * Emit `content_engaged` events when a user interacts with content.
280
+ *
281
+ * Call this when a user clicks a link, views an embedded product, or
282
+ * otherwise actively engages with retrieved content. This is the
283
+ * strongest attribution signal before a purchase event.
280
284
  *
281
- * Call this when the conversation concludes — at checkout, after
282
- * the user clicks a link, or when the session times out.
285
+ * @param externalSessionId - Caller-supplied conversation identifier.
286
+ * @param urls - URLs the user engaged with.
287
+ * @param options - Optional engagement metadata.
288
+ *
289
+ * @example
290
+ * ```ts
291
+ * // In a redirect/tracking endpoint
292
+ * await tracker.trackEngaged(sessionId, [productUrl], {
293
+ * interactionType: "click",
294
+ * });
295
+ * ```
296
+ */
297
+ async trackEngaged(externalSessionId, urls, options = {}) {
298
+ if (urls.length === 0) return;
299
+ const sessionId = await this.getOrCreateSession(externalSessionId);
300
+ if (sessionId == null) return;
301
+ const now = (/* @__PURE__ */ new Date()).toISOString();
302
+ const events = urls.map((url) => ({
303
+ id: crypto.randomUUID(),
304
+ type: "content_engaged",
305
+ timestamp: now,
306
+ contentUrl: url,
307
+ data: {
308
+ ...options.interactionType != null && {
309
+ interaction_type: options.interactionType
310
+ }
311
+ }
312
+ }));
313
+ await this.client.recordEvents(sessionId, events);
314
+ }
315
+ /**
316
+ * Record a checkout outcome and end the session.
317
+ *
318
+ * Call this when a user completes a purchase, abandons checkout, or
319
+ * the conversation concludes with a clear commerce outcome.
320
+ * Emits a `checkout_completed` or `checkout_abandoned` event then
321
+ * ends the session with the appropriate outcome type.
322
+ *
323
+ * @param externalSessionId - Caller-supplied conversation identifier.
324
+ * @param outcome - Purchase details.
325
+ *
326
+ * @example
327
+ * ```ts
328
+ * // User completed a purchase
329
+ * await tracker.trackCheckout(sessionId, {
330
+ * type: "completed",
331
+ * valueAmount: 4999, // $49.99 in minor units (cents)
332
+ * currency: "USD",
333
+ * });
334
+ *
335
+ * // User abandoned checkout
336
+ * await tracker.trackCheckout(sessionId, { type: "abandoned" });
337
+ * ```
338
+ */
339
+ async trackCheckout(externalSessionId, outcome) {
340
+ const sessionId = externalSessionId != null ? this.registry.get(externalSessionId) ?? null : null;
341
+ if (sessionId == null) return;
342
+ const eventType = outcome.type === "completed" ? "checkout_completed" : outcome.type === "abandoned" ? "checkout_abandoned" : "checkout_started";
343
+ await this.client.recordEvent(sessionId, eventType);
344
+ if (outcome.type === "completed" || outcome.type === "abandoned") {
345
+ const outcomeType = outcome.type === "completed" ? "conversion" : "abandonment";
346
+ await this.client.endSession(sessionId, {
347
+ type: outcomeType,
348
+ ...outcome.valueAmount != null && { valueAmount: outcome.valueAmount },
349
+ ...outcome.currency != null && { currency: outcome.currency },
350
+ ...outcome.products != null && { products: outcome.products }
351
+ });
352
+ if (externalSessionId != null) {
353
+ this.registry.delete(externalSessionId);
354
+ }
355
+ }
356
+ }
357
+ /**
358
+ * End a session with an explicit outcome.
359
+ *
360
+ * Use `trackCheckout` for commerce outcomes. Use this for non-commerce
361
+ * session endings (browse sessions, timeouts, explicit abandonment).
283
362
  */
284
363
  async endSession(externalSessionId, outcome) {
285
364
  const sessionId = externalSessionId != null ? this.registry.get(externalSessionId) ?? null : null;
@@ -321,6 +400,14 @@ function extractResultUrls(results) {
321
400
  }
322
401
  return [...new Set(urls)];
323
402
  }
403
+ function createTrackingUrl(contentUrl, options) {
404
+ const url = new URL(options.endpoint);
405
+ url.searchParams.set("url", contentUrl);
406
+ if (options.sessionId != null) {
407
+ url.searchParams.set("session_id", options.sessionId);
408
+ }
409
+ return url.toString();
410
+ }
324
411
  function cleanUrl(url) {
325
412
  return url.replace(/[.,;:!?]+$/, "");
326
413
  }
@@ -402,6 +489,7 @@ function sessionToAttribution(session) {
402
489
  export {
403
490
  MCPSessionTracker,
404
491
  TelemetryClient,
492
+ createTrackingUrl,
405
493
  extractCitationUrls,
406
494
  extractResultUrls,
407
495
  sessionToAttribution,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts","../src/mcp.ts","../src/extract.ts","../src/acp.ts","../src/ucp.ts"],"sourcesContent":["/**\n * OpenAttribution Telemetry — HTTP client.\n *\n * Zero dependencies — uses native fetch (Node 18+, Deno, browsers, Edge).\n */\n\nimport type {\n ConversationTurn,\n EventType,\n Initiator,\n SessionOutcome,\n StartSessionOptions,\n TelemetryClientOptions,\n TelemetryEvent,\n TelemetrySession,\n UserContext,\n} from \"./types.js\";\n\nconst TRANSIENT_STATUS_CODES = new Set([429, 500, 502, 503, 504]);\n\n// ---------------------------------------------------------------------------\n// Wire format helpers (camelCase → snake_case for the JSON body)\n// ---------------------------------------------------------------------------\n\nfunction turnToWire(turn: ConversationTurn): Record<string, unknown> {\n return {\n privacy_level: turn.privacyLevel,\n query_text: turn.queryText,\n response_text: turn.responseText,\n query_intent: turn.queryIntent,\n response_type: turn.responseType,\n topics: turn.topics,\n content_urls_retrieved: turn.contentUrlsRetrieved,\n content_urls_cited: turn.contentUrlsCited,\n query_tokens: turn.queryTokens,\n response_tokens: turn.responseTokens,\n model_id: turn.modelId,\n };\n}\n\nfunction eventToWire(event: TelemetryEvent): Record<string, unknown> {\n return {\n id: event.id,\n type: event.type,\n timestamp: event.timestamp,\n content_url: event.contentUrl,\n product_id: event.productId,\n turn: event.turn != null ? turnToWire(event.turn) : undefined,\n data: event.data ?? {},\n };\n}\n\nfunction initiatorToWire(i: Initiator): Record<string, unknown> {\n return {\n agent_id: i.agentId,\n manifest_ref: i.manifestRef,\n operator_id: i.operatorId,\n };\n}\n\nfunction userContextToWire(uc: UserContext): Record<string, unknown> {\n return {\n external_id: uc.externalId,\n segments: uc.segments ?? [],\n attributes: uc.attributes ?? {},\n };\n}\n\nfunction outcomeToWire(o: SessionOutcome): Record<string, unknown> {\n return {\n type: o.type,\n value_amount: o.valueAmount ?? 0,\n currency: o.currency ?? \"USD\",\n products: o.products ?? [],\n metadata: o.metadata ?? {},\n };\n}\n\n// ---------------------------------------------------------------------------\n// TelemetryClient\n// ---------------------------------------------------------------------------\n\n/**\n * Async client for recording OpenAttribution telemetry.\n *\n * Works in Node.js ≥ 18, Deno, browsers, and Edge runtimes (Vercel, Cloudflare).\n *\n * @example\n * ```ts\n * const client = new TelemetryClient({\n * endpoint: \"https://telemetry.example.com\",\n * apiKey: \"your-api-key\",\n * failSilently: true,\n * });\n *\n * const sessionId = await client.startSession({ contentScope: \"my-mix\" });\n *\n * await client.recordEvents(sessionId, [\n * { id: crypto.randomUUID(), type: \"content_retrieved\",\n * timestamp: new Date().toISOString(), contentUrl: \"https://...\" }\n * ]);\n *\n * await client.endSession(sessionId, { type: \"browse\" });\n * ```\n */\nexport class TelemetryClient {\n private readonly endpoint: string;\n private readonly apiKey: string | undefined;\n private readonly failSilently: boolean;\n private readonly timeout: number;\n private readonly maxRetries: number;\n\n constructor(options: TelemetryClientOptions) {\n this.endpoint = options.endpoint.replace(/\\/$/, \"\");\n this.apiKey = options.apiKey;\n this.failSilently = options.failSilently ?? true;\n this.timeout = options.timeout ?? 30_000;\n this.maxRetries = options.maxRetries ?? 3;\n }\n\n private headers(): Record<string, string> {\n const h: Record<string, string> = { \"Content-Type\": \"application/json\" };\n if (this.apiKey != null) h[\"X-API-Key\"] = this.apiKey;\n return h;\n }\n\n private async post(path: string, body: unknown): Promise<unknown> {\n const url = `${this.endpoint}${path}`;\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= this.maxRetries; attempt++) {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const res = await fetch(url, {\n method: \"POST\",\n headers: this.headers(),\n body: JSON.stringify(body),\n signal: controller.signal,\n });\n\n if (TRANSIENT_STATUS_CODES.has(res.status) && attempt < this.maxRetries) {\n const wait = 2 ** attempt * 1000 + Math.random() * 500;\n await sleep(wait);\n continue;\n }\n\n if (!res.ok) {\n throw new Error(`HTTP ${res.status} ${res.statusText} from ${url}`);\n }\n\n return await res.json();\n } catch (err) {\n lastError = err;\n if (attempt < this.maxRetries && isTransientError(err)) {\n const wait = 2 ** attempt * 1000 + Math.random() * 500;\n await sleep(wait);\n } else {\n break;\n }\n } finally {\n clearTimeout(timer);\n }\n }\n\n if (this.failSilently) {\n return null;\n }\n throw lastError;\n }\n\n /**\n * Start a new telemetry session.\n *\n * @returns Session ID string, or null on silent failure.\n */\n async startSession(options: StartSessionOptions = {}): Promise<string | null> {\n const result = await this.post(\"/session/start\", {\n initiator_type: options.initiatorType ?? \"user\",\n initiator:\n options.initiator != null ? initiatorToWire(options.initiator) : null,\n content_scope: options.contentScope,\n agent_id: options.agentId,\n external_session_id: options.externalSessionId,\n user_context:\n options.userContext != null\n ? userContextToWire(options.userContext)\n : {},\n manifest_ref: options.manifestRef,\n prior_session_ids: options.priorSessionIds ?? [],\n }) as { session_id?: string } | null;\n\n return result?.session_id ?? null;\n }\n\n /**\n * Record a single telemetry event.\n */\n async recordEvent(\n sessionId: string | null,\n eventType: EventType,\n options: {\n contentUrl?: string;\n productId?: string;\n turn?: ConversationTurn;\n data?: Record<string, unknown>;\n } = {},\n ): Promise<void> {\n if (sessionId == null) return;\n await this.recordEvents(sessionId, [\n {\n id: crypto.randomUUID(),\n type: eventType,\n timestamp: new Date().toISOString(),\n ...options,\n },\n ]);\n }\n\n /**\n * Record a batch of telemetry events.\n */\n async recordEvents(\n sessionId: string | null,\n events: TelemetryEvent[],\n ): Promise<void> {\n if (sessionId == null || events.length === 0) return;\n await this.post(\"/events\", {\n session_id: sessionId,\n events: events.map(eventToWire),\n });\n }\n\n /**\n * End a session with an outcome.\n */\n async endSession(\n sessionId: string | null,\n outcome: SessionOutcome,\n ): Promise<void> {\n if (sessionId == null) return;\n await this.post(\"/session/end\", {\n session_id: sessionId,\n outcome: outcomeToWire(outcome),\n });\n }\n\n /**\n * Upload a complete session in one request (bulk path).\n *\n * Useful for post-hoc reporting or when you've built the session\n * locally and want to submit it in one shot.\n *\n * @returns Server-assigned session ID, or null on silent failure.\n */\n async uploadSession(session: TelemetrySession): Promise<string | null> {\n const result = await this.post(\"/session/bulk\", sessionToWire(session)) as\n | { session_id?: string }\n | null;\n return result?.session_id ?? null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction isTransientError(err: unknown): boolean {\n if (err instanceof Error) {\n // AbortError (timeout), network errors\n return (\n err.name === \"AbortError\" ||\n err.name === \"TypeError\" ||\n err.message.includes(\"fetch\")\n );\n }\n return false;\n}\n\nfunction sessionToWire(session: TelemetrySession): Record<string, unknown> {\n return {\n schema_version: session.schemaVersion ?? \"0.4\",\n session_id: session.sessionId,\n initiator_type: session.initiatorType ?? \"user\",\n initiator:\n session.initiator != null ? initiatorToWire(session.initiator) : null,\n agent_id: session.agentId,\n content_scope: session.contentScope,\n manifest_ref: session.manifestRef,\n prior_session_ids: session.priorSessionIds ?? [],\n started_at: session.startedAt,\n ended_at: session.endedAt,\n user_context:\n session.userContext != null\n ? userContextToWire(session.userContext)\n : {},\n events: session.events.map(eventToWire),\n outcome:\n session.outcome != null ? outcomeToWire(session.outcome) : undefined,\n };\n}\n","/**\n * OpenAttribution Telemetry — MCP session tracker.\n *\n * Provides session continuity across stateless MCP tool calls.\n * The calling agent passes a stable `sessionId` string; this module\n * maps it to an OA session UUID and reuses it across tool calls within\n * the same server process.\n *\n * Usage in an MCP tool:\n *\n * ```ts\n * import { TelemetryClient, MCPSessionTracker } from \"@openattribution/telemetry\";\n *\n * const client = new TelemetryClient({ endpoint: \"...\", apiKey: \"...\" });\n * const tracker = new MCPSessionTracker(client, \"my-agent\");\n *\n * // In your MCP tool handler:\n * server.tool(\"search_products\", { query: z.string(), sessionId: z.string().optional() },\n * async ({ query, sessionId }) => {\n * const results = await searchProducts(query);\n * await tracker.trackRetrieved(sessionId, results.map(r => r.url));\n * return formatResults(results);\n * }\n * );\n * ```\n */\n\nimport type { TelemetryEvent } from \"./types.js\";\nimport type { TelemetryClient } from \"./client.js\";\n\n/**\n * Session tracker for MCP agents.\n *\n * Maintains an in-process mapping of external session IDs to OA session UUIDs.\n * Sessions are created on first use and reused across subsequent tool calls.\n *\n * The in-process registry is intentionally simple — it works correctly for\n * single-process MCP servers. For distributed deployments, pass a shared\n * external store to the constructor.\n */\nexport class MCPSessionTracker {\n private readonly client: TelemetryClient;\n private readonly contentScope: string;\n private readonly registry = new Map<string, string | null>();\n\n /**\n * @param client - Configured TelemetryClient instance.\n * @param contentScope - Stable identifier for this agent's content scope\n * (e.g. mix ID, manifest reference, or descriptive slug like \"my-shopping-agent\").\n */\n constructor(client: TelemetryClient, contentScope = \"mcp-agent\") {\n this.client = client;\n this.contentScope = contentScope;\n }\n\n /**\n * Get or create an OA session for the given external session ID.\n *\n * If `externalSessionId` is undefined, a new anonymous session is created\n * on every call (no continuity across tool calls).\n *\n * @returns OA session ID string, or null on silent failure.\n */\n async getOrCreateSession(\n externalSessionId: string | undefined,\n ): Promise<string | null> {\n if (externalSessionId != null && this.registry.has(externalSessionId)) {\n return this.registry.get(externalSessionId) ?? null;\n }\n\n const sessionId = await this.client.startSession({\n contentScope: this.contentScope,\n ...(externalSessionId != null && { externalSessionId }),\n });\n\n if (externalSessionId != null) {\n this.registry.set(externalSessionId, sessionId);\n }\n\n return sessionId;\n }\n\n /**\n * Emit `content_retrieved` events for a list of URLs.\n *\n * Call this after fetching products, search results, or any content\n * that influenced the agent's response.\n *\n * @param externalSessionId - Caller-supplied conversation identifier.\n * @param urls - URLs of content retrieved during this tool call.\n */\n async trackRetrieved(\n externalSessionId: string | undefined,\n urls: string[],\n ): Promise<void> {\n if (urls.length === 0) return;\n const sessionId = await this.getOrCreateSession(externalSessionId);\n if (sessionId == null) return;\n\n const now = new Date().toISOString();\n const events: TelemetryEvent[] = urls.map((url) => ({\n id: crypto.randomUUID(),\n type: \"content_retrieved\" as const,\n timestamp: now,\n contentUrl: url,\n }));\n\n await this.client.recordEvents(sessionId, events);\n }\n\n /**\n * Emit `content_cited` events for content explicitly referenced in a response.\n *\n * Call this when you know which content the agent cited — e.g. the top\n * search result, an editorial quote, or a product recommendation.\n *\n * @param externalSessionId - Caller-supplied conversation identifier.\n * @param urls - URLs of content cited in the agent's response.\n * @param options - Optional citation metadata.\n */\n async trackCited(\n externalSessionId: string | undefined,\n urls: string[],\n options: {\n citationType?: \"direct_quote\" | \"paraphrase\" | \"reference\" | \"contradiction\";\n position?: \"primary\" | \"supporting\" | \"mentioned\";\n } = {},\n ): Promise<void> {\n if (urls.length === 0) return;\n const sessionId = await this.getOrCreateSession(externalSessionId);\n if (sessionId == null) return;\n\n const now = new Date().toISOString();\n const events: TelemetryEvent[] = urls.map((url) => ({\n id: crypto.randomUUID(),\n type: \"content_cited\" as const,\n timestamp: now,\n contentUrl: url,\n data: {\n ...(options.citationType != null && { citation_type: options.citationType }),\n ...(options.position != null && { position: options.position }),\n },\n }));\n\n await this.client.recordEvents(sessionId, events);\n }\n\n /**\n * End a session with an outcome.\n *\n * Call this when the conversation concludes — at checkout, after\n * the user clicks a link, or when the session times out.\n */\n async endSession(\n externalSessionId: string | undefined,\n outcome: { type: \"conversion\" | \"abandonment\" | \"browse\"; valueAmount?: number; currency?: string },\n ): Promise<void> {\n const sessionId = externalSessionId != null\n ? (this.registry.get(externalSessionId) ?? null)\n : null;\n\n if (sessionId == null) return;\n\n await this.client.endSession(sessionId, {\n type: outcome.type,\n ...(outcome.valueAmount != null && { valueAmount: outcome.valueAmount }),\n ...(outcome.currency != null && { currency: outcome.currency }),\n });\n\n this.registry.delete(externalSessionId!);\n }\n\n /** Number of active sessions in the registry. */\n get sessionCount(): number {\n return this.registry.size;\n }\n}\n","/**\n * OpenAttribution Telemetry — content URL extraction utilities.\n *\n * Helpers for extracting content URLs from AI-generated text, so they\n * can be recorded as telemetry events without manual instrumentation.\n */\n\n/** Matches `[text](url)` — standard Markdown links. */\nconst MARKDOWN_LINK_RE = /\\[([^\\]]+)\\]\\((https?:\\/\\/[^)]+)\\)/g;\n\n/** Matches bare URLs starting with http/https. */\nconst BARE_URL_RE = /https?:\\/\\/[^\\s<>\"')\\]]+/g;\n\n/**\n * Extract all HTTP/HTTPS URLs from Markdown-formatted text.\n *\n * Finds URLs in two forms:\n * - Markdown links: `[anchor text](https://...)`\n * - Bare URLs: `https://...`\n *\n * Results are deduplicated. Useful for extracting citation URLs from\n * AI model responses that include web search results.\n *\n * @example\n * ```ts\n * const text = \"According to [Wirecutter](https://nytimes.com/wirecutter/reviews/...), ...\";\n * const urls = extractCitationUrls(text);\n * // [\"https://nytimes.com/wirecutter/reviews/...\"]\n * ```\n */\nexport function extractCitationUrls(text: string): string[] {\n const urls = new Set<string>();\n\n for (const match of text.matchAll(MARKDOWN_LINK_RE)) {\n const url = match[2];\n if (url != null) urls.add(cleanUrl(url));\n }\n\n // Only look for bare URLs if Markdown links didn't cover everything\n for (const match of text.matchAll(BARE_URL_RE)) {\n urls.add(cleanUrl(match[0]));\n }\n\n return [...urls];\n}\n\n/**\n * Extract URLs from a list of search result objects.\n *\n * Accepts any object with a `url` string property — works with\n * Channel3, Exa, Tavily, and most search API responses.\n *\n * @example\n * ```ts\n * const products = await channel3.search({ query: \"headphones\" });\n * const urls = extractResultUrls(products);\n * await tracker.trackRetrieved(sessionId, urls);\n * ```\n */\nexport function extractResultUrls(\n results: Array<{ url?: string | null; link?: string | null }>,\n): string[] {\n const urls: string[] = [];\n for (const r of results) {\n const url = r.url ?? r.link;\n if (url != null && url.startsWith(\"http\")) {\n urls.push(url);\n }\n }\n return [...new Set(urls)];\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/** Remove trailing punctuation that commonly attaches to bare URLs. */\nfunction cleanUrl(url: string): string {\n return url.replace(/[.,;:!?]+$/, \"\");\n}\n","/**\n * OpenAttribution Telemetry — ACP (Agentic Commerce Protocol) bridge.\n *\n * Converts a TelemetrySession into the `content_attribution` object\n * defined in the ACP RFC. Include this in a CheckoutSessionCreateRequest\n * or CheckoutSessionCompleteRequest.\n *\n * Reference: acp/rfc.content_attribution.md\n */\n\nimport type { TelemetrySession } from \"./types.js\";\n\nexport interface ContentAttributionRetrieved {\n content_url: string;\n timestamp: string;\n}\n\nexport interface ContentAttributionCited {\n content_url: string;\n timestamp: string;\n citation_type?: string;\n excerpt_tokens?: number;\n position?: string;\n content_hash?: string;\n}\n\nexport interface ConversationSummary {\n turn_count: number;\n topics: string[];\n}\n\n/**\n * ACP `content_attribution` object.\n * Include in CheckoutSessionCreateRequest.content_attribution.\n */\nexport interface ContentAttribution {\n content_scope?: string;\n content_retrieved: ContentAttributionRetrieved[];\n content_cited?: ContentAttributionCited[];\n conversation_summary?: ConversationSummary;\n}\n\n/**\n * Convert a TelemetrySession into an ACP `content_attribution` object.\n *\n * @example\n * ```ts\n * import { sessionToContentAttribution } from \"@openattribution/telemetry/acp\";\n *\n * const attribution = sessionToContentAttribution(session);\n *\n * // Include in ACP checkout request:\n * await acp.createCheckout({\n * cart: { ... },\n * content_attribution: attribution,\n * });\n * ```\n */\nexport function sessionToContentAttribution(\n session: TelemetrySession,\n): ContentAttribution {\n const retrieved: ContentAttributionRetrieved[] = session.events\n .filter((e) => e.type === \"content_retrieved\" && e.contentUrl != null)\n .map((e) => ({\n content_url: e.contentUrl!,\n timestamp: e.timestamp,\n }));\n\n const cited: ContentAttributionCited[] = session.events\n .filter((e) => e.type === \"content_cited\" && e.contentUrl != null)\n .map((e) => ({\n content_url: e.contentUrl!,\n timestamp: e.timestamp,\n ...(e.data?.[\"citation_type\"] != null && {\n citation_type: String(e.data[\"citation_type\"]),\n }),\n ...(e.data?.[\"excerpt_tokens\"] != null && {\n excerpt_tokens: Number(e.data[\"excerpt_tokens\"]),\n }),\n ...(e.data?.[\"position\"] != null && {\n position: String(e.data[\"position\"]),\n }),\n ...(e.data?.[\"content_hash\"] != null && {\n content_hash: String(e.data[\"content_hash\"]),\n }),\n }));\n\n const turnEvents = session.events.filter(\n (e) => e.type === \"turn_completed\" || e.type === \"turn_started\",\n );\n const turnCount = Math.max(\n session.events.filter((e) => e.type === \"turn_completed\").length,\n 1,\n );\n\n const topics = [\n ...new Set(\n session.events.flatMap((e) => e.turn?.topics ?? []),\n ),\n ];\n\n const result: ContentAttribution = {\n ...(session.contentScope != null && { content_scope: session.contentScope }),\n content_retrieved: retrieved,\n ...(cited.length > 0 && { content_cited: cited }),\n ...((turnEvents.length > 0 || topics.length > 0) && {\n conversation_summary: { turn_count: turnCount, topics },\n }),\n };\n\n return result;\n}\n","/**\n * OpenAttribution Telemetry — UCP (Universal Checkout Protocol) bridge.\n *\n * Converts a TelemetrySession into the UCP attribution extension object.\n *\n * Reference: ucp/EXTENSION.md\n */\n\nimport type { TelemetrySession } from \"./types.js\";\n\nexport interface UCPAttribution {\n content_scope?: string;\n prior_session_ids?: string[];\n content_retrieved: Array<{ content_url: string; timestamp: string }>;\n content_cited?: Array<{\n content_url: string;\n timestamp: string;\n citation_type?: string;\n position?: string;\n }>;\n conversation_summary?: {\n turn_count: number;\n topics: string[];\n };\n}\n\n/**\n * Convert a TelemetrySession into a UCP attribution extension object.\n *\n * @example\n * ```ts\n * import { sessionToAttribution } from \"@openattribution/telemetry/ucp\";\n *\n * const attribution = sessionToAttribution(session);\n *\n * // Include in UCP checkout:\n * await ucp.completeCheckout({\n * order: { ... },\n * extensions: { \"org.openattribution.telemetry\": attribution },\n * });\n * ```\n */\nexport function sessionToAttribution(session: TelemetrySession): UCPAttribution {\n const retrieved = session.events\n .filter((e) => e.type === \"content_retrieved\" && e.contentUrl != null)\n .map((e) => ({ content_url: e.contentUrl!, timestamp: e.timestamp }));\n\n const cited = session.events\n .filter((e) => e.type === \"content_cited\" && e.contentUrl != null)\n .map((e) => ({\n content_url: e.contentUrl!,\n timestamp: e.timestamp,\n ...(e.data?.[\"citation_type\"] != null && {\n citation_type: String(e.data[\"citation_type\"]),\n }),\n ...(e.data?.[\"position\"] != null && {\n position: String(e.data[\"position\"]),\n }),\n }));\n\n const turnCount = Math.max(\n session.events.filter((e) => e.type === \"turn_completed\").length,\n 1,\n );\n const topics = [...new Set(session.events.flatMap((e) => e.turn?.topics ?? []))];\n const priorIds = (session.priorSessionIds ?? []).filter(Boolean);\n\n return {\n ...(session.contentScope != null && { content_scope: session.contentScope }),\n ...(priorIds.length > 0 && { prior_session_ids: priorIds }),\n content_retrieved: retrieved,\n ...(cited.length > 0 && { content_cited: cited }),\n ...((topics.length > 0 || turnCount > 0) && {\n conversation_summary: { turn_count: turnCount, topics },\n }),\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/client.ts","../src/mcp.ts","../src/extract.ts","../src/acp.ts","../src/ucp.ts"],"sourcesContent":["/**\n * OpenAttribution Telemetry — HTTP client.\n *\n * Zero dependencies — uses native fetch (Node 18+, Deno, browsers, Edge).\n */\n\nimport type {\n ConversationTurn,\n EventType,\n Initiator,\n SessionOutcome,\n StartSessionOptions,\n TelemetryClientOptions,\n TelemetryEvent,\n TelemetrySession,\n UserContext,\n} from \"./types.js\";\n\nconst TRANSIENT_STATUS_CODES = new Set([429, 500, 502, 503, 504]);\n\n// ---------------------------------------------------------------------------\n// Wire format helpers (camelCase → snake_case for the JSON body)\n// ---------------------------------------------------------------------------\n\nfunction turnToWire(turn: ConversationTurn): Record<string, unknown> {\n return {\n privacy_level: turn.privacyLevel,\n query_text: turn.queryText,\n response_text: turn.responseText,\n query_intent: turn.queryIntent,\n response_type: turn.responseType,\n topics: turn.topics,\n content_urls_retrieved: turn.contentUrlsRetrieved,\n content_urls_cited: turn.contentUrlsCited,\n query_tokens: turn.queryTokens,\n response_tokens: turn.responseTokens,\n model_id: turn.modelId,\n };\n}\n\nfunction eventToWire(event: TelemetryEvent): Record<string, unknown> {\n return {\n id: event.id,\n type: event.type,\n timestamp: event.timestamp,\n content_url: event.contentUrl,\n product_id: event.productId,\n turn: event.turn != null ? turnToWire(event.turn) : undefined,\n data: event.data ?? {},\n };\n}\n\nfunction initiatorToWire(i: Initiator): Record<string, unknown> {\n return {\n agent_id: i.agentId,\n manifest_ref: i.manifestRef,\n operator_id: i.operatorId,\n };\n}\n\nfunction userContextToWire(uc: UserContext): Record<string, unknown> {\n return {\n external_id: uc.externalId,\n segments: uc.segments ?? [],\n attributes: uc.attributes ?? {},\n };\n}\n\nfunction outcomeToWire(o: SessionOutcome): Record<string, unknown> {\n return {\n type: o.type,\n value_amount: o.valueAmount ?? 0,\n currency: o.currency ?? \"USD\",\n products: o.products ?? [],\n metadata: o.metadata ?? {},\n };\n}\n\n// ---------------------------------------------------------------------------\n// TelemetryClient\n// ---------------------------------------------------------------------------\n\n/**\n * Async client for recording OpenAttribution telemetry.\n *\n * Works in Node.js ≥ 18, Deno, browsers, and Edge runtimes (Vercel, Cloudflare).\n *\n * @example\n * ```ts\n * const client = new TelemetryClient({\n * endpoint: \"https://telemetry.example.com\",\n * apiKey: \"your-api-key\",\n * failSilently: true,\n * });\n *\n * const sessionId = await client.startSession({ contentScope: \"my-mix\" });\n *\n * await client.recordEvents(sessionId, [\n * { id: crypto.randomUUID(), type: \"content_retrieved\",\n * timestamp: new Date().toISOString(), contentUrl: \"https://...\" }\n * ]);\n *\n * await client.endSession(sessionId, { type: \"browse\" });\n * ```\n */\nexport class TelemetryClient {\n private readonly endpoint: string;\n private readonly apiKey: string | undefined;\n private readonly failSilently: boolean;\n private readonly timeout: number;\n private readonly maxRetries: number;\n\n constructor(options: TelemetryClientOptions) {\n this.endpoint = options.endpoint.replace(/\\/$/, \"\");\n this.apiKey = options.apiKey;\n this.failSilently = options.failSilently ?? true;\n this.timeout = options.timeout ?? 30_000;\n this.maxRetries = options.maxRetries ?? 3;\n }\n\n private headers(): Record<string, string> {\n const h: Record<string, string> = { \"Content-Type\": \"application/json\" };\n if (this.apiKey != null) h[\"X-API-Key\"] = this.apiKey;\n return h;\n }\n\n private async post(path: string, body: unknown): Promise<unknown> {\n const url = `${this.endpoint}${path}`;\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= this.maxRetries; attempt++) {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const res = await fetch(url, {\n method: \"POST\",\n headers: this.headers(),\n body: JSON.stringify(body),\n signal: controller.signal,\n });\n\n if (TRANSIENT_STATUS_CODES.has(res.status) && attempt < this.maxRetries) {\n const wait = 2 ** attempt * 1000 + Math.random() * 500;\n await sleep(wait);\n continue;\n }\n\n if (!res.ok) {\n throw new Error(`HTTP ${res.status} ${res.statusText} from ${url}`);\n }\n\n return await res.json();\n } catch (err) {\n lastError = err;\n if (attempt < this.maxRetries && isTransientError(err)) {\n const wait = 2 ** attempt * 1000 + Math.random() * 500;\n await sleep(wait);\n } else {\n break;\n }\n } finally {\n clearTimeout(timer);\n }\n }\n\n if (this.failSilently) {\n return null;\n }\n throw lastError;\n }\n\n /**\n * Start a new telemetry session.\n *\n * @returns Session ID string, or null on silent failure.\n */\n async startSession(options: StartSessionOptions = {}): Promise<string | null> {\n const result = await this.post(\"/session/start\", {\n initiator_type: options.initiatorType ?? \"user\",\n initiator:\n options.initiator != null ? initiatorToWire(options.initiator) : null,\n content_scope: options.contentScope,\n agent_id: options.agentId,\n external_session_id: options.externalSessionId,\n user_context:\n options.userContext != null\n ? userContextToWire(options.userContext)\n : {},\n manifest_ref: options.manifestRef,\n prior_session_ids: options.priorSessionIds ?? [],\n }) as { session_id?: string } | null;\n\n return result?.session_id ?? null;\n }\n\n /**\n * Record a single telemetry event.\n */\n async recordEvent(\n sessionId: string | null,\n eventType: EventType,\n options: {\n contentUrl?: string;\n productId?: string;\n turn?: ConversationTurn;\n data?: Record<string, unknown>;\n } = {},\n ): Promise<void> {\n if (sessionId == null) return;\n await this.recordEvents(sessionId, [\n {\n id: crypto.randomUUID(),\n type: eventType,\n timestamp: new Date().toISOString(),\n ...options,\n },\n ]);\n }\n\n /**\n * Record a batch of telemetry events.\n */\n async recordEvents(\n sessionId: string | null,\n events: TelemetryEvent[],\n ): Promise<void> {\n if (sessionId == null || events.length === 0) return;\n await this.post(\"/events\", {\n session_id: sessionId,\n events: events.map(eventToWire),\n });\n }\n\n /**\n * End a session with an outcome.\n */\n async endSession(\n sessionId: string | null,\n outcome: SessionOutcome,\n ): Promise<void> {\n if (sessionId == null) return;\n await this.post(\"/session/end\", {\n session_id: sessionId,\n outcome: outcomeToWire(outcome),\n });\n }\n\n /**\n * Upload a complete session in one request (bulk path).\n *\n * Useful for post-hoc reporting or when you've built the session\n * locally and want to submit it in one shot.\n *\n * @returns Server-assigned session ID, or null on silent failure.\n */\n async uploadSession(session: TelemetrySession): Promise<string | null> {\n const result = await this.post(\"/session/bulk\", sessionToWire(session)) as\n | { session_id?: string }\n | null;\n return result?.session_id ?? null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction isTransientError(err: unknown): boolean {\n if (err instanceof Error) {\n // AbortError (timeout), network errors\n return (\n err.name === \"AbortError\" ||\n err.name === \"TypeError\" ||\n err.message.includes(\"fetch\")\n );\n }\n return false;\n}\n\nfunction sessionToWire(session: TelemetrySession): Record<string, unknown> {\n return {\n schema_version: session.schemaVersion ?? \"0.4\",\n session_id: session.sessionId,\n initiator_type: session.initiatorType ?? \"user\",\n initiator:\n session.initiator != null ? initiatorToWire(session.initiator) : null,\n agent_id: session.agentId,\n content_scope: session.contentScope,\n manifest_ref: session.manifestRef,\n prior_session_ids: session.priorSessionIds ?? [],\n started_at: session.startedAt,\n ended_at: session.endedAt,\n user_context:\n session.userContext != null\n ? userContextToWire(session.userContext)\n : {},\n events: session.events.map(eventToWire),\n outcome:\n session.outcome != null ? outcomeToWire(session.outcome) : undefined,\n };\n}\n","/**\n * OpenAttribution Telemetry — MCP session tracker.\n *\n * Provides session continuity across stateless MCP tool calls.\n * The calling agent passes a stable `sessionId` string; this module\n * maps it to an OA session UUID and reuses it across tool calls within\n * the same server process.\n *\n * Usage in an MCP tool:\n *\n * ```ts\n * import { TelemetryClient, MCPSessionTracker } from \"@openattribution/telemetry\";\n *\n * const client = new TelemetryClient({ endpoint: \"...\", apiKey: \"...\" });\n * const tracker = new MCPSessionTracker(client, \"my-agent\");\n *\n * // In your MCP tool handler:\n * server.tool(\"search_products\", { query: z.string(), sessionId: z.string().optional() },\n * async ({ query, sessionId }) => {\n * const results = await searchProducts(query);\n * await tracker.trackRetrieved(sessionId, results.map(r => r.url));\n * return formatResults(results);\n * }\n * );\n * ```\n */\n\nimport type { TelemetryEvent } from \"./types.js\";\nimport type { TelemetryClient } from \"./client.js\";\n\n/**\n * Session tracker for MCP agents.\n *\n * Maintains an in-process mapping of external session IDs to OA session UUIDs.\n * Sessions are created on first use and reused across subsequent tool calls.\n *\n * The in-process registry is intentionally simple — it works correctly for\n * single-process MCP servers. For distributed deployments, pass a shared\n * external store to the constructor.\n */\nexport class MCPSessionTracker {\n private readonly client: TelemetryClient;\n private readonly contentScope: string;\n private readonly registry = new Map<string, string | null>();\n\n /**\n * @param client - Configured TelemetryClient instance.\n * @param contentScope - Stable identifier for this agent's content scope\n * (e.g. mix ID, manifest reference, or descriptive slug like \"my-shopping-agent\").\n */\n constructor(client: TelemetryClient, contentScope = \"mcp-agent\") {\n this.client = client;\n this.contentScope = contentScope;\n }\n\n /**\n * Get or create an OA session for the given external session ID.\n *\n * If `externalSessionId` is undefined, a new anonymous session is created\n * on every call (no continuity across tool calls).\n *\n * @returns OA session ID string, or null on silent failure.\n */\n async getOrCreateSession(\n externalSessionId: string | undefined,\n ): Promise<string | null> {\n if (externalSessionId != null && this.registry.has(externalSessionId)) {\n return this.registry.get(externalSessionId) ?? null;\n }\n\n const sessionId = await this.client.startSession({\n contentScope: this.contentScope,\n ...(externalSessionId != null && { externalSessionId }),\n });\n\n if (externalSessionId != null) {\n this.registry.set(externalSessionId, sessionId);\n }\n\n return sessionId;\n }\n\n /**\n * Emit `content_retrieved` events for a list of URLs.\n *\n * Call this after fetching products, search results, or any content\n * that influenced the agent's response.\n *\n * @param externalSessionId - Caller-supplied conversation identifier.\n * @param urls - URLs of content retrieved during this tool call.\n */\n async trackRetrieved(\n externalSessionId: string | undefined,\n urls: string[],\n ): Promise<void> {\n if (urls.length === 0) return;\n const sessionId = await this.getOrCreateSession(externalSessionId);\n if (sessionId == null) return;\n\n const now = new Date().toISOString();\n const events: TelemetryEvent[] = urls.map((url) => ({\n id: crypto.randomUUID(),\n type: \"content_retrieved\" as const,\n timestamp: now,\n contentUrl: url,\n }));\n\n await this.client.recordEvents(sessionId, events);\n }\n\n /**\n * Emit `content_cited` events for content explicitly referenced in a response.\n *\n * Call this when you know which content the agent cited — e.g. the top\n * search result, an editorial quote, or a product recommendation.\n *\n * @param externalSessionId - Caller-supplied conversation identifier.\n * @param urls - URLs of content cited in the agent's response.\n * @param options - Optional citation metadata.\n */\n async trackCited(\n externalSessionId: string | undefined,\n urls: string[],\n options: {\n citationType?: \"direct_quote\" | \"paraphrase\" | \"reference\" | \"contradiction\";\n position?: \"primary\" | \"supporting\" | \"mentioned\";\n } = {},\n ): Promise<void> {\n if (urls.length === 0) return;\n const sessionId = await this.getOrCreateSession(externalSessionId);\n if (sessionId == null) return;\n\n const now = new Date().toISOString();\n const events: TelemetryEvent[] = urls.map((url) => ({\n id: crypto.randomUUID(),\n type: \"content_cited\" as const,\n timestamp: now,\n contentUrl: url,\n data: {\n ...(options.citationType != null && { citation_type: options.citationType }),\n ...(options.position != null && { position: options.position }),\n },\n }));\n\n await this.client.recordEvents(sessionId, events);\n }\n\n /**\n * Emit `content_engaged` events when a user interacts with content.\n *\n * Call this when a user clicks a link, views an embedded product, or\n * otherwise actively engages with retrieved content. This is the\n * strongest attribution signal before a purchase event.\n *\n * @param externalSessionId - Caller-supplied conversation identifier.\n * @param urls - URLs the user engaged with.\n * @param options - Optional engagement metadata.\n *\n * @example\n * ```ts\n * // In a redirect/tracking endpoint\n * await tracker.trackEngaged(sessionId, [productUrl], {\n * interactionType: \"click\",\n * });\n * ```\n */\n async trackEngaged(\n externalSessionId: string | undefined,\n urls: string[],\n options: {\n interactionType?: \"click\" | \"view\" | \"expand\" | \"share\";\n } = {},\n ): Promise<void> {\n if (urls.length === 0) return;\n const sessionId = await this.getOrCreateSession(externalSessionId);\n if (sessionId == null) return;\n\n const now = new Date().toISOString();\n const events: TelemetryEvent[] = urls.map((url) => ({\n id: crypto.randomUUID(),\n type: \"content_engaged\" as const,\n timestamp: now,\n contentUrl: url,\n data: {\n ...(options.interactionType != null && {\n interaction_type: options.interactionType,\n }),\n },\n }));\n\n await this.client.recordEvents(sessionId, events);\n }\n\n /**\n * Record a checkout outcome and end the session.\n *\n * Call this when a user completes a purchase, abandons checkout, or\n * the conversation concludes with a clear commerce outcome.\n * Emits a `checkout_completed` or `checkout_abandoned` event then\n * ends the session with the appropriate outcome type.\n *\n * @param externalSessionId - Caller-supplied conversation identifier.\n * @param outcome - Purchase details.\n *\n * @example\n * ```ts\n * // User completed a purchase\n * await tracker.trackCheckout(sessionId, {\n * type: \"completed\",\n * valueAmount: 4999, // $49.99 in minor units (cents)\n * currency: \"USD\",\n * });\n *\n * // User abandoned checkout\n * await tracker.trackCheckout(sessionId, { type: \"abandoned\" });\n * ```\n */\n async trackCheckout(\n externalSessionId: string | undefined,\n outcome: {\n type: \"completed\" | \"abandoned\" | \"started\";\n valueAmount?: number;\n currency?: string;\n products?: string[];\n },\n ): Promise<void> {\n const sessionId = externalSessionId != null\n ? (this.registry.get(externalSessionId) ?? null)\n : null;\n\n if (sessionId == null) return;\n\n // Emit the checkout event\n const eventType = outcome.type === \"completed\"\n ? \"checkout_completed\" as const\n : outcome.type === \"abandoned\"\n ? \"checkout_abandoned\" as const\n : \"checkout_started\" as const;\n\n await this.client.recordEvent(sessionId, eventType);\n\n // End the session with an outcome for completed/abandoned\n if (outcome.type === \"completed\" || outcome.type === \"abandoned\") {\n const outcomeType = outcome.type === \"completed\" ? \"conversion\" as const : \"abandonment\" as const;\n await this.client.endSession(sessionId, {\n type: outcomeType,\n ...(outcome.valueAmount != null && { valueAmount: outcome.valueAmount }),\n ...(outcome.currency != null && { currency: outcome.currency }),\n ...(outcome.products != null && { products: outcome.products }),\n });\n if (externalSessionId != null) {\n this.registry.delete(externalSessionId);\n }\n }\n }\n\n /**\n * End a session with an explicit outcome.\n *\n * Use `trackCheckout` for commerce outcomes. Use this for non-commerce\n * session endings (browse sessions, timeouts, explicit abandonment).\n */\n async endSession(\n externalSessionId: string | undefined,\n outcome: { type: \"conversion\" | \"abandonment\" | \"browse\"; valueAmount?: number; currency?: string },\n ): Promise<void> {\n const sessionId = externalSessionId != null\n ? (this.registry.get(externalSessionId) ?? null)\n : null;\n\n if (sessionId == null) return;\n\n await this.client.endSession(sessionId, {\n type: outcome.type,\n ...(outcome.valueAmount != null && { valueAmount: outcome.valueAmount }),\n ...(outcome.currency != null && { currency: outcome.currency }),\n });\n\n this.registry.delete(externalSessionId!);\n }\n\n /** Number of active sessions in the registry. */\n get sessionCount(): number {\n return this.registry.size;\n }\n}\n","/**\n * OpenAttribution Telemetry — content URL extraction utilities.\n *\n * Helpers for extracting content URLs from AI-generated text, so they\n * can be recorded as telemetry events without manual instrumentation.\n */\n\n/** Matches `[text](url)` — standard Markdown links. */\nconst MARKDOWN_LINK_RE = /\\[([^\\]]+)\\]\\((https?:\\/\\/[^)]+)\\)/g;\n\n/** Matches bare URLs starting with http/https. */\nconst BARE_URL_RE = /https?:\\/\\/[^\\s<>\"')\\]]+/g;\n\n/**\n * Extract all HTTP/HTTPS URLs from Markdown-formatted text.\n *\n * Finds URLs in two forms:\n * - Markdown links: `[anchor text](https://...)`\n * - Bare URLs: `https://...`\n *\n * Results are deduplicated. Useful for extracting citation URLs from\n * AI model responses that include web search results.\n *\n * @example\n * ```ts\n * const text = \"According to [Wirecutter](https://nytimes.com/wirecutter/reviews/...), ...\";\n * const urls = extractCitationUrls(text);\n * // [\"https://nytimes.com/wirecutter/reviews/...\"]\n * ```\n */\nexport function extractCitationUrls(text: string): string[] {\n const urls = new Set<string>();\n\n for (const match of text.matchAll(MARKDOWN_LINK_RE)) {\n const url = match[2];\n if (url != null) urls.add(cleanUrl(url));\n }\n\n // Only look for bare URLs if Markdown links didn't cover everything\n for (const match of text.matchAll(BARE_URL_RE)) {\n urls.add(cleanUrl(match[0]));\n }\n\n return [...urls];\n}\n\n/**\n * Extract URLs from a list of search result objects.\n *\n * Accepts any object with a `url` string property — works with\n * Channel3, Exa, Tavily, and most search API responses.\n *\n * @example\n * ```ts\n * const products = await channel3.search({ query: \"headphones\" });\n * const urls = extractResultUrls(products);\n * await tracker.trackRetrieved(sessionId, urls);\n * ```\n */\nexport function extractResultUrls(\n results: Array<{ url?: string | null; link?: string | null }>,\n): string[] {\n const urls: string[] = [];\n for (const r of results) {\n const url = r.url ?? r.link;\n if (url != null && url.startsWith(\"http\")) {\n urls.push(url);\n }\n }\n return [...new Set(urls)];\n}\n\n/**\n * Build a tracking URL that routes through your server-side redirect endpoint.\n *\n * Your endpoint emits a `content_engaged` event then redirects the user to\n * the original URL. This is the most reliable pattern for click tracking —\n * it fires server-side before the redirect, so it works regardless of\n * browser JS state or navigation timing.\n *\n * @param contentUrl - The destination URL to redirect to after tracking.\n * @param options.endpoint - Your tracking endpoint base URL (e.g. `https://example.com/api/track`).\n * @param options.sessionId - Optional session ID to link the click to an existing session.\n *\n * @example\n * ```ts\n * // Build a tracked URL for a product link\n * const trackedUrl = createTrackingUrl(\"https://shop.example.com/product\", {\n * endpoint: \"https://myagent.com/api/track\",\n * sessionId: \"conv-abc123\",\n * });\n * // → \"https://myagent.com/api/track?url=https%3A%2F%2Fshop.example.com%2Fproduct&session_id=conv-abc123\"\n *\n * // Server-side handler (Next.js example):\n * export async function GET(req: Request) {\n * const { searchParams } = new URL(req.url);\n * const url = searchParams.get(\"url\");\n * const sessionId = searchParams.get(\"session_id\") ?? undefined;\n * if (!url) return new Response(\"Missing url\", { status: 400 });\n * void tracker.trackEngaged(sessionId, [url], { interactionType: \"click\" });\n * return Response.redirect(url, 302);\n * }\n * ```\n */\nexport function createTrackingUrl(\n contentUrl: string,\n options: {\n endpoint: string;\n sessionId?: string;\n },\n): string {\n const url = new URL(options.endpoint);\n url.searchParams.set(\"url\", contentUrl);\n if (options.sessionId != null) {\n url.searchParams.set(\"session_id\", options.sessionId);\n }\n return url.toString();\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/** Remove trailing punctuation that commonly attaches to bare URLs. */\nfunction cleanUrl(url: string): string {\n return url.replace(/[.,;:!?]+$/, \"\");\n}\n","/**\n * OpenAttribution Telemetry — ACP (Agentic Commerce Protocol) bridge.\n *\n * Converts a TelemetrySession into the `content_attribution` object\n * defined in the ACP RFC. Include this in a CheckoutSessionCreateRequest\n * or CheckoutSessionCompleteRequest.\n *\n * Reference: acp/rfc.content_attribution.md\n */\n\nimport type { TelemetrySession } from \"./types.js\";\n\nexport interface ContentAttributionRetrieved {\n content_url: string;\n timestamp: string;\n}\n\nexport interface ContentAttributionCited {\n content_url: string;\n timestamp: string;\n citation_type?: string;\n excerpt_tokens?: number;\n position?: string;\n content_hash?: string;\n}\n\nexport interface ConversationSummary {\n turn_count: number;\n topics: string[];\n}\n\n/**\n * ACP `content_attribution` object.\n * Include in CheckoutSessionCreateRequest.content_attribution.\n */\nexport interface ContentAttribution {\n content_scope?: string;\n content_retrieved: ContentAttributionRetrieved[];\n content_cited?: ContentAttributionCited[];\n conversation_summary?: ConversationSummary;\n}\n\n/**\n * Convert a TelemetrySession into an ACP `content_attribution` object.\n *\n * @example\n * ```ts\n * import { sessionToContentAttribution } from \"@openattribution/telemetry/acp\";\n *\n * const attribution = sessionToContentAttribution(session);\n *\n * // Include in ACP checkout request:\n * await acp.createCheckout({\n * cart: { ... },\n * content_attribution: attribution,\n * });\n * ```\n */\nexport function sessionToContentAttribution(\n session: TelemetrySession,\n): ContentAttribution {\n const retrieved: ContentAttributionRetrieved[] = session.events\n .filter((e) => e.type === \"content_retrieved\" && e.contentUrl != null)\n .map((e) => ({\n content_url: e.contentUrl!,\n timestamp: e.timestamp,\n }));\n\n const cited: ContentAttributionCited[] = session.events\n .filter((e) => e.type === \"content_cited\" && e.contentUrl != null)\n .map((e) => ({\n content_url: e.contentUrl!,\n timestamp: e.timestamp,\n ...(e.data?.[\"citation_type\"] != null && {\n citation_type: String(e.data[\"citation_type\"]),\n }),\n ...(e.data?.[\"excerpt_tokens\"] != null && {\n excerpt_tokens: Number(e.data[\"excerpt_tokens\"]),\n }),\n ...(e.data?.[\"position\"] != null && {\n position: String(e.data[\"position\"]),\n }),\n ...(e.data?.[\"content_hash\"] != null && {\n content_hash: String(e.data[\"content_hash\"]),\n }),\n }));\n\n const turnEvents = session.events.filter(\n (e) => e.type === \"turn_completed\" || e.type === \"turn_started\",\n );\n const turnCount = Math.max(\n session.events.filter((e) => e.type === \"turn_completed\").length,\n 1,\n );\n\n const topics = [\n ...new Set(\n session.events.flatMap((e) => e.turn?.topics ?? []),\n ),\n ];\n\n const result: ContentAttribution = {\n ...(session.contentScope != null && { content_scope: session.contentScope }),\n content_retrieved: retrieved,\n ...(cited.length > 0 && { content_cited: cited }),\n ...((turnEvents.length > 0 || topics.length > 0) && {\n conversation_summary: { turn_count: turnCount, topics },\n }),\n };\n\n return result;\n}\n","/**\n * OpenAttribution Telemetry — UCP (Universal Checkout Protocol) bridge.\n *\n * Converts a TelemetrySession into the UCP attribution extension object.\n *\n * Reference: ucp/EXTENSION.md\n */\n\nimport type { TelemetrySession } from \"./types.js\";\n\nexport interface UCPAttribution {\n content_scope?: string;\n prior_session_ids?: string[];\n content_retrieved: Array<{ content_url: string; timestamp: string }>;\n content_cited?: Array<{\n content_url: string;\n timestamp: string;\n citation_type?: string;\n position?: string;\n }>;\n conversation_summary?: {\n turn_count: number;\n topics: string[];\n };\n}\n\n/**\n * Convert a TelemetrySession into a UCP attribution extension object.\n *\n * @example\n * ```ts\n * import { sessionToAttribution } from \"@openattribution/telemetry/ucp\";\n *\n * const attribution = sessionToAttribution(session);\n *\n * // Include in UCP checkout:\n * await ucp.completeCheckout({\n * order: { ... },\n * extensions: { \"org.openattribution.telemetry\": attribution },\n * });\n * ```\n */\nexport function sessionToAttribution(session: TelemetrySession): UCPAttribution {\n const retrieved = session.events\n .filter((e) => e.type === \"content_retrieved\" && e.contentUrl != null)\n .map((e) => ({ content_url: e.contentUrl!, timestamp: e.timestamp }));\n\n const cited = session.events\n .filter((e) => e.type === \"content_cited\" && e.contentUrl != null)\n .map((e) => ({\n content_url: e.contentUrl!,\n timestamp: e.timestamp,\n ...(e.data?.[\"citation_type\"] != null && {\n citation_type: String(e.data[\"citation_type\"]),\n }),\n ...(e.data?.[\"position\"] != null && {\n position: String(e.data[\"position\"]),\n }),\n }));\n\n const turnCount = Math.max(\n session.events.filter((e) => e.type === \"turn_completed\").length,\n 1,\n );\n const topics = [...new Set(session.events.flatMap((e) => e.turn?.topics ?? []))];\n const priorIds = (session.priorSessionIds ?? []).filter(Boolean);\n\n return {\n ...(session.contentScope != null && { content_scope: session.contentScope }),\n ...(priorIds.length > 0 && { prior_session_ids: priorIds }),\n content_retrieved: retrieved,\n ...(cited.length > 0 && { content_cited: cited }),\n ...((topics.length > 0 || turnCount > 0) && {\n conversation_summary: { turn_count: turnCount, topics },\n }),\n };\n}\n"],"mappings":";AAkBA,IAAM,yBAAyB,oBAAI,IAAI,CAAC,KAAK,KAAK,KAAK,KAAK,GAAG,CAAC;AAMhE,SAAS,WAAW,MAAiD;AACnE,SAAO;AAAA,IACL,eAAe,KAAK;AAAA,IACpB,YAAY,KAAK;AAAA,IACjB,eAAe,KAAK;AAAA,IACpB,cAAc,KAAK;AAAA,IACnB,eAAe,KAAK;AAAA,IACpB,QAAQ,KAAK;AAAA,IACb,wBAAwB,KAAK;AAAA,IAC7B,oBAAoB,KAAK;AAAA,IACzB,cAAc,KAAK;AAAA,IACnB,iBAAiB,KAAK;AAAA,IACtB,UAAU,KAAK;AAAA,EACjB;AACF;AAEA,SAAS,YAAY,OAAgD;AACnE,SAAO;AAAA,IACL,IAAI,MAAM;AAAA,IACV,MAAM,MAAM;AAAA,IACZ,WAAW,MAAM;AAAA,IACjB,aAAa,MAAM;AAAA,IACnB,YAAY,MAAM;AAAA,IAClB,MAAM,MAAM,QAAQ,OAAO,WAAW,MAAM,IAAI,IAAI;AAAA,IACpD,MAAM,MAAM,QAAQ,CAAC;AAAA,EACvB;AACF;AAEA,SAAS,gBAAgB,GAAuC;AAC9D,SAAO;AAAA,IACL,UAAU,EAAE;AAAA,IACZ,cAAc,EAAE;AAAA,IAChB,aAAa,EAAE;AAAA,EACjB;AACF;AAEA,SAAS,kBAAkB,IAA0C;AACnE,SAAO;AAAA,IACL,aAAa,GAAG;AAAA,IAChB,UAAU,GAAG,YAAY,CAAC;AAAA,IAC1B,YAAY,GAAG,cAAc,CAAC;AAAA,EAChC;AACF;AAEA,SAAS,cAAc,GAA4C;AACjE,SAAO;AAAA,IACL,MAAM,EAAE;AAAA,IACR,cAAc,EAAE,eAAe;AAAA,IAC/B,UAAU,EAAE,YAAY;AAAA,IACxB,UAAU,EAAE,YAAY,CAAC;AAAA,IACzB,UAAU,EAAE,YAAY,CAAC;AAAA,EAC3B;AACF;AA6BO,IAAM,kBAAN,MAAsB;AAAA,EAO3B,YAAY,SAAiC;AAC3C,SAAK,WAAW,QAAQ,SAAS,QAAQ,OAAO,EAAE;AAClD,SAAK,SAAS,QAAQ;AACtB,SAAK,eAAe,QAAQ,gBAAgB;AAC5C,SAAK,UAAU,QAAQ,WAAW;AAClC,SAAK,aAAa,QAAQ,cAAc;AAAA,EAC1C;AAAA,EAEQ,UAAkC;AACxC,UAAM,IAA4B,EAAE,gBAAgB,mBAAmB;AACvE,QAAI,KAAK,UAAU,KAAM,GAAE,WAAW,IAAI,KAAK;AAC/C,WAAO;AAAA,EACT;AAAA,EAEA,MAAc,KAAK,MAAc,MAAiC;AAChE,UAAM,MAAM,GAAG,KAAK,QAAQ,GAAG,IAAI;AACnC,QAAI;AAEJ,aAAS,UAAU,GAAG,WAAW,KAAK,YAAY,WAAW;AAC3D,YAAM,aAAa,IAAI,gBAAgB;AACvC,YAAM,QAAQ,WAAW,MAAM,WAAW,MAAM,GAAG,KAAK,OAAO;AAE/D,UAAI;AACF,cAAM,MAAM,MAAM,MAAM,KAAK;AAAA,UAC3B,QAAQ;AAAA,UACR,SAAS,KAAK,QAAQ;AAAA,UACtB,MAAM,KAAK,UAAU,IAAI;AAAA,UACzB,QAAQ,WAAW;AAAA,QACrB,CAAC;AAED,YAAI,uBAAuB,IAAI,IAAI,MAAM,KAAK,UAAU,KAAK,YAAY;AACvE,gBAAM,OAAO,KAAK,UAAU,MAAO,KAAK,OAAO,IAAI;AACnD,gBAAM,MAAM,IAAI;AAChB;AAAA,QACF;AAEA,YAAI,CAAC,IAAI,IAAI;AACX,gBAAM,IAAI,MAAM,QAAQ,IAAI,MAAM,IAAI,IAAI,UAAU,SAAS,GAAG,EAAE;AAAA,QACpE;AAEA,eAAO,MAAM,IAAI,KAAK;AAAA,MACxB,SAAS,KAAK;AACZ,oBAAY;AACZ,YAAI,UAAU,KAAK,cAAc,iBAAiB,GAAG,GAAG;AACtD,gBAAM,OAAO,KAAK,UAAU,MAAO,KAAK,OAAO,IAAI;AACnD,gBAAM,MAAM,IAAI;AAAA,QAClB,OAAO;AACL;AAAA,QACF;AAAA,MACF,UAAE;AACA,qBAAa,KAAK;AAAA,MACpB;AAAA,IACF;AAEA,QAAI,KAAK,cAAc;AACrB,aAAO;AAAA,IACT;AACA,UAAM;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,aAAa,UAA+B,CAAC,GAA2B;AAC5E,UAAM,SAAS,MAAM,KAAK,KAAK,kBAAkB;AAAA,MAC/C,gBAAgB,QAAQ,iBAAiB;AAAA,MACzC,WACE,QAAQ,aAAa,OAAO,gBAAgB,QAAQ,SAAS,IAAI;AAAA,MACnE,eAAe,QAAQ;AAAA,MACvB,UAAU,QAAQ;AAAA,MAClB,qBAAqB,QAAQ;AAAA,MAC7B,cACE,QAAQ,eAAe,OACnB,kBAAkB,QAAQ,WAAW,IACrC,CAAC;AAAA,MACP,cAAc,QAAQ;AAAA,MACtB,mBAAmB,QAAQ,mBAAmB,CAAC;AAAA,IACjD,CAAC;AAED,WAAO,QAAQ,cAAc;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,YACJ,WACA,WACA,UAKI,CAAC,GACU;AACf,QAAI,aAAa,KAAM;AACvB,UAAM,KAAK,aAAa,WAAW;AAAA,MACjC;AAAA,QACE,IAAI,OAAO,WAAW;AAAA,QACtB,MAAM;AAAA,QACN,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,QAClC,GAAG;AAAA,MACL;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,aACJ,WACA,QACe;AACf,QAAI,aAAa,QAAQ,OAAO,WAAW,EAAG;AAC9C,UAAM,KAAK,KAAK,WAAW;AAAA,MACzB,YAAY;AAAA,MACZ,QAAQ,OAAO,IAAI,WAAW;AAAA,IAChC,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,WACJ,WACA,SACe;AACf,QAAI,aAAa,KAAM;AACvB,UAAM,KAAK,KAAK,gBAAgB;AAAA,MAC9B,YAAY;AAAA,MACZ,SAAS,cAAc,OAAO;AAAA,IAChC,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,cAAc,SAAmD;AACrE,UAAM,SAAS,MAAM,KAAK,KAAK,iBAAiB,cAAc,OAAO,CAAC;AAGtE,WAAO,QAAQ,cAAc;AAAA,EAC/B;AACF;AAMA,SAAS,MAAM,IAA2B;AACxC,SAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AACzD;AAEA,SAAS,iBAAiB,KAAuB;AAC/C,MAAI,eAAe,OAAO;AAExB,WACE,IAAI,SAAS,gBACb,IAAI,SAAS,eACb,IAAI,QAAQ,SAAS,OAAO;AAAA,EAEhC;AACA,SAAO;AACT;AAEA,SAAS,cAAc,SAAoD;AACzE,SAAO;AAAA,IACL,gBAAgB,QAAQ,iBAAiB;AAAA,IACzC,YAAY,QAAQ;AAAA,IACpB,gBAAgB,QAAQ,iBAAiB;AAAA,IACzC,WACE,QAAQ,aAAa,OAAO,gBAAgB,QAAQ,SAAS,IAAI;AAAA,IACnE,UAAU,QAAQ;AAAA,IAClB,eAAe,QAAQ;AAAA,IACvB,cAAc,QAAQ;AAAA,IACtB,mBAAmB,QAAQ,mBAAmB,CAAC;AAAA,IAC/C,YAAY,QAAQ;AAAA,IACpB,UAAU,QAAQ;AAAA,IAClB,cACE,QAAQ,eAAe,OACnB,kBAAkB,QAAQ,WAAW,IACrC,CAAC;AAAA,IACP,QAAQ,QAAQ,OAAO,IAAI,WAAW;AAAA,IACtC,SACE,QAAQ,WAAW,OAAO,cAAc,QAAQ,OAAO,IAAI;AAAA,EAC/D;AACF;;;ACzQO,IAAM,oBAAN,MAAwB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAU7B,YAAY,QAAyB,eAAe,aAAa;AAPjE,SAAiB,WAAW,oBAAI,IAA2B;AAQzD,SAAK,SAAS;AACd,SAAK,eAAe;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,mBACJ,mBACwB;AACxB,QAAI,qBAAqB,QAAQ,KAAK,SAAS,IAAI,iBAAiB,GAAG;AACrE,aAAO,KAAK,SAAS,IAAI,iBAAiB,KAAK;AAAA,IACjD;AAEA,UAAM,YAAY,MAAM,KAAK,OAAO,aAAa;AAAA,MAC/C,cAAc,KAAK;AAAA,MACnB,GAAI,qBAAqB,QAAQ,EAAE,kBAAkB;AAAA,IACvD,CAAC;AAED,QAAI,qBAAqB,MAAM;AAC7B,WAAK,SAAS,IAAI,mBAAmB,SAAS;AAAA,IAChD;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,eACJ,mBACA,MACe;AACf,QAAI,KAAK,WAAW,EAAG;AACvB,UAAM,YAAY,MAAM,KAAK,mBAAmB,iBAAiB;AACjE,QAAI,aAAa,KAAM;AAEvB,UAAM,OAAM,oBAAI,KAAK,GAAE,YAAY;AACnC,UAAM,SAA2B,KAAK,IAAI,CAAC,SAAS;AAAA,MAClD,IAAI,OAAO,WAAW;AAAA,MACtB,MAAM;AAAA,MACN,WAAW;AAAA,MACX,YAAY;AAAA,IACd,EAAE;AAEF,UAAM,KAAK,OAAO,aAAa,WAAW,MAAM;AAAA,EAClD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAM,WACJ,mBACA,MACA,UAGI,CAAC,GACU;AACf,QAAI,KAAK,WAAW,EAAG;AACvB,UAAM,YAAY,MAAM,KAAK,mBAAmB,iBAAiB;AACjE,QAAI,aAAa,KAAM;AAEvB,UAAM,OAAM,oBAAI,KAAK,GAAE,YAAY;AACnC,UAAM,SAA2B,KAAK,IAAI,CAAC,SAAS;AAAA,MAClD,IAAI,OAAO,WAAW;AAAA,MACtB,MAAM;AAAA,MACN,WAAW;AAAA,MACX,YAAY;AAAA,MACZ,MAAM;AAAA,QACJ,GAAI,QAAQ,gBAAgB,QAAQ,EAAE,eAAe,QAAQ,aAAa;AAAA,QAC1E,GAAI,QAAQ,YAAY,QAAQ,EAAE,UAAU,QAAQ,SAAS;AAAA,MAC/D;AAAA,IACF,EAAE;AAEF,UAAM,KAAK,OAAO,aAAa,WAAW,MAAM;AAAA,EAClD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAqBA,MAAM,aACJ,mBACA,MACA,UAEI,CAAC,GACU;AACf,QAAI,KAAK,WAAW,EAAG;AACvB,UAAM,YAAY,MAAM,KAAK,mBAAmB,iBAAiB;AACjE,QAAI,aAAa,KAAM;AAEvB,UAAM,OAAM,oBAAI,KAAK,GAAE,YAAY;AACnC,UAAM,SAA2B,KAAK,IAAI,CAAC,SAAS;AAAA,MAClD,IAAI,OAAO,WAAW;AAAA,MACtB,MAAM;AAAA,MACN,WAAW;AAAA,MACX,YAAY;AAAA,MACZ,MAAM;AAAA,QACJ,GAAI,QAAQ,mBAAmB,QAAQ;AAAA,UACrC,kBAAkB,QAAQ;AAAA,QAC5B;AAAA,MACF;AAAA,IACF,EAAE;AAEF,UAAM,KAAK,OAAO,aAAa,WAAW,MAAM;AAAA,EAClD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA0BA,MAAM,cACJ,mBACA,SAMe;AACf,UAAM,YAAY,qBAAqB,OAClC,KAAK,SAAS,IAAI,iBAAiB,KAAK,OACzC;AAEJ,QAAI,aAAa,KAAM;AAGvB,UAAM,YAAY,QAAQ,SAAS,cAC/B,uBACA,QAAQ,SAAS,cACjB,uBACA;AAEJ,UAAM,KAAK,OAAO,YAAY,WAAW,SAAS;AAGlD,QAAI,QAAQ,SAAS,eAAe,QAAQ,SAAS,aAAa;AAChE,YAAM,cAAc,QAAQ,SAAS,cAAc,eAAwB;AAC3E,YAAM,KAAK,OAAO,WAAW,WAAW;AAAA,QACtC,MAAM;AAAA,QACN,GAAI,QAAQ,eAAe,QAAQ,EAAE,aAAa,QAAQ,YAAY;AAAA,QACtE,GAAI,QAAQ,YAAY,QAAQ,EAAE,UAAU,QAAQ,SAAS;AAAA,QAC7D,GAAI,QAAQ,YAAY,QAAQ,EAAE,UAAU,QAAQ,SAAS;AAAA,MAC/D,CAAC;AACD,UAAI,qBAAqB,MAAM;AAC7B,aAAK,SAAS,OAAO,iBAAiB;AAAA,MACxC;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,WACJ,mBACA,SACe;AACf,UAAM,YAAY,qBAAqB,OAClC,KAAK,SAAS,IAAI,iBAAiB,KAAK,OACzC;AAEJ,QAAI,aAAa,KAAM;AAEvB,UAAM,KAAK,OAAO,WAAW,WAAW;AAAA,MACtC,MAAM,QAAQ;AAAA,MACd,GAAI,QAAQ,eAAe,QAAQ,EAAE,aAAa,QAAQ,YAAY;AAAA,MACtE,GAAI,QAAQ,YAAY,QAAQ,EAAE,UAAU,QAAQ,SAAS;AAAA,IAC/D,CAAC;AAED,SAAK,SAAS,OAAO,iBAAkB;AAAA,EACzC;AAAA;AAAA,EAGA,IAAI,eAAuB;AACzB,WAAO,KAAK,SAAS;AAAA,EACvB;AACF;;;ACrRA,IAAM,mBAAmB;AAGzB,IAAM,cAAc;AAmBb,SAAS,oBAAoB,MAAwB;AAC1D,QAAM,OAAO,oBAAI,IAAY;AAE7B,aAAW,SAAS,KAAK,SAAS,gBAAgB,GAAG;AACnD,UAAM,MAAM,MAAM,CAAC;AACnB,QAAI,OAAO,KAAM,MAAK,IAAI,SAAS,GAAG,CAAC;AAAA,EACzC;AAGA,aAAW,SAAS,KAAK,SAAS,WAAW,GAAG;AAC9C,SAAK,IAAI,SAAS,MAAM,CAAC,CAAC,CAAC;AAAA,EAC7B;AAEA,SAAO,CAAC,GAAG,IAAI;AACjB;AAeO,SAAS,kBACd,SACU;AACV,QAAM,OAAiB,CAAC;AACxB,aAAW,KAAK,SAAS;AACvB,UAAM,MAAM,EAAE,OAAO,EAAE;AACvB,QAAI,OAAO,QAAQ,IAAI,WAAW,MAAM,GAAG;AACzC,WAAK,KAAK,GAAG;AAAA,IACf;AAAA,EACF;AACA,SAAO,CAAC,GAAG,IAAI,IAAI,IAAI,CAAC;AAC1B;AAkCO,SAAS,kBACd,YACA,SAIQ;AACR,QAAM,MAAM,IAAI,IAAI,QAAQ,QAAQ;AACpC,MAAI,aAAa,IAAI,OAAO,UAAU;AACtC,MAAI,QAAQ,aAAa,MAAM;AAC7B,QAAI,aAAa,IAAI,cAAc,QAAQ,SAAS;AAAA,EACtD;AACA,SAAO,IAAI,SAAS;AACtB;AAOA,SAAS,SAAS,KAAqB;AACrC,SAAO,IAAI,QAAQ,cAAc,EAAE;AACrC;;;ACpEO,SAAS,4BACd,SACoB;AACpB,QAAM,YAA2C,QAAQ,OACtD,OAAO,CAAC,MAAM,EAAE,SAAS,uBAAuB,EAAE,cAAc,IAAI,EACpE,IAAI,CAAC,OAAO;AAAA,IACX,aAAa,EAAE;AAAA,IACf,WAAW,EAAE;AAAA,EACf,EAAE;AAEJ,QAAM,QAAmC,QAAQ,OAC9C,OAAO,CAAC,MAAM,EAAE,SAAS,mBAAmB,EAAE,cAAc,IAAI,EAChE,IAAI,CAAC,OAAO;AAAA,IACX,aAAa,EAAE;AAAA,IACf,WAAW,EAAE;AAAA,IACb,GAAI,EAAE,OAAO,eAAe,KAAK,QAAQ;AAAA,MACvC,eAAe,OAAO,EAAE,KAAK,eAAe,CAAC;AAAA,IAC/C;AAAA,IACA,GAAI,EAAE,OAAO,gBAAgB,KAAK,QAAQ;AAAA,MACxC,gBAAgB,OAAO,EAAE,KAAK,gBAAgB,CAAC;AAAA,IACjD;AAAA,IACA,GAAI,EAAE,OAAO,UAAU,KAAK,QAAQ;AAAA,MAClC,UAAU,OAAO,EAAE,KAAK,UAAU,CAAC;AAAA,IACrC;AAAA,IACA,GAAI,EAAE,OAAO,cAAc,KAAK,QAAQ;AAAA,MACtC,cAAc,OAAO,EAAE,KAAK,cAAc,CAAC;AAAA,IAC7C;AAAA,EACF,EAAE;AAEJ,QAAM,aAAa,QAAQ,OAAO;AAAA,IAChC,CAAC,MAAM,EAAE,SAAS,oBAAoB,EAAE,SAAS;AAAA,EACnD;AACA,QAAM,YAAY,KAAK;AAAA,IACrB,QAAQ,OAAO,OAAO,CAAC,MAAM,EAAE,SAAS,gBAAgB,EAAE;AAAA,IAC1D;AAAA,EACF;AAEA,QAAM,SAAS;AAAA,IACb,GAAG,IAAI;AAAA,MACL,QAAQ,OAAO,QAAQ,CAAC,MAAM,EAAE,MAAM,UAAU,CAAC,CAAC;AAAA,IACpD;AAAA,EACF;AAEA,QAAM,SAA6B;AAAA,IACjC,GAAI,QAAQ,gBAAgB,QAAQ,EAAE,eAAe,QAAQ,aAAa;AAAA,IAC1E,mBAAmB;AAAA,IACnB,GAAI,MAAM,SAAS,KAAK,EAAE,eAAe,MAAM;AAAA,IAC/C,IAAK,WAAW,SAAS,KAAK,OAAO,SAAS,MAAM;AAAA,MAClD,sBAAsB,EAAE,YAAY,WAAW,OAAO;AAAA,IACxD;AAAA,EACF;AAEA,SAAO;AACT;;;ACrEO,SAAS,qBAAqB,SAA2C;AAC9E,QAAM,YAAY,QAAQ,OACvB,OAAO,CAAC,MAAM,EAAE,SAAS,uBAAuB,EAAE,cAAc,IAAI,EACpE,IAAI,CAAC,OAAO,EAAE,aAAa,EAAE,YAAa,WAAW,EAAE,UAAU,EAAE;AAEtE,QAAM,QAAQ,QAAQ,OACnB,OAAO,CAAC,MAAM,EAAE,SAAS,mBAAmB,EAAE,cAAc,IAAI,EAChE,IAAI,CAAC,OAAO;AAAA,IACX,aAAa,EAAE;AAAA,IACf,WAAW,EAAE;AAAA,IACb,GAAI,EAAE,OAAO,eAAe,KAAK,QAAQ;AAAA,MACvC,eAAe,OAAO,EAAE,KAAK,eAAe,CAAC;AAAA,IAC/C;AAAA,IACA,GAAI,EAAE,OAAO,UAAU,KAAK,QAAQ;AAAA,MAClC,UAAU,OAAO,EAAE,KAAK,UAAU,CAAC;AAAA,IACrC;AAAA,EACF,EAAE;AAEJ,QAAM,YAAY,KAAK;AAAA,IACrB,QAAQ,OAAO,OAAO,CAAC,MAAM,EAAE,SAAS,gBAAgB,EAAE;AAAA,IAC1D;AAAA,EACF;AACA,QAAM,SAAS,CAAC,GAAG,IAAI,IAAI,QAAQ,OAAO,QAAQ,CAAC,MAAM,EAAE,MAAM,UAAU,CAAC,CAAC,CAAC,CAAC;AAC/E,QAAM,YAAY,QAAQ,mBAAmB,CAAC,GAAG,OAAO,OAAO;AAE/D,SAAO;AAAA,IACL,GAAI,QAAQ,gBAAgB,QAAQ,EAAE,eAAe,QAAQ,aAAa;AAAA,IAC1E,GAAI,SAAS,SAAS,KAAK,EAAE,mBAAmB,SAAS;AAAA,IACzD,mBAAmB;AAAA,IACnB,GAAI,MAAM,SAAS,KAAK,EAAE,eAAe,MAAM;AAAA,IAC/C,IAAK,OAAO,SAAS,KAAK,YAAY,MAAM;AAAA,MAC1C,sBAAsB,EAAE,YAAY,WAAW,OAAO;AAAA,IACxD;AAAA,EACF;AACF;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openattribution/telemetry",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "OpenAttribution Telemetry SDK for TypeScript/JavaScript — track content attribution in AI agent interactions",
5
5
  "license": "Apache-2.0",
6
6
  "author": "OpenAttribution Project",