@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 +50 -1
- package/dist/index.cjs +92 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +92 -4
- package/dist/index.d.ts +92 -4
- package/dist/index.js +91 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
| `
|
|
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
|
-
*
|
|
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
|
-
*
|
|
313
|
-
*
|
|
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,
|
package/dist/index.cjs.map
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
289
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
289
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
282
|
-
*
|
|
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":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openattribution/telemetry",
|
|
3
|
-
"version": "0.1.
|
|
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",
|