@metrone-io/server 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -0
- package/dist/index.cjs +185 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +167 -2
- package/dist/index.d.ts +167 -2
- package/dist/index.js +180 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,10 +40,47 @@ const live = await metrone.getLive()
|
|
|
40
40
|
await metrone.shutdown()
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
## AI agent capture
|
|
44
|
+
|
|
45
|
+
AI agents (GPTBot, ChatGPT-User, PerplexityBot, ClaudeBot, …) fetch your
|
|
46
|
+
pages without executing JavaScript, so browser analytics never sees them.
|
|
47
|
+
Capture them where the request is served and they appear on the Metrone
|
|
48
|
+
**AI Traffic** dashboard. Only known AI agents are forwarded — human
|
|
49
|
+
traffic is never sent from here.
|
|
50
|
+
|
|
51
|
+
```javascript
|
|
52
|
+
import { MetroneServer, agentMiddleware } from '@metrone-io/server'
|
|
53
|
+
|
|
54
|
+
const metrone = new MetroneServer({ apiKey: process.env.METRONE_API_KEY })
|
|
55
|
+
|
|
56
|
+
// Express / Connect — one line, mounted before routing
|
|
57
|
+
app.use(agentMiddleware(metrone))
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```javascript
|
|
61
|
+
// Fetch-style runtimes (Hono, Next.js middleware, Bun, Deno, Workers).
|
|
62
|
+
// In serverless, use batchSize: 0 so hits send immediately.
|
|
63
|
+
import { MetroneServer, trackAgentRequest } from '@metrone-io/server'
|
|
64
|
+
|
|
65
|
+
const metrone = new MetroneServer({ apiKey: process.env.METRONE_API_KEY, batchSize: 0 })
|
|
66
|
+
|
|
67
|
+
export function middleware(request) {
|
|
68
|
+
trackAgentRequest(metrone, request) // fire-and-forget, never throws
|
|
69
|
+
return NextResponse.next()
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Static assets are skipped automatically; pass `{ ignorePaths: ['/api/*'] }`
|
|
74
|
+
to exclude more. The original User-Agent is forwarded for classification
|
|
75
|
+
only and is never stored by Metrone.
|
|
76
|
+
|
|
77
|
+
On Cloudflare? Use the zero-code-change [edge Worker template](https://metrone.io/docs#agent-capture) instead.
|
|
78
|
+
|
|
43
79
|
## Features
|
|
44
80
|
|
|
45
81
|
- Zero runtime dependencies
|
|
46
82
|
- Event tracking with batching and retries
|
|
83
|
+
- AI agent capture middleware (GPTBot, ChatGPT, Perplexity, Claude, …)
|
|
47
84
|
- AI call and chat session tracking
|
|
48
85
|
- Read API for stats, pages, sources, events
|
|
49
86
|
- Live visitor count
|
package/dist/index.cjs
CHANGED
|
@@ -29,7 +29,11 @@ __export(index_exports, {
|
|
|
29
29
|
MetroneServer: () => MetroneServer,
|
|
30
30
|
MetroneServerError: () => MetroneServerError,
|
|
31
31
|
MetroneTimeoutError: () => MetroneTimeoutError,
|
|
32
|
-
MetroneValidationError: () => MetroneValidationError
|
|
32
|
+
MetroneValidationError: () => MetroneValidationError,
|
|
33
|
+
agentMiddleware: () => agentMiddleware,
|
|
34
|
+
captureAgentHit: () => captureAgentHit,
|
|
35
|
+
matchAiAgent: () => matchAiAgent,
|
|
36
|
+
trackAgentRequest: () => trackAgentRequest
|
|
33
37
|
});
|
|
34
38
|
module.exports = __toCommonJS(index_exports);
|
|
35
39
|
|
|
@@ -373,6 +377,68 @@ var MetroneServer = class {
|
|
|
373
377
|
properties: data.properties
|
|
374
378
|
});
|
|
375
379
|
}
|
|
380
|
+
// ─── v1.1 source helpers (SPEC-01) ─────────────────────────────────────────
|
|
381
|
+
// Thin ergonomic wrappers over track(). Required fields are typed; any
|
|
382
|
+
// extra keys flow into the JSONB `properties` payload (storage acceptance
|
|
383
|
+
// is permissive — see @metrone/schemas per-source recommended shapes).
|
|
384
|
+
/**
|
|
385
|
+
* Track a campaign lifecycle event (email / SMS sends, opens, clicks…).
|
|
386
|
+
* Recommended event_type values: email_sent | email_delivered |
|
|
387
|
+
* email_opened | email_clicked | email_bounced | email_unsubscribed |
|
|
388
|
+
* sms_sent | sms_delivered | sms_replied | sms_opt_out
|
|
389
|
+
*/
|
|
390
|
+
trackCampaignEvent(data) {
|
|
391
|
+
const { event_type, ...properties } = data;
|
|
392
|
+
this.track(event_type, { source: "campaign", properties });
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Track a social media event. Recommended event_type values:
|
|
396
|
+
* post_published | impression | engagement | click | share
|
|
397
|
+
*/
|
|
398
|
+
trackSocialEvent(data) {
|
|
399
|
+
const { event_type, ...properties } = data;
|
|
400
|
+
this.track(event_type, { source: "social", properties });
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Track a review event. Recommended event_type values: received | responded
|
|
404
|
+
*/
|
|
405
|
+
trackReviewEvent(data) {
|
|
406
|
+
const { event_type, ...properties } = data;
|
|
407
|
+
this.track(event_type, { source: "review", properties });
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Track a paid-ad metrics event. Recommended event_type values:
|
|
411
|
+
* impression | click | spend_daily | conversion
|
|
412
|
+
*/
|
|
413
|
+
trackAdEvent(data) {
|
|
414
|
+
const { event_type, ...properties } = data;
|
|
415
|
+
this.track(event_type, { source: "ad", properties });
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Track an SMS conversation event. Recommended event_type values:
|
|
419
|
+
* received | replied. agent_id is promoted to the top-level column so
|
|
420
|
+
* existing per-agent dashboard breakdowns keep working.
|
|
421
|
+
*/
|
|
422
|
+
trackSmsEvent(data) {
|
|
423
|
+
const { event_type, agent_id, ...properties } = data;
|
|
424
|
+
this.track(event_type, { source: "sms", agent_id, properties });
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Track an autonomous agent action (audit-log style). event_type is the
|
|
428
|
+
* action itself (free-form vocabulary). agent_type / agent_id are written
|
|
429
|
+
* BOTH to the top-level columns (for existing dashboard breakdowns) AND
|
|
430
|
+
* into properties (for the JSONB-first get_agent_activity_feed RPC) —
|
|
431
|
+
* decision B4 of the v1.1 build.
|
|
432
|
+
*/
|
|
433
|
+
trackAgentAction(data) {
|
|
434
|
+
const { agent_type, action_taken, agent_id, ...rest } = data;
|
|
435
|
+
this.track(action_taken, {
|
|
436
|
+
source: "agent_action",
|
|
437
|
+
agent_id,
|
|
438
|
+
agent_type,
|
|
439
|
+
properties: { agent_type, action_taken, ...rest }
|
|
440
|
+
});
|
|
441
|
+
}
|
|
376
442
|
// ─── Read API ──────────────────────────────────────────────────────────────
|
|
377
443
|
/**
|
|
378
444
|
* Get aggregated analytics stats for a time period.
|
|
@@ -538,6 +604,119 @@ var MetroneServer = class {
|
|
|
538
604
|
return result;
|
|
539
605
|
}
|
|
540
606
|
};
|
|
607
|
+
|
|
608
|
+
// src/agent-tracker.ts
|
|
609
|
+
var AI_AGENT_RULES = [
|
|
610
|
+
// OpenAI
|
|
611
|
+
{ pattern: /chatgpt-user/i, name: "ChatGPT-User" },
|
|
612
|
+
{ pattern: /oai-searchbot/i, name: "OAI-SearchBot" },
|
|
613
|
+
{ pattern: /gptbot/i, name: "GPTBot" },
|
|
614
|
+
// Anthropic
|
|
615
|
+
{ pattern: /claude-user/i, name: "Claude-User" },
|
|
616
|
+
{ pattern: /claude-searchbot/i, name: "Claude-SearchBot" },
|
|
617
|
+
{ pattern: /claudebot/i, name: "ClaudeBot" },
|
|
618
|
+
{ pattern: /claude-web/i, name: "Claude-Web" },
|
|
619
|
+
{ pattern: /anthropic-ai/i, name: "anthropic-ai" },
|
|
620
|
+
// Perplexity
|
|
621
|
+
{ pattern: /perplexity-user/i, name: "Perplexity-User" },
|
|
622
|
+
{ pattern: /perplexitybot/i, name: "PerplexityBot" },
|
|
623
|
+
// Google AI
|
|
624
|
+
{ pattern: /google-cloudvertexbot/i, name: "Google-CloudVertexBot" },
|
|
625
|
+
{ pattern: /googleother/i, name: "GoogleOther" },
|
|
626
|
+
{ pattern: /gemini-deep-research/i, name: "Gemini-Deep-Research" },
|
|
627
|
+
// Meta
|
|
628
|
+
{ pattern: /meta-externalagent/i, name: "Meta-ExternalAgent" },
|
|
629
|
+
{ pattern: /meta-externalfetcher/i, name: "Meta-ExternalFetcher" },
|
|
630
|
+
// Microsoft Copilot
|
|
631
|
+
{ pattern: /bingpreview/i, name: "BingPreview" },
|
|
632
|
+
// xAI
|
|
633
|
+
{ pattern: /grokbot/i, name: "GrokBot" },
|
|
634
|
+
{ pattern: /xai-crawler/i, name: "xAI-Crawler" },
|
|
635
|
+
// Others
|
|
636
|
+
{ pattern: /duckassistbot/i, name: "DuckAssistBot" },
|
|
637
|
+
{ pattern: /mistralai-user/i, name: "MistralAI-User" },
|
|
638
|
+
{ pattern: /cohere-training-data-crawler/i, name: "Cohere-Training-Crawler" },
|
|
639
|
+
{ pattern: /cohere-ai/i, name: "cohere-ai" },
|
|
640
|
+
{ pattern: /bytespider/i, name: "Bytespider" },
|
|
641
|
+
{ pattern: /amazonbot/i, name: "Amazonbot" },
|
|
642
|
+
{ pattern: /applebot-extended/i, name: "Applebot-Extended" },
|
|
643
|
+
{ pattern: /ccbot/i, name: "CCBot" },
|
|
644
|
+
{ pattern: /youbot/i, name: "YouBot" },
|
|
645
|
+
{ pattern: /timpibot/i, name: "TimpiBot" },
|
|
646
|
+
{ pattern: /diffbot/i, name: "Diffbot" }
|
|
647
|
+
];
|
|
648
|
+
var STATIC_ASSET_PATTERN = /\.(css|js|mjs|json|xml|txt|ico|png|jpe?g|gif|svg|webp|avif|woff2?|ttf|otf|eot|map|pdf|zip|gz|mp4|webm|mp3)$/i;
|
|
649
|
+
function matchAiAgent(userAgent) {
|
|
650
|
+
if (!userAgent) return void 0;
|
|
651
|
+
for (const rule of AI_AGENT_RULES) {
|
|
652
|
+
if (rule.pattern.test(userAgent)) return rule.name;
|
|
653
|
+
}
|
|
654
|
+
return void 0;
|
|
655
|
+
}
|
|
656
|
+
function pathFromUrl(url) {
|
|
657
|
+
try {
|
|
658
|
+
return new URL(url, "http://localhost").pathname;
|
|
659
|
+
} catch {
|
|
660
|
+
return url.split("?")[0] ?? url;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
function isIgnored(path, options) {
|
|
664
|
+
if (options?.skipStaticAssets !== false && STATIC_ASSET_PATTERN.test(path)) return true;
|
|
665
|
+
for (const rule of options?.ignorePaths ?? []) {
|
|
666
|
+
if (rule.endsWith("*") ? path.startsWith(rule.slice(0, -1)) : path === rule) return true;
|
|
667
|
+
}
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
function captureAgentHit(metrone, hit, options) {
|
|
671
|
+
try {
|
|
672
|
+
const method = (hit.method ?? "GET").toUpperCase();
|
|
673
|
+
if (method !== "GET" && method !== "HEAD") return void 0;
|
|
674
|
+
const agent = matchAiAgent(hit.userAgent);
|
|
675
|
+
if (!agent) return void 0;
|
|
676
|
+
const path = pathFromUrl(hit.url);
|
|
677
|
+
if (isIgnored(path, options)) return void 0;
|
|
678
|
+
metrone.track("pageview", {
|
|
679
|
+
source: "web",
|
|
680
|
+
page_url: hit.url,
|
|
681
|
+
page_path: path,
|
|
682
|
+
referrer: hit.referrer ?? void 0,
|
|
683
|
+
user_agent: hit.userAgent ?? void 0,
|
|
684
|
+
properties: { agent_capture: "server" }
|
|
685
|
+
});
|
|
686
|
+
return agent;
|
|
687
|
+
} catch {
|
|
688
|
+
return void 0;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
function agentMiddleware(metrone, options) {
|
|
692
|
+
return function metroneAgentMiddleware(req, _res, next) {
|
|
693
|
+
try {
|
|
694
|
+
const header = (name) => {
|
|
695
|
+
const v = req.headers[name];
|
|
696
|
+
return Array.isArray(v) ? v[0] : v;
|
|
697
|
+
};
|
|
698
|
+
const host = header("host") ?? "localhost";
|
|
699
|
+
const proto = req.protocol ?? (header("x-forwarded-proto") ?? "https");
|
|
700
|
+
const rawPath = req.originalUrl ?? req.url ?? "/";
|
|
701
|
+
captureAgentHit(metrone, {
|
|
702
|
+
url: `${proto}://${host}${rawPath}`,
|
|
703
|
+
userAgent: header("user-agent"),
|
|
704
|
+
referrer: header("referer") ?? header("referrer"),
|
|
705
|
+
method: req.method
|
|
706
|
+
}, options);
|
|
707
|
+
} catch {
|
|
708
|
+
}
|
|
709
|
+
next();
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
function trackAgentRequest(metrone, request, options) {
|
|
713
|
+
return captureAgentHit(metrone, {
|
|
714
|
+
url: request.url,
|
|
715
|
+
userAgent: request.headers.get("user-agent"),
|
|
716
|
+
referrer: request.headers.get("referer"),
|
|
717
|
+
method: request.method
|
|
718
|
+
}, options);
|
|
719
|
+
}
|
|
541
720
|
// Annotate the CommonJS export names for ESM import in node:
|
|
542
721
|
0 && (module.exports = {
|
|
543
722
|
MetroneAuthError,
|
|
@@ -549,6 +728,10 @@ var MetroneServer = class {
|
|
|
549
728
|
MetroneServer,
|
|
550
729
|
MetroneServerError,
|
|
551
730
|
MetroneTimeoutError,
|
|
552
|
-
MetroneValidationError
|
|
731
|
+
MetroneValidationError,
|
|
732
|
+
agentMiddleware,
|
|
733
|
+
captureAgentHit,
|
|
734
|
+
matchAiAgent,
|
|
735
|
+
trackAgentRequest
|
|
553
736
|
});
|
|
554
737
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/http.ts","../src/client.ts"],"sourcesContent":["/**\n * @metrone-io/server — Server-side SDK for Metrone analytics.\n *\n * Works in Node.js 18+, Deno, Bun, Cloudflare Workers, and any runtime\n * with a global fetch(). Zero dependencies.\n *\n * @example\n * ```ts\n * import { MetroneServer } from '@metrone-io/server'\n *\n * const metrone = new MetroneServer({\n * apiKey: 'metrone_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',\n * })\n *\n * // Track events from your server\n * metrone.track('user_signup', { properties: { plan: 'pro' } })\n *\n * // Track AI interactions\n * metrone.trackAICall({\n * call_id: 'call_123',\n * provider: 'openai',\n * duration: 120,\n * intent: 'booking',\n * outcome: 'converted',\n * })\n *\n * // Read analytics\n * const stats = await metrone.getStats({ days: 30 })\n * console.log(stats.unique_visitors)\n *\n * // Graceful shutdown\n * await metrone.shutdown()\n * ```\n */\n\nexport { MetroneServer } from './client.js'\n\nexport {\n MetroneError,\n MetroneConfigError,\n MetroneAuthError,\n MetroneRateLimitError,\n MetroneQuotaError,\n MetroneNetworkError,\n MetroneTimeoutError,\n MetroneValidationError,\n MetroneServerError,\n} from './errors.js'\n\nexport type {\n MetroneServerConfig,\n EventPayload,\n EventSource,\n AICallData,\n AIChatData,\n AIIntentData,\n AISessionData,\n ConversionData,\n StatsParams,\n EventsParams,\n PagesParams,\n SourcesParams,\n FlushResult,\n ApiResponse,\n ApiError,\n StatsResponse,\n EventsResponse,\n EventRow,\n PagesResponse,\n PageRow,\n SourcesResponse,\n ChannelRow,\n ReferrerRow,\n LiveResponse,\n} from './types.js'\n","/**\n * Structured error types for the Metrone server SDK.\n *\n * All errors carry a machine-readable `code` that agents can switch on,\n * plus a human-readable `message` for logs.\n */\n\nexport class MetroneError extends Error {\n public readonly code: string\n public readonly status?: number\n public readonly retryable: boolean\n\n constructor(code: string, message: string, status?: number, retryable = false) {\n super(message)\n this.name = 'MetroneError'\n this.code = code\n this.status = status\n this.retryable = retryable\n }\n}\n\nexport class MetroneConfigError extends MetroneError {\n constructor(message: string) {\n super('CONFIG_INVALID', message)\n this.name = 'MetroneConfigError'\n }\n}\n\nexport class MetroneAuthError extends MetroneError {\n constructor(message = 'Invalid or missing API key') {\n super('AUTH_FAILED', message, 401, false)\n this.name = 'MetroneAuthError'\n }\n}\n\nexport class MetroneRateLimitError extends MetroneError {\n public readonly retryAfterMs: number\n\n constructor(retryAfterMs: number) {\n super('RATE_LIMITED', `Rate limit exceeded. Retry after ${retryAfterMs}ms.`, 429, true)\n this.name = 'MetroneRateLimitError'\n this.retryAfterMs = retryAfterMs\n }\n}\n\nexport class MetroneQuotaError extends MetroneError {\n constructor(message = 'Monthly event quota exceeded') {\n super('QUOTA_EXCEEDED', message, 429, false)\n this.name = 'MetroneQuotaError'\n }\n}\n\nexport class MetroneNetworkError extends MetroneError {\n constructor(message = 'Network request failed') {\n super('NETWORK_ERROR', message, undefined, true)\n this.name = 'MetroneNetworkError'\n }\n}\n\nexport class MetroneTimeoutError extends MetroneError {\n constructor(timeoutMs: number) {\n super('TIMEOUT', `Request timed out after ${timeoutMs}ms`, undefined, true)\n this.name = 'MetroneTimeoutError'\n }\n}\n\nexport class MetroneValidationError extends MetroneError {\n public readonly fields: Array<{ field: string; message: string }>\n\n constructor(fields: Array<{ field: string; message: string }>) {\n const summary = fields.map(f => `${f.field}: ${f.message}`).join('; ')\n super('VALIDATION_FAILED', `Validation failed: ${summary}`, 400, false)\n this.name = 'MetroneValidationError'\n this.fields = fields\n }\n}\n\nexport class MetroneServerError extends MetroneError {\n constructor(status: number, message = 'Internal server error') {\n super('SERVER_ERROR', message, status, true)\n this.name = 'MetroneServerError'\n }\n}\n\n/**\n * 4xx response that isn't one of the more specific subclasses (auth, quota,\n * validation, rate-limit). Always permanent — re-sending the same payload\n * will produce the same error, so callers must NOT requeue events that\n * triggered it. This breaks the retry-storm pattern where a permanent server\n * rejection (e.g. `BATCH_ALL_INVALID`) used to be re-queued every flush\n * interval forever.\n */\nexport class MetroneClientError extends MetroneError {\n constructor(status: number, message = 'Request rejected by server') {\n super('CLIENT_ERROR', message, status, false)\n this.name = 'MetroneClientError'\n }\n}\n","/**\n * Runtime-agnostic HTTP layer.\n *\n * Uses the global fetch() which is available in Node 18+, Deno, Bun,\n * Cloudflare Workers, and all modern edge runtimes. A custom fetch can\n * be injected via config for environments that need it.\n */\n\nimport type { ResolvedConfig, ApiResponse } from './types.js'\nimport {\n MetroneNetworkError,\n MetroneTimeoutError,\n MetroneAuthError,\n MetroneRateLimitError,\n MetroneServerError,\n MetroneClientError,\n} from './errors.js'\n\nexport async function httpPost(\n config: ResolvedConfig,\n path: string,\n body: unknown,\n headers?: Record<string, string>,\n): Promise<ApiResponse> {\n return httpRequest(config, 'POST', path, body, headers)\n}\n\nexport async function httpGet(\n config: ResolvedConfig,\n path: string,\n params?: Record<string, string | number | undefined>,\n headers?: Record<string, string>,\n): Promise<ApiResponse> {\n let url = path\n if (params) {\n const qs = new URLSearchParams()\n for (const [k, v] of Object.entries(params)) {\n if (v !== undefined) qs.set(k, String(v))\n }\n const str = qs.toString()\n if (str) url += `?${str}`\n }\n return httpRequest(config, 'GET', url, undefined, headers)\n}\n\nasync function httpRequest(\n config: ResolvedConfig,\n method: string,\n path: string,\n body?: unknown,\n extraHeaders?: Record<string, string>,\n): Promise<ApiResponse> {\n const url = `${config.endpoint}${path}`\n const fetchFn = config.fetch\n\n const headers: Record<string, string> = {\n 'X-Api-Key': config.apiKey,\n 'User-Agent': 'metrone-server-sdk/1.0.2',\n ...extraHeaders,\n }\n\n if (body !== undefined) {\n headers['Content-Type'] = 'application/json'\n }\n\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), config.timeoutMs)\n\n try {\n const response = await fetchFn(url, {\n method,\n headers,\n body: body !== undefined ? JSON.stringify(body) : undefined,\n signal: controller.signal,\n })\n\n clearTimeout(timer)\n\n let data: unknown\n const ct = response.headers.get('content-type') ?? ''\n if (ct.includes('application/json')) {\n data = await response.json()\n } else {\n data = await response.text()\n }\n\n if (response.ok) {\n return { ok: true, status: response.status, data }\n }\n\n const errBody = data as Record<string, unknown> | undefined\n const errMsg = typeof errBody?.error === 'string'\n ? errBody.error\n : typeof errBody?.error === 'object' && errBody.error !== null\n ? (errBody.error as Record<string, unknown>).message as string ?? response.statusText\n : response.statusText\n\n if (response.status === 401) throw new MetroneAuthError(errMsg)\n if (response.status === 429) {\n const retryMs = typeof errBody?.retry_after_ms === 'number'\n ? errBody.retry_after_ms\n : 60_000\n throw new MetroneRateLimitError(retryMs)\n }\n if (response.status >= 500) throw new MetroneServerError(response.status, errMsg)\n // Any other 4xx (validation failure, quota exceeded as 4xx, etc.) is a\n // permanent rejection. Throw a typed error so `withRetry` doesn't loop\n // and `flush()` knows to drop the events instead of re-queueing them.\n if (response.status >= 400 && response.status < 500) {\n throw new MetroneClientError(response.status, errMsg)\n }\n\n return {\n ok: false,\n status: response.status,\n error: { code: 'API_ERROR', message: errMsg, details: errBody },\n }\n } catch (err) {\n clearTimeout(timer)\n\n if (err instanceof MetroneAuthError) throw err\n if (err instanceof MetroneRateLimitError) throw err\n if (err instanceof MetroneServerError) throw err\n if (err instanceof MetroneClientError) throw err\n\n if (err instanceof DOMException && err.name === 'AbortError') {\n throw new MetroneTimeoutError(config.timeoutMs)\n }\n if (err instanceof TypeError) {\n throw new MetroneNetworkError(err.message)\n }\n\n throw new MetroneNetworkError(\n err instanceof Error ? err.message : 'Unknown network error',\n )\n }\n}\n\n/**\n * Retry wrapper with exponential backoff.\n * Retries on MetroneNetworkError, MetroneTimeoutError, MetroneServerError,\n * and MetroneRateLimitError. Stops on auth or validation errors.\n */\nexport async function withRetry<T>(\n config: ResolvedConfig,\n fn: () => Promise<T>,\n): Promise<T> {\n let lastError: Error | undefined\n\n for (let attempt = 0; attempt <= config.maxRetries; attempt++) {\n try {\n return await fn()\n } catch (err) {\n lastError = err instanceof Error ? err : new Error(String(err))\n\n const isRetryable =\n err instanceof MetroneNetworkError ||\n err instanceof MetroneTimeoutError ||\n err instanceof MetroneServerError ||\n err instanceof MetroneRateLimitError\n\n if (!isRetryable || attempt >= config.maxRetries) throw err\n\n const baseDelay = err instanceof MetroneRateLimitError\n ? err.retryAfterMs\n : config.retryBaseMs * Math.pow(2, attempt)\n\n const jitter = Math.random() * baseDelay * 0.1\n const delay = baseDelay + jitter\n\n if (config.debug) {\n const errLabel = err instanceof Error ? err.message : String(err)\n console.error(\n `[metrone] Retry ${attempt + 1}/${config.maxRetries} after ${Math.round(delay)}ms: ${errLabel}`,\n )\n }\n\n await sleep(delay)\n }\n }\n\n throw lastError\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms))\n}\n","/**\n * MetroneServer — the main server-side SDK client.\n *\n * Zero dependencies. Uses global fetch (Node 18+, Deno, Bun, edge runtimes).\n * Runtime-agnostic: no DOM, no window, no navigator, no localStorage.\n *\n * Features:\n * - Event ingestion (single + batch) with automatic batching\n * - AI tracking (calls, chats, intents, sessions)\n * - Read API (stats, events, pages, sources, live)\n * - Retry with exponential backoff + jitter\n * - Idempotency key support\n * - Agent identity tracking\n * - Graceful shutdown with flush()\n */\n\nimport type {\n MetroneServerConfig,\n ResolvedConfig,\n EventPayload,\n AICallData,\n AIChatData,\n AIIntentData,\n AISessionData,\n ConversionData,\n StatsParams,\n EventsParams,\n PagesParams,\n SourcesParams,\n FlushResult,\n StatsResponse,\n EventsResponse,\n PagesResponse,\n SourcesResponse,\n LiveResponse,\n ApiResponse,\n} from './types.js'\nimport {\n MetroneConfigError,\n MetroneValidationError,\n MetroneClientError,\n MetroneAuthError,\n MetroneError,\n} from './errors.js'\nimport { httpPost, httpGet, withRetry } from './http.js'\n\nconst API_KEY_PATTERN = /^(metrone_(live|test)_|mk_(live|test)_)[a-f0-9]{32}$/\n\nexport class MetroneServer {\n private readonly config: ResolvedConfig\n private queue: Array<EventPayload & { api_key: string }> = []\n private flushTimer: ReturnType<typeof setInterval> | null = null\n private flushing = false\n private destroyed = false\n\n constructor(config: MetroneServerConfig) {\n if (!config.apiKey) {\n throw new MetroneConfigError('apiKey is required')\n }\n if (!API_KEY_PATTERN.test(config.apiKey)) {\n throw new MetroneConfigError(\n 'Invalid apiKey format. Expected metrone_live_*, metrone_test_*, mk_live_*, or mk_test_* followed by 32 hex chars.',\n )\n }\n\n const endpoint = (config.endpoint ?? 'https://api.metrone.io').replace(/\\/+$/, '')\n\n this.config = {\n apiKey: config.apiKey,\n endpoint,\n batchSize: config.batchSize ?? 10,\n flushIntervalMs: config.flushIntervalMs ?? 5000,\n maxRetries: config.maxRetries ?? 3,\n retryBaseMs: config.retryBaseMs ?? 1000,\n timeoutMs: config.timeoutMs ?? 10_000,\n maxQueueSize: config.maxQueueSize ?? 1000,\n debug: config.debug ?? false,\n fetch: config.fetch ?? globalThis.fetch.bind(globalThis),\n }\n\n if (this.config.batchSize > 0 && this.config.flushIntervalMs > 0) {\n this.flushTimer = setInterval(() => {\n this.flush().catch(err => {\n if (this.config.debug) {\n console.error('[metrone] Auto-flush error:', err)\n }\n })\n }, this.config.flushIntervalMs)\n\n if (typeof this.flushTimer === 'object' && 'unref' in this.flushTimer) {\n (this.flushTimer as NodeJS.Timeout).unref()\n }\n }\n\n if (this.config.debug) {\n console.error(`[metrone] Server SDK initialized (endpoint: ${this.config.endpoint})`)\n }\n }\n\n // ─── Event Tracking ────────────────────────────────────────────────────────\n\n /**\n * Track a custom event. Queued for batch sending unless batchSize is 0.\n */\n track(eventType: string, data?: Partial<EventPayload>): void {\n this.assertNotDestroyed()\n if (!eventType) throw new MetroneValidationError([{ field: 'event_type', message: 'required' }])\n\n const event: EventPayload & { api_key: string } = {\n api_key: this.config.apiKey,\n event_type: eventType,\n timestamp: new Date().toISOString(),\n ...data,\n }\n\n if (this.config.batchSize <= 0) {\n withRetry(this.config, () => httpPost(this.config, '/v1/events', event)).catch(err => {\n if (this.config.debug) console.error('[metrone] Send error:', err)\n })\n return\n }\n\n this.queue.push(event)\n if (this.queue.length >= this.config.batchSize) {\n this.flush().catch(err => {\n if (this.config.debug) console.error('[metrone] Flush error:', err)\n })\n }\n }\n\n /**\n * Track a page view.\n */\n pageview(url: string, title?: string, data?: Partial<EventPayload>): void {\n this.track('pageview', {\n page_url: url,\n page_title: title,\n source: 'web',\n ...data,\n })\n }\n\n /**\n * Track a conversion event.\n */\n conversion(data: ConversionData): void {\n this.track('conversion', {\n source: 'web',\n properties: {\n conversion_type: data.conversion_type,\n value: data.value,\n currency: data.currency,\n ...data.properties,\n },\n })\n }\n\n // ─── AI Tracking ───────────────────────────────────────────────────────────\n\n /**\n * Track an AI voice call.\n */\n trackAICall(data: AICallData): void {\n this.track('ai_call', {\n source: 'voice',\n channel: 'phone',\n ai_provider: data.provider ?? 'unknown',\n ai_call_id: data.call_id,\n ai_intent: data.intent,\n ai_duration_sec: data.duration,\n session_id: data.session_id,\n properties: {\n transcript_snippet: data.transcript_snippet,\n outcome: data.outcome,\n ...data.properties,\n },\n })\n }\n\n /**\n * Track an AI chat interaction.\n */\n trackAIChat(data: AIChatData): void {\n this.track('ai_chat', {\n source: 'chat',\n channel: 'website',\n ai_provider: data.provider ?? 'unknown',\n ai_session_id: data.session_id,\n ai_intent: data.intent,\n ai_duration_sec: data.duration,\n properties: {\n message_count: data.message_count,\n resolved: data.resolved,\n ...data.properties,\n },\n })\n }\n\n /**\n * Track an AI intent detection.\n */\n trackAIIntent(data: AIIntentData): void {\n this.track('ai_intent', {\n source: data.source ?? 'assistant',\n ai_intent: data.intent,\n properties: {\n confidence: data.confidence,\n ...data.properties,\n },\n })\n }\n\n /**\n * Track an AI session lifecycle event (start, end, timeout).\n */\n trackAISession(data: AISessionData): void {\n this.track(`ai_session_${data.action}`, {\n source: 'assistant',\n ai_provider: data.provider ?? 'unknown',\n ai_session_id: data.session_id,\n ai_duration_sec: data.duration,\n properties: data.properties,\n })\n }\n\n // ─── Read API ──────────────────────────────────────────────────────────────\n\n /**\n * Get aggregated analytics stats for a time period.\n */\n async getStats(params?: StatsParams): Promise<StatsResponse> {\n this.assertNotDestroyed()\n const qp = this.buildDateParams(params)\n const res = await withRetry(this.config, () =>\n httpGet(this.config, '/v1/api/stats', qp),\n )\n return res.data as StatsResponse\n }\n\n /**\n * Get individual analytics events with pagination and filters.\n */\n async getEvents(params?: EventsParams): Promise<EventsResponse> {\n this.assertNotDestroyed()\n const qp: Record<string, string | number | undefined> = {\n ...this.buildDateParams(params),\n limit: params?.limit,\n offset: params?.offset,\n event_type: params?.event_type,\n source: params?.source,\n }\n const res = await withRetry(this.config, () =>\n httpGet(this.config, '/v1/api/events', qp),\n )\n return res.data as EventsResponse\n }\n\n /**\n * Get page analytics broken down by path.\n */\n async getPages(params?: PagesParams): Promise<PagesResponse> {\n this.assertNotDestroyed()\n const qp: Record<string, string | number | undefined> = {\n ...this.buildDateParams(params),\n limit: params?.limit,\n }\n const res = await withRetry(this.config, () =>\n httpGet(this.config, '/v1/api/pages', qp),\n )\n return res.data as PagesResponse\n }\n\n /**\n * Get traffic source and referrer analytics.\n */\n async getSources(params?: SourcesParams): Promise<SourcesResponse> {\n this.assertNotDestroyed()\n const qp = this.buildDateParams(params)\n const res = await withRetry(this.config, () =>\n httpGet(this.config, '/v1/api/sources', qp),\n )\n return res.data as SourcesResponse\n }\n\n /**\n * Get real-time live stats (active visitors, today's totals).\n */\n async getLive(): Promise<LiveResponse> {\n this.assertNotDestroyed()\n const res = await withRetry(this.config, () =>\n httpGet(this.config, '/v1/api/live'),\n )\n return res.data as LiveResponse\n }\n\n // ─── Batch & Lifecycle ─────────────────────────────────────────────────────\n\n /**\n * Immediately flush all queued events. Returns the result.\n * Safe to call multiple times concurrently — only one flush runs at a time.\n */\n async flush(): Promise<FlushResult> {\n if (this.flushing || this.queue.length === 0) {\n return { sent: 0, failed: 0, errors: [] }\n }\n\n this.flushing = true\n const batch = this.queue.splice(0, 100)\n\n try {\n const res = await withRetry(this.config, () =>\n httpPost(this.config, '/v1/events/batch', batch),\n )\n\n const body = res.data as Record<string, unknown> | undefined\n const accepted = typeof body?.accepted === 'number' ? body.accepted : batch.length\n\n if (this.config.debug) {\n console.error(`[metrone] Flushed ${accepted}/${batch.length} events`)\n }\n\n return { sent: accepted, failed: batch.length - accepted, errors: [] }\n } catch (err) {\n // Permanent failures (auth, validation, other 4xx) MUST NOT be\n // re-queued — re-sending the same payload will produce the same error,\n // turning every flushIntervalMs tick into a retry storm. Drop the\n // events and surface the failure to the caller instead.\n const isPermanent =\n err instanceof MetroneAuthError ||\n err instanceof MetroneClientError ||\n (err instanceof MetroneError && err.retryable === false)\n\n if (isPermanent) {\n if (this.config.debug) {\n console.error(\n `[metrone] Dropping ${batch.length} events — server rejected them permanently:`,\n err instanceof Error ? err.message : err,\n )\n }\n return {\n sent: 0,\n failed: batch.length,\n errors: [{ index: -1, error: err instanceof Error ? err.message : String(err) }],\n }\n }\n\n // Transient failures (network, timeout, 5xx, 429 after retries) — keep\n // events for the next flush attempt.\n this.queue.unshift(...batch)\n if (this.queue.length > this.config.maxQueueSize) {\n this.queue.length = this.config.maxQueueSize\n }\n\n if (this.config.debug) {\n console.error('[metrone] Flush failed (transient), re-queued:', err)\n }\n\n return {\n sent: 0,\n failed: batch.length,\n errors: [{ index: -1, error: err instanceof Error ? err.message : String(err) }],\n }\n } finally {\n this.flushing = false\n\n if (this.queue.length >= this.config.batchSize && this.config.batchSize > 0) {\n this.flush().catch(() => {})\n }\n }\n }\n\n /**\n * Flush remaining events, stop the auto-flush timer, and mark the\n * client as destroyed. Subsequent calls will throw.\n */\n async shutdown(): Promise<FlushResult> {\n if (this.destroyed) return { sent: 0, failed: 0, errors: [] }\n\n if (this.flushTimer) {\n clearInterval(this.flushTimer)\n this.flushTimer = null\n }\n\n const result = await this.flush()\n this.destroyed = true\n\n if (this.config.debug) {\n console.error('[metrone] Server SDK shut down')\n }\n\n return result\n }\n\n /** Number of events currently in the send queue. */\n get queueSize(): number {\n return this.queue.length\n }\n\n /** Whether shutdown() has been called. */\n get isDestroyed(): boolean {\n return this.destroyed\n }\n\n // ─── Internal ──────────────────────────────────────────────────────────────\n\n private assertNotDestroyed(): void {\n if (this.destroyed) {\n throw new MetroneConfigError('Client has been shut down. Create a new instance.')\n }\n }\n\n private buildDateParams(\n params?: { days?: number; from?: string | Date; to?: string | Date },\n ): Record<string, string | number | undefined> {\n const result: Record<string, string | number | undefined> = {}\n if (params?.days) result.days = params.days\n if (params?.from) result.from = params.from instanceof Date ? params.from.toISOString() : params.from\n if (params?.to) result.to = params.to instanceof Date ? params.to.toISOString() : params.to\n return result\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACOO,IAAM,eAAN,cAA2B,MAAM;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EAEhB,YAAY,MAAc,SAAiB,QAAiB,YAAY,OAAO;AAC7E,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,YAAY;AAAA,EACnB;AACF;AAEO,IAAM,qBAAN,cAAiC,aAAa;AAAA,EACnD,YAAY,SAAiB;AAC3B,UAAM,kBAAkB,OAAO;AAC/B,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,mBAAN,cAA+B,aAAa;AAAA,EACjD,YAAY,UAAU,8BAA8B;AAClD,UAAM,eAAe,SAAS,KAAK,KAAK;AACxC,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,wBAAN,cAAoC,aAAa;AAAA,EACtC;AAAA,EAEhB,YAAY,cAAsB;AAChC,UAAM,gBAAgB,oCAAoC,YAAY,OAAO,KAAK,IAAI;AACtF,SAAK,OAAO;AACZ,SAAK,eAAe;AAAA,EACtB;AACF;AAEO,IAAM,oBAAN,cAAgC,aAAa;AAAA,EAClD,YAAY,UAAU,gCAAgC;AACpD,UAAM,kBAAkB,SAAS,KAAK,KAAK;AAC3C,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,sBAAN,cAAkC,aAAa;AAAA,EACpD,YAAY,UAAU,0BAA0B;AAC9C,UAAM,iBAAiB,SAAS,QAAW,IAAI;AAC/C,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,sBAAN,cAAkC,aAAa;AAAA,EACpD,YAAY,WAAmB;AAC7B,UAAM,WAAW,2BAA2B,SAAS,MAAM,QAAW,IAAI;AAC1E,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,yBAAN,cAAqC,aAAa;AAAA,EACvC;AAAA,EAEhB,YAAY,QAAmD;AAC7D,UAAM,UAAU,OAAO,IAAI,OAAK,GAAG,EAAE,KAAK,KAAK,EAAE,OAAO,EAAE,EAAE,KAAK,IAAI;AACrE,UAAM,qBAAqB,sBAAsB,OAAO,IAAI,KAAK,KAAK;AACtE,SAAK,OAAO;AACZ,SAAK,SAAS;AAAA,EAChB;AACF;AAEO,IAAM,qBAAN,cAAiC,aAAa;AAAA,EACnD,YAAY,QAAgB,UAAU,yBAAyB;AAC7D,UAAM,gBAAgB,SAAS,QAAQ,IAAI;AAC3C,SAAK,OAAO;AAAA,EACd;AACF;AAUO,IAAM,qBAAN,cAAiC,aAAa;AAAA,EACnD,YAAY,QAAgB,UAAU,8BAA8B;AAClE,UAAM,gBAAgB,SAAS,QAAQ,KAAK;AAC5C,SAAK,OAAO;AAAA,EACd;AACF;;;AC/EA,eAAsB,SACpB,QACA,MACA,MACA,SACsB;AACtB,SAAO,YAAY,QAAQ,QAAQ,MAAM,MAAM,OAAO;AACxD;AAEA,eAAsB,QACpB,QACA,MACA,QACA,SACsB;AACtB,MAAI,MAAM;AACV,MAAI,QAAQ;AACV,UAAM,KAAK,IAAI,gBAAgB;AAC/B,eAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,MAAM,GAAG;AAC3C,UAAI,MAAM,OAAW,IAAG,IAAI,GAAG,OAAO,CAAC,CAAC;AAAA,IAC1C;AACA,UAAM,MAAM,GAAG,SAAS;AACxB,QAAI,IAAK,QAAO,IAAI,GAAG;AAAA,EACzB;AACA,SAAO,YAAY,QAAQ,OAAO,KAAK,QAAW,OAAO;AAC3D;AAEA,eAAe,YACb,QACA,QACA,MACA,MACA,cACsB;AACtB,QAAM,MAAM,GAAG,OAAO,QAAQ,GAAG,IAAI;AACrC,QAAM,UAAU,OAAO;AAEvB,QAAM,UAAkC;AAAA,IACtC,aAAa,OAAO;AAAA,IACpB,cAAc;AAAA,IACd,GAAG;AAAA,EACL;AAEA,MAAI,SAAS,QAAW;AACtB,YAAQ,cAAc,IAAI;AAAA,EAC5B;AAEA,QAAM,aAAa,IAAI,gBAAgB;AACvC,QAAM,QAAQ,WAAW,MAAM,WAAW,MAAM,GAAG,OAAO,SAAS;AAEnE,MAAI;AACF,UAAM,WAAW,MAAM,QAAQ,KAAK;AAAA,MAClC;AAAA,MACA;AAAA,MACA,MAAM,SAAS,SAAY,KAAK,UAAU,IAAI,IAAI;AAAA,MAClD,QAAQ,WAAW;AAAA,IACrB,CAAC;AAED,iBAAa,KAAK;AAElB,QAAI;AACJ,UAAM,KAAK,SAAS,QAAQ,IAAI,cAAc,KAAK;AACnD,QAAI,GAAG,SAAS,kBAAkB,GAAG;AACnC,aAAO,MAAM,SAAS,KAAK;AAAA,IAC7B,OAAO;AACL,aAAO,MAAM,SAAS,KAAK;AAAA,IAC7B;AAEA,QAAI,SAAS,IAAI;AACf,aAAO,EAAE,IAAI,MAAM,QAAQ,SAAS,QAAQ,KAAK;AAAA,IACnD;AAEA,UAAM,UAAU;AAChB,UAAM,SAAS,OAAO,SAAS,UAAU,WACrC,QAAQ,QACR,OAAO,SAAS,UAAU,YAAY,QAAQ,UAAU,OACrD,QAAQ,MAAkC,WAAqB,SAAS,aACzE,SAAS;AAEf,QAAI,SAAS,WAAW,IAAK,OAAM,IAAI,iBAAiB,MAAM;AAC9D,QAAI,SAAS,WAAW,KAAK;AAC3B,YAAM,UAAU,OAAO,SAAS,mBAAmB,WAC/C,QAAQ,iBACR;AACJ,YAAM,IAAI,sBAAsB,OAAO;AAAA,IACzC;AACA,QAAI,SAAS,UAAU,IAAK,OAAM,IAAI,mBAAmB,SAAS,QAAQ,MAAM;AAIhF,QAAI,SAAS,UAAU,OAAO,SAAS,SAAS,KAAK;AACnD,YAAM,IAAI,mBAAmB,SAAS,QAAQ,MAAM;AAAA,IACtD;AAEA,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,QAAQ,SAAS;AAAA,MACjB,OAAO,EAAE,MAAM,aAAa,SAAS,QAAQ,SAAS,QAAQ;AAAA,IAChE;AAAA,EACF,SAAS,KAAK;AACZ,iBAAa,KAAK;AAElB,QAAI,eAAe,iBAAkB,OAAM;AAC3C,QAAI,eAAe,sBAAuB,OAAM;AAChD,QAAI,eAAe,mBAAoB,OAAM;AAC7C,QAAI,eAAe,mBAAoB,OAAM;AAE7C,QAAI,eAAe,gBAAgB,IAAI,SAAS,cAAc;AAC5D,YAAM,IAAI,oBAAoB,OAAO,SAAS;AAAA,IAChD;AACA,QAAI,eAAe,WAAW;AAC5B,YAAM,IAAI,oBAAoB,IAAI,OAAO;AAAA,IAC3C;AAEA,UAAM,IAAI;AAAA,MACR,eAAe,QAAQ,IAAI,UAAU;AAAA,IACvC;AAAA,EACF;AACF;AAOA,eAAsB,UACpB,QACA,IACY;AACZ,MAAI;AAEJ,WAAS,UAAU,GAAG,WAAW,OAAO,YAAY,WAAW;AAC7D,QAAI;AACF,aAAO,MAAM,GAAG;AAAA,IAClB,SAAS,KAAK;AACZ,kBAAY,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;AAE9D,YAAM,cACJ,eAAe,uBACf,eAAe,uBACf,eAAe,sBACf,eAAe;AAEjB,UAAI,CAAC,eAAe,WAAW,OAAO,WAAY,OAAM;AAExD,YAAM,YAAY,eAAe,wBAC7B,IAAI,eACJ,OAAO,cAAc,KAAK,IAAI,GAAG,OAAO;AAE5C,YAAM,SAAS,KAAK,OAAO,IAAI,YAAY;AAC3C,YAAM,QAAQ,YAAY;AAE1B,UAAI,OAAO,OAAO;AAChB,cAAM,WAAW,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAChE,gBAAQ;AAAA,UACN,mBAAmB,UAAU,CAAC,IAAI,OAAO,UAAU,UAAU,KAAK,MAAM,KAAK,CAAC,OAAO,QAAQ;AAAA,QAC/F;AAAA,MACF;AAEA,YAAM,MAAM,KAAK;AAAA,IACnB;AAAA,EACF;AAEA,QAAM;AACR;AAEA,SAAS,MAAM,IAA2B;AACxC,SAAO,IAAI,QAAQ,aAAW,WAAW,SAAS,EAAE,CAAC;AACvD;;;AC5IA,IAAM,kBAAkB;AAEjB,IAAM,gBAAN,MAAoB;AAAA,EACR;AAAA,EACT,QAAmD,CAAC;AAAA,EACpD,aAAoD;AAAA,EACpD,WAAW;AAAA,EACX,YAAY;AAAA,EAEpB,YAAY,QAA6B;AACvC,QAAI,CAAC,OAAO,QAAQ;AAClB,YAAM,IAAI,mBAAmB,oBAAoB;AAAA,IACnD;AACA,QAAI,CAAC,gBAAgB,KAAK,OAAO,MAAM,GAAG;AACxC,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAEA,UAAM,YAAY,OAAO,YAAY,0BAA0B,QAAQ,QAAQ,EAAE;AAEjF,SAAK,SAAS;AAAA,MACZ,QAAQ,OAAO;AAAA,MACf;AAAA,MACA,WAAW,OAAO,aAAa;AAAA,MAC/B,iBAAiB,OAAO,mBAAmB;AAAA,MAC3C,YAAY,OAAO,cAAc;AAAA,MACjC,aAAa,OAAO,eAAe;AAAA,MACnC,WAAW,OAAO,aAAa;AAAA,MAC/B,cAAc,OAAO,gBAAgB;AAAA,MACrC,OAAO,OAAO,SAAS;AAAA,MACvB,OAAO,OAAO,SAAS,WAAW,MAAM,KAAK,UAAU;AAAA,IACzD;AAEA,QAAI,KAAK,OAAO,YAAY,KAAK,KAAK,OAAO,kBAAkB,GAAG;AAChE,WAAK,aAAa,YAAY,MAAM;AAClC,aAAK,MAAM,EAAE,MAAM,SAAO;AACxB,cAAI,KAAK,OAAO,OAAO;AACrB,oBAAQ,MAAM,+BAA+B,GAAG;AAAA,UAClD;AAAA,QACF,CAAC;AAAA,MACH,GAAG,KAAK,OAAO,eAAe;AAE9B,UAAI,OAAO,KAAK,eAAe,YAAY,WAAW,KAAK,YAAY;AACrE,QAAC,KAAK,WAA8B,MAAM;AAAA,MAC5C;AAAA,IACF;AAEA,QAAI,KAAK,OAAO,OAAO;AACrB,cAAQ,MAAM,+CAA+C,KAAK,OAAO,QAAQ,GAAG;AAAA,IACtF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,WAAmB,MAAoC;AAC3D,SAAK,mBAAmB;AACxB,QAAI,CAAC,UAAW,OAAM,IAAI,uBAAuB,CAAC,EAAE,OAAO,cAAc,SAAS,WAAW,CAAC,CAAC;AAE/F,UAAM,QAA4C;AAAA,MAChD,SAAS,KAAK,OAAO;AAAA,MACrB,YAAY;AAAA,MACZ,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MAClC,GAAG;AAAA,IACL;AAEA,QAAI,KAAK,OAAO,aAAa,GAAG;AAC9B,gBAAU,KAAK,QAAQ,MAAM,SAAS,KAAK,QAAQ,cAAc,KAAK,CAAC,EAAE,MAAM,SAAO;AACpF,YAAI,KAAK,OAAO,MAAO,SAAQ,MAAM,yBAAyB,GAAG;AAAA,MACnE,CAAC;AACD;AAAA,IACF;AAEA,SAAK,MAAM,KAAK,KAAK;AACrB,QAAI,KAAK,MAAM,UAAU,KAAK,OAAO,WAAW;AAC9C,WAAK,MAAM,EAAE,MAAM,SAAO;AACxB,YAAI,KAAK,OAAO,MAAO,SAAQ,MAAM,0BAA0B,GAAG;AAAA,MACpE,CAAC;AAAA,IACH;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,SAAS,KAAa,OAAgB,MAAoC;AACxE,SAAK,MAAM,YAAY;AAAA,MACrB,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,GAAG;AAAA,IACL,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,WAAW,MAA4B;AACrC,SAAK,MAAM,cAAc;AAAA,MACvB,QAAQ;AAAA,MACR,YAAY;AAAA,QACV,iBAAiB,KAAK;AAAA,QACtB,OAAO,KAAK;AAAA,QACZ,UAAU,KAAK;AAAA,QACf,GAAG,KAAK;AAAA,MACV;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,YAAY,MAAwB;AAClC,SAAK,MAAM,WAAW;AAAA,MACpB,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,aAAa,KAAK,YAAY;AAAA,MAC9B,YAAY,KAAK;AAAA,MACjB,WAAW,KAAK;AAAA,MAChB,iBAAiB,KAAK;AAAA,MACtB,YAAY,KAAK;AAAA,MACjB,YAAY;AAAA,QACV,oBAAoB,KAAK;AAAA,QACzB,SAAS,KAAK;AAAA,QACd,GAAG,KAAK;AAAA,MACV;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,YAAY,MAAwB;AAClC,SAAK,MAAM,WAAW;AAAA,MACpB,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,aAAa,KAAK,YAAY;AAAA,MAC9B,eAAe,KAAK;AAAA,MACpB,WAAW,KAAK;AAAA,MAChB,iBAAiB,KAAK;AAAA,MACtB,YAAY;AAAA,QACV,eAAe,KAAK;AAAA,QACpB,UAAU,KAAK;AAAA,QACf,GAAG,KAAK;AAAA,MACV;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,cAAc,MAA0B;AACtC,SAAK,MAAM,aAAa;AAAA,MACtB,QAAQ,KAAK,UAAU;AAAA,MACvB,WAAW,KAAK;AAAA,MAChB,YAAY;AAAA,QACV,YAAY,KAAK;AAAA,QACjB,GAAG,KAAK;AAAA,MACV;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,eAAe,MAA2B;AACxC,SAAK,MAAM,cAAc,KAAK,MAAM,IAAI;AAAA,MACtC,QAAQ;AAAA,MACR,aAAa,KAAK,YAAY;AAAA,MAC9B,eAAe,KAAK;AAAA,MACpB,iBAAiB,KAAK;AAAA,MACtB,YAAY,KAAK;AAAA,IACnB,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,SAAS,QAA8C;AAC3D,SAAK,mBAAmB;AACxB,UAAM,KAAK,KAAK,gBAAgB,MAAM;AACtC,UAAM,MAAM,MAAM;AAAA,MAAU,KAAK;AAAA,MAAQ,MACvC,QAAQ,KAAK,QAAQ,iBAAiB,EAAE;AAAA,IAC1C;AACA,WAAO,IAAI;AAAA,EACb;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,UAAU,QAAgD;AAC9D,SAAK,mBAAmB;AACxB,UAAM,KAAkD;AAAA,MACtD,GAAG,KAAK,gBAAgB,MAAM;AAAA,MAC9B,OAAO,QAAQ;AAAA,MACf,QAAQ,QAAQ;AAAA,MAChB,YAAY,QAAQ;AAAA,MACpB,QAAQ,QAAQ;AAAA,IAClB;AACA,UAAM,MAAM,MAAM;AAAA,MAAU,KAAK;AAAA,MAAQ,MACvC,QAAQ,KAAK,QAAQ,kBAAkB,EAAE;AAAA,IAC3C;AACA,WAAO,IAAI;AAAA,EACb;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,SAAS,QAA8C;AAC3D,SAAK,mBAAmB;AACxB,UAAM,KAAkD;AAAA,MACtD,GAAG,KAAK,gBAAgB,MAAM;AAAA,MAC9B,OAAO,QAAQ;AAAA,IACjB;AACA,UAAM,MAAM,MAAM;AAAA,MAAU,KAAK;AAAA,MAAQ,MACvC,QAAQ,KAAK,QAAQ,iBAAiB,EAAE;AAAA,IAC1C;AACA,WAAO,IAAI;AAAA,EACb;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,WAAW,QAAkD;AACjE,SAAK,mBAAmB;AACxB,UAAM,KAAK,KAAK,gBAAgB,MAAM;AACtC,UAAM,MAAM,MAAM;AAAA,MAAU,KAAK;AAAA,MAAQ,MACvC,QAAQ,KAAK,QAAQ,mBAAmB,EAAE;AAAA,IAC5C;AACA,WAAO,IAAI;AAAA,EACb;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,UAAiC;AACrC,SAAK,mBAAmB;AACxB,UAAM,MAAM,MAAM;AAAA,MAAU,KAAK;AAAA,MAAQ,MACvC,QAAQ,KAAK,QAAQ,cAAc;AAAA,IACrC;AACA,WAAO,IAAI;AAAA,EACb;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,QAA8B;AAClC,QAAI,KAAK,YAAY,KAAK,MAAM,WAAW,GAAG;AAC5C,aAAO,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC,EAAE;AAAA,IAC1C;AAEA,SAAK,WAAW;AAChB,UAAM,QAAQ,KAAK,MAAM,OAAO,GAAG,GAAG;AAEtC,QAAI;AACF,YAAM,MAAM,MAAM;AAAA,QAAU,KAAK;AAAA,QAAQ,MACvC,SAAS,KAAK,QAAQ,oBAAoB,KAAK;AAAA,MACjD;AAEA,YAAM,OAAO,IAAI;AACjB,YAAM,WAAW,OAAO,MAAM,aAAa,WAAW,KAAK,WAAW,MAAM;AAE5E,UAAI,KAAK,OAAO,OAAO;AACrB,gBAAQ,MAAM,qBAAqB,QAAQ,IAAI,MAAM,MAAM,SAAS;AAAA,MACtE;AAEA,aAAO,EAAE,MAAM,UAAU,QAAQ,MAAM,SAAS,UAAU,QAAQ,CAAC,EAAE;AAAA,IACvE,SAAS,KAAK;AAKZ,YAAM,cACJ,eAAe,oBACf,eAAe,sBACd,eAAe,gBAAgB,IAAI,cAAc;AAEpD,UAAI,aAAa;AACf,YAAI,KAAK,OAAO,OAAO;AACrB,kBAAQ;AAAA,YACN,sBAAsB,MAAM,MAAM;AAAA,YAClC,eAAe,QAAQ,IAAI,UAAU;AAAA,UACvC;AAAA,QACF;AACA,eAAO;AAAA,UACL,MAAM;AAAA,UACN,QAAQ,MAAM;AAAA,UACd,QAAQ,CAAC,EAAE,OAAO,IAAI,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE,CAAC;AAAA,QACjF;AAAA,MACF;AAIA,WAAK,MAAM,QAAQ,GAAG,KAAK;AAC3B,UAAI,KAAK,MAAM,SAAS,KAAK,OAAO,cAAc;AAChD,aAAK,MAAM,SAAS,KAAK,OAAO;AAAA,MAClC;AAEA,UAAI,KAAK,OAAO,OAAO;AACrB,gBAAQ,MAAM,kDAAkD,GAAG;AAAA,MACrE;AAEA,aAAO;AAAA,QACL,MAAM;AAAA,QACN,QAAQ,MAAM;AAAA,QACd,QAAQ,CAAC,EAAE,OAAO,IAAI,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE,CAAC;AAAA,MACjF;AAAA,IACF,UAAE;AACA,WAAK,WAAW;AAEhB,UAAI,KAAK,MAAM,UAAU,KAAK,OAAO,aAAa,KAAK,OAAO,YAAY,GAAG;AAC3E,aAAK,MAAM,EAAE,MAAM,MAAM;AAAA,QAAC,CAAC;AAAA,MAC7B;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,WAAiC;AACrC,QAAI,KAAK,UAAW,QAAO,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC,EAAE;AAE5D,QAAI,KAAK,YAAY;AACnB,oBAAc,KAAK,UAAU;AAC7B,WAAK,aAAa;AAAA,IACpB;AAEA,UAAM,SAAS,MAAM,KAAK,MAAM;AAChC,SAAK,YAAY;AAEjB,QAAI,KAAK,OAAO,OAAO;AACrB,cAAQ,MAAM,gCAAgC;AAAA,IAChD;AAEA,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,IAAI,YAAoB;AACtB,WAAO,KAAK,MAAM;AAAA,EACpB;AAAA;AAAA,EAGA,IAAI,cAAuB;AACzB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAIQ,qBAA2B;AACjC,QAAI,KAAK,WAAW;AAClB,YAAM,IAAI,mBAAmB,mDAAmD;AAAA,IAClF;AAAA,EACF;AAAA,EAEQ,gBACN,QAC6C;AAC7C,UAAM,SAAsD,CAAC;AAC7D,QAAI,QAAQ,KAAM,QAAO,OAAO,OAAO;AACvC,QAAI,QAAQ,KAAM,QAAO,OAAO,OAAO,gBAAgB,OAAO,OAAO,KAAK,YAAY,IAAI,OAAO;AACjG,QAAI,QAAQ,GAAI,QAAO,KAAK,OAAO,cAAc,OAAO,OAAO,GAAG,YAAY,IAAI,OAAO;AACzF,WAAO;AAAA,EACT;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/http.ts","../src/client.ts","../src/agent-tracker.ts"],"sourcesContent":["/**\n * @metrone-io/server — Server-side SDK for Metrone analytics.\n *\n * Works in Node.js 18+, Deno, Bun, Cloudflare Workers, and any runtime\n * with a global fetch(). Zero dependencies.\n *\n * @example\n * ```ts\n * import { MetroneServer } from '@metrone-io/server'\n *\n * const metrone = new MetroneServer({\n * apiKey: 'metrone_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',\n * })\n *\n * // Track events from your server\n * metrone.track('user_signup', { properties: { plan: 'pro' } })\n *\n * // Track AI interactions\n * metrone.trackAICall({\n * call_id: 'call_123',\n * provider: 'openai',\n * duration: 120,\n * intent: 'booking',\n * outcome: 'converted',\n * })\n *\n * // Read analytics\n * const stats = await metrone.getStats({ days: 30 })\n * console.log(stats.unique_visitors)\n *\n * // Graceful shutdown\n * await metrone.shutdown()\n * ```\n */\n\nexport { MetroneServer } from './client.js'\n\nexport {\n matchAiAgent,\n captureAgentHit,\n agentMiddleware,\n trackAgentRequest,\n} from './agent-tracker.js'\nexport type { AgentHit, AgentTrackerOptions } from './agent-tracker.js'\n\nexport {\n MetroneError,\n MetroneConfigError,\n MetroneAuthError,\n MetroneRateLimitError,\n MetroneQuotaError,\n MetroneNetworkError,\n MetroneTimeoutError,\n MetroneValidationError,\n MetroneServerError,\n} from './errors.js'\n\nexport type {\n MetroneServerConfig,\n EventPayload,\n EventSource,\n AICallData,\n AIChatData,\n AIIntentData,\n AISessionData,\n ConversionData,\n StatsParams,\n EventsParams,\n PagesParams,\n SourcesParams,\n FlushResult,\n ApiResponse,\n ApiError,\n StatsResponse,\n EventsResponse,\n EventRow,\n PagesResponse,\n PageRow,\n SourcesResponse,\n ChannelRow,\n ReferrerRow,\n LiveResponse,\n} from './types.js'\n","/**\n * Structured error types for the Metrone server SDK.\n *\n * All errors carry a machine-readable `code` that agents can switch on,\n * plus a human-readable `message` for logs.\n */\n\nexport class MetroneError extends Error {\n public readonly code: string\n public readonly status?: number\n public readonly retryable: boolean\n\n constructor(code: string, message: string, status?: number, retryable = false) {\n super(message)\n this.name = 'MetroneError'\n this.code = code\n this.status = status\n this.retryable = retryable\n }\n}\n\nexport class MetroneConfigError extends MetroneError {\n constructor(message: string) {\n super('CONFIG_INVALID', message)\n this.name = 'MetroneConfigError'\n }\n}\n\nexport class MetroneAuthError extends MetroneError {\n constructor(message = 'Invalid or missing API key') {\n super('AUTH_FAILED', message, 401, false)\n this.name = 'MetroneAuthError'\n }\n}\n\nexport class MetroneRateLimitError extends MetroneError {\n public readonly retryAfterMs: number\n\n constructor(retryAfterMs: number) {\n super('RATE_LIMITED', `Rate limit exceeded. Retry after ${retryAfterMs}ms.`, 429, true)\n this.name = 'MetroneRateLimitError'\n this.retryAfterMs = retryAfterMs\n }\n}\n\nexport class MetroneQuotaError extends MetroneError {\n constructor(message = 'Monthly event quota exceeded') {\n super('QUOTA_EXCEEDED', message, 429, false)\n this.name = 'MetroneQuotaError'\n }\n}\n\nexport class MetroneNetworkError extends MetroneError {\n constructor(message = 'Network request failed') {\n super('NETWORK_ERROR', message, undefined, true)\n this.name = 'MetroneNetworkError'\n }\n}\n\nexport class MetroneTimeoutError extends MetroneError {\n constructor(timeoutMs: number) {\n super('TIMEOUT', `Request timed out after ${timeoutMs}ms`, undefined, true)\n this.name = 'MetroneTimeoutError'\n }\n}\n\nexport class MetroneValidationError extends MetroneError {\n public readonly fields: Array<{ field: string; message: string }>\n\n constructor(fields: Array<{ field: string; message: string }>) {\n const summary = fields.map(f => `${f.field}: ${f.message}`).join('; ')\n super('VALIDATION_FAILED', `Validation failed: ${summary}`, 400, false)\n this.name = 'MetroneValidationError'\n this.fields = fields\n }\n}\n\nexport class MetroneServerError extends MetroneError {\n constructor(status: number, message = 'Internal server error') {\n super('SERVER_ERROR', message, status, true)\n this.name = 'MetroneServerError'\n }\n}\n\n/**\n * 4xx response that isn't one of the more specific subclasses (auth, quota,\n * validation, rate-limit). Always permanent — re-sending the same payload\n * will produce the same error, so callers must NOT requeue events that\n * triggered it. This breaks the retry-storm pattern where a permanent server\n * rejection (e.g. `BATCH_ALL_INVALID`) used to be re-queued every flush\n * interval forever.\n */\nexport class MetroneClientError extends MetroneError {\n constructor(status: number, message = 'Request rejected by server') {\n super('CLIENT_ERROR', message, status, false)\n this.name = 'MetroneClientError'\n }\n}\n","/**\n * Runtime-agnostic HTTP layer.\n *\n * Uses the global fetch() which is available in Node 18+, Deno, Bun,\n * Cloudflare Workers, and all modern edge runtimes. A custom fetch can\n * be injected via config for environments that need it.\n */\n\nimport type { ResolvedConfig, ApiResponse } from './types.js'\nimport {\n MetroneNetworkError,\n MetroneTimeoutError,\n MetroneAuthError,\n MetroneRateLimitError,\n MetroneServerError,\n MetroneClientError,\n} from './errors.js'\n\nexport async function httpPost(\n config: ResolvedConfig,\n path: string,\n body: unknown,\n headers?: Record<string, string>,\n): Promise<ApiResponse> {\n return httpRequest(config, 'POST', path, body, headers)\n}\n\nexport async function httpGet(\n config: ResolvedConfig,\n path: string,\n params?: Record<string, string | number | undefined>,\n headers?: Record<string, string>,\n): Promise<ApiResponse> {\n let url = path\n if (params) {\n const qs = new URLSearchParams()\n for (const [k, v] of Object.entries(params)) {\n if (v !== undefined) qs.set(k, String(v))\n }\n const str = qs.toString()\n if (str) url += `?${str}`\n }\n return httpRequest(config, 'GET', url, undefined, headers)\n}\n\nasync function httpRequest(\n config: ResolvedConfig,\n method: string,\n path: string,\n body?: unknown,\n extraHeaders?: Record<string, string>,\n): Promise<ApiResponse> {\n const url = `${config.endpoint}${path}`\n const fetchFn = config.fetch\n\n const headers: Record<string, string> = {\n 'X-Api-Key': config.apiKey,\n 'User-Agent': 'metrone-server-sdk/1.0.2',\n ...extraHeaders,\n }\n\n if (body !== undefined) {\n headers['Content-Type'] = 'application/json'\n }\n\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), config.timeoutMs)\n\n try {\n const response = await fetchFn(url, {\n method,\n headers,\n body: body !== undefined ? JSON.stringify(body) : undefined,\n signal: controller.signal,\n })\n\n clearTimeout(timer)\n\n let data: unknown\n const ct = response.headers.get('content-type') ?? ''\n if (ct.includes('application/json')) {\n data = await response.json()\n } else {\n data = await response.text()\n }\n\n if (response.ok) {\n return { ok: true, status: response.status, data }\n }\n\n const errBody = data as Record<string, unknown> | undefined\n const errMsg = typeof errBody?.error === 'string'\n ? errBody.error\n : typeof errBody?.error === 'object' && errBody.error !== null\n ? (errBody.error as Record<string, unknown>).message as string ?? response.statusText\n : response.statusText\n\n if (response.status === 401) throw new MetroneAuthError(errMsg)\n if (response.status === 429) {\n const retryMs = typeof errBody?.retry_after_ms === 'number'\n ? errBody.retry_after_ms\n : 60_000\n throw new MetroneRateLimitError(retryMs)\n }\n if (response.status >= 500) throw new MetroneServerError(response.status, errMsg)\n // Any other 4xx (validation failure, quota exceeded as 4xx, etc.) is a\n // permanent rejection. Throw a typed error so `withRetry` doesn't loop\n // and `flush()` knows to drop the events instead of re-queueing them.\n if (response.status >= 400 && response.status < 500) {\n throw new MetroneClientError(response.status, errMsg)\n }\n\n return {\n ok: false,\n status: response.status,\n error: { code: 'API_ERROR', message: errMsg, details: errBody },\n }\n } catch (err) {\n clearTimeout(timer)\n\n if (err instanceof MetroneAuthError) throw err\n if (err instanceof MetroneRateLimitError) throw err\n if (err instanceof MetroneServerError) throw err\n if (err instanceof MetroneClientError) throw err\n\n if (err instanceof DOMException && err.name === 'AbortError') {\n throw new MetroneTimeoutError(config.timeoutMs)\n }\n if (err instanceof TypeError) {\n throw new MetroneNetworkError(err.message)\n }\n\n throw new MetroneNetworkError(\n err instanceof Error ? err.message : 'Unknown network error',\n )\n }\n}\n\n/**\n * Retry wrapper with exponential backoff.\n * Retries on MetroneNetworkError, MetroneTimeoutError, MetroneServerError,\n * and MetroneRateLimitError. Stops on auth or validation errors.\n */\nexport async function withRetry<T>(\n config: ResolvedConfig,\n fn: () => Promise<T>,\n): Promise<T> {\n let lastError: Error | undefined\n\n for (let attempt = 0; attempt <= config.maxRetries; attempt++) {\n try {\n return await fn()\n } catch (err) {\n lastError = err instanceof Error ? err : new Error(String(err))\n\n const isRetryable =\n err instanceof MetroneNetworkError ||\n err instanceof MetroneTimeoutError ||\n err instanceof MetroneServerError ||\n err instanceof MetroneRateLimitError\n\n if (!isRetryable || attempt >= config.maxRetries) throw err\n\n const baseDelay = err instanceof MetroneRateLimitError\n ? err.retryAfterMs\n : config.retryBaseMs * Math.pow(2, attempt)\n\n const jitter = Math.random() * baseDelay * 0.1\n const delay = baseDelay + jitter\n\n if (config.debug) {\n const errLabel = err instanceof Error ? err.message : String(err)\n console.error(\n `[metrone] Retry ${attempt + 1}/${config.maxRetries} after ${Math.round(delay)}ms: ${errLabel}`,\n )\n }\n\n await sleep(delay)\n }\n }\n\n throw lastError\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms))\n}\n","/**\n * MetroneServer — the main server-side SDK client.\n *\n * Zero dependencies. Uses global fetch (Node 18+, Deno, Bun, edge runtimes).\n * Runtime-agnostic: no DOM, no window, no navigator, no localStorage.\n *\n * Features:\n * - Event ingestion (single + batch) with automatic batching\n * - AI tracking (calls, chats, intents, sessions)\n * - Read API (stats, events, pages, sources, live)\n * - Retry with exponential backoff + jitter\n * - Idempotency key support\n * - Agent identity tracking\n * - Graceful shutdown with flush()\n */\n\nimport type {\n MetroneServerConfig,\n ResolvedConfig,\n EventPayload,\n AICallData,\n AIChatData,\n AIIntentData,\n AISessionData,\n ConversionData,\n StatsParams,\n EventsParams,\n PagesParams,\n SourcesParams,\n FlushResult,\n StatsResponse,\n EventsResponse,\n PagesResponse,\n SourcesResponse,\n LiveResponse,\n ApiResponse,\n} from './types.js'\nimport {\n MetroneConfigError,\n MetroneValidationError,\n MetroneClientError,\n MetroneAuthError,\n MetroneError,\n} from './errors.js'\nimport { httpPost, httpGet, withRetry } from './http.js'\n\nconst API_KEY_PATTERN = /^(metrone_(live|test)_|mk_(live|test)_)[a-f0-9]{32}$/\n\nexport class MetroneServer {\n private readonly config: ResolvedConfig\n private queue: Array<EventPayload & { api_key: string }> = []\n private flushTimer: ReturnType<typeof setInterval> | null = null\n private flushing = false\n private destroyed = false\n\n constructor(config: MetroneServerConfig) {\n if (!config.apiKey) {\n throw new MetroneConfigError('apiKey is required')\n }\n if (!API_KEY_PATTERN.test(config.apiKey)) {\n throw new MetroneConfigError(\n 'Invalid apiKey format. Expected metrone_live_*, metrone_test_*, mk_live_*, or mk_test_* followed by 32 hex chars.',\n )\n }\n\n const endpoint = (config.endpoint ?? 'https://api.metrone.io').replace(/\\/+$/, '')\n\n this.config = {\n apiKey: config.apiKey,\n endpoint,\n batchSize: config.batchSize ?? 10,\n flushIntervalMs: config.flushIntervalMs ?? 5000,\n maxRetries: config.maxRetries ?? 3,\n retryBaseMs: config.retryBaseMs ?? 1000,\n timeoutMs: config.timeoutMs ?? 10_000,\n maxQueueSize: config.maxQueueSize ?? 1000,\n debug: config.debug ?? false,\n fetch: config.fetch ?? globalThis.fetch.bind(globalThis),\n }\n\n if (this.config.batchSize > 0 && this.config.flushIntervalMs > 0) {\n this.flushTimer = setInterval(() => {\n this.flush().catch(err => {\n if (this.config.debug) {\n console.error('[metrone] Auto-flush error:', err)\n }\n })\n }, this.config.flushIntervalMs)\n\n if (typeof this.flushTimer === 'object' && 'unref' in this.flushTimer) {\n (this.flushTimer as NodeJS.Timeout).unref()\n }\n }\n\n if (this.config.debug) {\n console.error(`[metrone] Server SDK initialized (endpoint: ${this.config.endpoint})`)\n }\n }\n\n // ─── Event Tracking ────────────────────────────────────────────────────────\n\n /**\n * Track a custom event. Queued for batch sending unless batchSize is 0.\n */\n track(eventType: string, data?: Partial<EventPayload>): void {\n this.assertNotDestroyed()\n if (!eventType) throw new MetroneValidationError([{ field: 'event_type', message: 'required' }])\n\n const event: EventPayload & { api_key: string } = {\n api_key: this.config.apiKey,\n event_type: eventType,\n timestamp: new Date().toISOString(),\n ...data,\n }\n\n if (this.config.batchSize <= 0) {\n withRetry(this.config, () => httpPost(this.config, '/v1/events', event)).catch(err => {\n if (this.config.debug) console.error('[metrone] Send error:', err)\n })\n return\n }\n\n this.queue.push(event)\n if (this.queue.length >= this.config.batchSize) {\n this.flush().catch(err => {\n if (this.config.debug) console.error('[metrone] Flush error:', err)\n })\n }\n }\n\n /**\n * Track a page view.\n */\n pageview(url: string, title?: string, data?: Partial<EventPayload>): void {\n this.track('pageview', {\n page_url: url,\n page_title: title,\n source: 'web',\n ...data,\n })\n }\n\n /**\n * Track a conversion event.\n */\n conversion(data: ConversionData): void {\n this.track('conversion', {\n source: 'web',\n properties: {\n conversion_type: data.conversion_type,\n value: data.value,\n currency: data.currency,\n ...data.properties,\n },\n })\n }\n\n // ─── AI Tracking ───────────────────────────────────────────────────────────\n\n /**\n * Track an AI voice call.\n */\n trackAICall(data: AICallData): void {\n this.track('ai_call', {\n source: 'voice',\n channel: 'phone',\n ai_provider: data.provider ?? 'unknown',\n ai_call_id: data.call_id,\n ai_intent: data.intent,\n ai_duration_sec: data.duration,\n session_id: data.session_id,\n properties: {\n transcript_snippet: data.transcript_snippet,\n outcome: data.outcome,\n ...data.properties,\n },\n })\n }\n\n /**\n * Track an AI chat interaction.\n */\n trackAIChat(data: AIChatData): void {\n this.track('ai_chat', {\n source: 'chat',\n channel: 'website',\n ai_provider: data.provider ?? 'unknown',\n ai_session_id: data.session_id,\n ai_intent: data.intent,\n ai_duration_sec: data.duration,\n properties: {\n message_count: data.message_count,\n resolved: data.resolved,\n ...data.properties,\n },\n })\n }\n\n /**\n * Track an AI intent detection.\n */\n trackAIIntent(data: AIIntentData): void {\n this.track('ai_intent', {\n source: data.source ?? 'assistant',\n ai_intent: data.intent,\n properties: {\n confidence: data.confidence,\n ...data.properties,\n },\n })\n }\n\n /**\n * Track an AI session lifecycle event (start, end, timeout).\n */\n trackAISession(data: AISessionData): void {\n this.track(`ai_session_${data.action}`, {\n source: 'assistant',\n ai_provider: data.provider ?? 'unknown',\n ai_session_id: data.session_id,\n ai_duration_sec: data.duration,\n properties: data.properties,\n })\n }\n\n // ─── v1.1 source helpers (SPEC-01) ─────────────────────────────────────────\n // Thin ergonomic wrappers over track(). Required fields are typed; any\n // extra keys flow into the JSONB `properties` payload (storage acceptance\n // is permissive — see @metrone/schemas per-source recommended shapes).\n\n /**\n * Track a campaign lifecycle event (email / SMS sends, opens, clicks…).\n * Recommended event_type values: email_sent | email_delivered |\n * email_opened | email_clicked | email_bounced | email_unsubscribed |\n * sms_sent | sms_delivered | sms_replied | sms_opt_out\n */\n trackCampaignEvent(data: { event_type: string; campaign_id: string; recipient_id_hash: string } & Record<string, unknown>): void {\n const { event_type, ...properties } = data\n this.track(event_type, { source: 'campaign', properties })\n }\n\n /**\n * Track a social media event. Recommended event_type values:\n * post_published | impression | engagement | click | share\n */\n trackSocialEvent(data: { event_type: string; platform: string; post_id: string } & Record<string, unknown>): void {\n const { event_type, ...properties } = data\n this.track(event_type, { source: 'social', properties })\n }\n\n /**\n * Track a review event. Recommended event_type values: received | responded\n */\n trackReviewEvent(data: { event_type: string; platform: string; review_id: string; rating: number } & Record<string, unknown>): void {\n const { event_type, ...properties } = data\n this.track(event_type, { source: 'review', properties })\n }\n\n /**\n * Track a paid-ad metrics event. Recommended event_type values:\n * impression | click | spend_daily | conversion\n */\n trackAdEvent(data: { event_type: string; platform: string; campaign_id: string } & Record<string, unknown>): void {\n const { event_type, ...properties } = data\n this.track(event_type, { source: 'ad', properties })\n }\n\n /**\n * Track an SMS conversation event. Recommended event_type values:\n * received | replied. agent_id is promoted to the top-level column so\n * existing per-agent dashboard breakdowns keep working.\n */\n trackSmsEvent(data: { event_type: string; agent_id: string } & Record<string, unknown>): void {\n const { event_type, agent_id, ...properties } = data\n this.track(event_type, { source: 'sms', agent_id: agent_id as string, properties })\n }\n\n /**\n * Track an autonomous agent action (audit-log style). event_type is the\n * action itself (free-form vocabulary). agent_type / agent_id are written\n * BOTH to the top-level columns (for existing dashboard breakdowns) AND\n * into properties (for the JSONB-first get_agent_activity_feed RPC) —\n * decision B4 of the v1.1 build.\n */\n trackAgentAction(data: { agent_type: string; action_taken: string; agent_id?: string; target?: string; outcome?: string } & Record<string, unknown>): void {\n const { agent_type, action_taken, agent_id, ...rest } = data\n this.track(action_taken, {\n source: 'agent_action',\n agent_id: agent_id as string | undefined,\n agent_type: agent_type as string,\n properties: { agent_type, action_taken, ...rest },\n })\n }\n\n // ─── Read API ──────────────────────────────────────────────────────────────\n\n /**\n * Get aggregated analytics stats for a time period.\n */\n async getStats(params?: StatsParams): Promise<StatsResponse> {\n this.assertNotDestroyed()\n const qp = this.buildDateParams(params)\n const res = await withRetry(this.config, () =>\n httpGet(this.config, '/v1/api/stats', qp),\n )\n return res.data as StatsResponse\n }\n\n /**\n * Get individual analytics events with pagination and filters.\n */\n async getEvents(params?: EventsParams): Promise<EventsResponse> {\n this.assertNotDestroyed()\n const qp: Record<string, string | number | undefined> = {\n ...this.buildDateParams(params),\n limit: params?.limit,\n offset: params?.offset,\n event_type: params?.event_type,\n source: params?.source,\n }\n const res = await withRetry(this.config, () =>\n httpGet(this.config, '/v1/api/events', qp),\n )\n return res.data as EventsResponse\n }\n\n /**\n * Get page analytics broken down by path.\n */\n async getPages(params?: PagesParams): Promise<PagesResponse> {\n this.assertNotDestroyed()\n const qp: Record<string, string | number | undefined> = {\n ...this.buildDateParams(params),\n limit: params?.limit,\n }\n const res = await withRetry(this.config, () =>\n httpGet(this.config, '/v1/api/pages', qp),\n )\n return res.data as PagesResponse\n }\n\n /**\n * Get traffic source and referrer analytics.\n */\n async getSources(params?: SourcesParams): Promise<SourcesResponse> {\n this.assertNotDestroyed()\n const qp = this.buildDateParams(params)\n const res = await withRetry(this.config, () =>\n httpGet(this.config, '/v1/api/sources', qp),\n )\n return res.data as SourcesResponse\n }\n\n /**\n * Get real-time live stats (active visitors, today's totals).\n */\n async getLive(): Promise<LiveResponse> {\n this.assertNotDestroyed()\n const res = await withRetry(this.config, () =>\n httpGet(this.config, '/v1/api/live'),\n )\n return res.data as LiveResponse\n }\n\n // ─── Batch & Lifecycle ─────────────────────────────────────────────────────\n\n /**\n * Immediately flush all queued events. Returns the result.\n * Safe to call multiple times concurrently — only one flush runs at a time.\n */\n async flush(): Promise<FlushResult> {\n if (this.flushing || this.queue.length === 0) {\n return { sent: 0, failed: 0, errors: [] }\n }\n\n this.flushing = true\n const batch = this.queue.splice(0, 100)\n\n try {\n const res = await withRetry(this.config, () =>\n httpPost(this.config, '/v1/events/batch', batch),\n )\n\n const body = res.data as Record<string, unknown> | undefined\n const accepted = typeof body?.accepted === 'number' ? body.accepted : batch.length\n\n if (this.config.debug) {\n console.error(`[metrone] Flushed ${accepted}/${batch.length} events`)\n }\n\n return { sent: accepted, failed: batch.length - accepted, errors: [] }\n } catch (err) {\n // Permanent failures (auth, validation, other 4xx) MUST NOT be\n // re-queued — re-sending the same payload will produce the same error,\n // turning every flushIntervalMs tick into a retry storm. Drop the\n // events and surface the failure to the caller instead.\n const isPermanent =\n err instanceof MetroneAuthError ||\n err instanceof MetroneClientError ||\n (err instanceof MetroneError && err.retryable === false)\n\n if (isPermanent) {\n if (this.config.debug) {\n console.error(\n `[metrone] Dropping ${batch.length} events — server rejected them permanently:`,\n err instanceof Error ? err.message : err,\n )\n }\n return {\n sent: 0,\n failed: batch.length,\n errors: [{ index: -1, error: err instanceof Error ? err.message : String(err) }],\n }\n }\n\n // Transient failures (network, timeout, 5xx, 429 after retries) — keep\n // events for the next flush attempt.\n this.queue.unshift(...batch)\n if (this.queue.length > this.config.maxQueueSize) {\n this.queue.length = this.config.maxQueueSize\n }\n\n if (this.config.debug) {\n console.error('[metrone] Flush failed (transient), re-queued:', err)\n }\n\n return {\n sent: 0,\n failed: batch.length,\n errors: [{ index: -1, error: err instanceof Error ? err.message : String(err) }],\n }\n } finally {\n this.flushing = false\n\n if (this.queue.length >= this.config.batchSize && this.config.batchSize > 0) {\n this.flush().catch(() => {})\n }\n }\n }\n\n /**\n * Flush remaining events, stop the auto-flush timer, and mark the\n * client as destroyed. Subsequent calls will throw.\n */\n async shutdown(): Promise<FlushResult> {\n if (this.destroyed) return { sent: 0, failed: 0, errors: [] }\n\n if (this.flushTimer) {\n clearInterval(this.flushTimer)\n this.flushTimer = null\n }\n\n const result = await this.flush()\n this.destroyed = true\n\n if (this.config.debug) {\n console.error('[metrone] Server SDK shut down')\n }\n\n return result\n }\n\n /** Number of events currently in the send queue. */\n get queueSize(): number {\n return this.queue.length\n }\n\n /** Whether shutdown() has been called. */\n get isDestroyed(): boolean {\n return this.destroyed\n }\n\n // ─── Internal ──────────────────────────────────────────────────────────────\n\n private assertNotDestroyed(): void {\n if (this.destroyed) {\n throw new MetroneConfigError('Client has been shut down. Create a new instance.')\n }\n }\n\n private buildDateParams(\n params?: { days?: number; from?: string | Date; to?: string | Date },\n ): Record<string, string | number | undefined> {\n const result: Record<string, string | number | undefined> = {}\n if (params?.days) result.days = params.days\n if (params?.from) result.from = params.from instanceof Date ? params.from.toISOString() : params.from\n if (params?.to) result.to = params.to instanceof Date ? params.to.toISOString() : params.to\n return result\n }\n}\n","/**\n * agent-tracker.ts — server-side capture of AI agent traffic.\n *\n * AI agents (GPTBot, ChatGPT-User, PerplexityBot, ClaudeBot, …) fetch pages\n * without executing JavaScript, so the browser tracker never sees them. This\n * module runs where the request is actually served — an Express/Connect\n * middleware, a fetch-style handler wrapper, or a manual call — and forwards\n * matched requests to Metrone with the original User-Agent in the\n * `user_agent` payload field. The ingestion worker performs the\n * authoritative classification (visitor_type = 'ai_agent') and the raw UA\n * is never stored.\n *\n * Only requests whose UA matches the AI-agent list are forwarded — normal\n * human traffic is never sent from here (the browser tracker handles it),\n * so this adds zero overhead and zero quota usage for regular visitors.\n *\n * Keep the UA list in sync with AI_AGENT_RULES in\n * packages/worker/src/lib/device-parser.ts (the authoritative matcher).\n */\n\nimport type { MetroneServer } from './client.js'\n\nconst AI_AGENT_RULES: Array<{ pattern: RegExp; name: string }> = [\n // OpenAI\n { pattern: /chatgpt-user/i, name: 'ChatGPT-User' },\n { pattern: /oai-searchbot/i, name: 'OAI-SearchBot' },\n { pattern: /gptbot/i, name: 'GPTBot' },\n // Anthropic\n { pattern: /claude-user/i, name: 'Claude-User' },\n { pattern: /claude-searchbot/i, name: 'Claude-SearchBot' },\n { pattern: /claudebot/i, name: 'ClaudeBot' },\n { pattern: /claude-web/i, name: 'Claude-Web' },\n { pattern: /anthropic-ai/i, name: 'anthropic-ai' },\n // Perplexity\n { pattern: /perplexity-user/i, name: 'Perplexity-User' },\n { pattern: /perplexitybot/i, name: 'PerplexityBot' },\n // Google AI\n { pattern: /google-cloudvertexbot/i, name: 'Google-CloudVertexBot' },\n { pattern: /googleother/i, name: 'GoogleOther' },\n { pattern: /gemini-deep-research/i, name: 'Gemini-Deep-Research' },\n // Meta\n { pattern: /meta-externalagent/i, name: 'Meta-ExternalAgent' },\n { pattern: /meta-externalfetcher/i, name: 'Meta-ExternalFetcher' },\n // Microsoft Copilot\n { pattern: /bingpreview/i, name: 'BingPreview' },\n // xAI\n { pattern: /grokbot/i, name: 'GrokBot' },\n { pattern: /xai-crawler/i, name: 'xAI-Crawler' },\n // Others\n { pattern: /duckassistbot/i, name: 'DuckAssistBot' },\n { pattern: /mistralai-user/i, name: 'MistralAI-User' },\n { pattern: /cohere-training-data-crawler/i, name: 'Cohere-Training-Crawler' },\n { pattern: /cohere-ai/i, name: 'cohere-ai' },\n { pattern: /bytespider/i, name: 'Bytespider' },\n { pattern: /amazonbot/i, name: 'Amazonbot' },\n { pattern: /applebot-extended/i, name: 'Applebot-Extended' },\n { pattern: /ccbot/i, name: 'CCBot' },\n { pattern: /youbot/i, name: 'YouBot' },\n { pattern: /timpibot/i, name: 'TimpiBot' },\n { pattern: /diffbot/i, name: 'Diffbot' },\n]\n\n/** Static-asset extensions that are noise even when fetched by an agent. */\nconst STATIC_ASSET_PATTERN = /\\.(css|js|mjs|json|xml|txt|ico|png|jpe?g|gif|svg|webp|avif|woff2?|ttf|otf|eot|map|pdf|zip|gz|mp4|webm|mp3)$/i\n\n/**\n * Returns the canonical AI agent name for a User-Agent, or undefined when\n * the UA does not belong to a known AI agent.\n */\nexport function matchAiAgent(userAgent: string | null | undefined): string | undefined {\n if (!userAgent) return undefined\n for (const rule of AI_AGENT_RULES) {\n if (rule.pattern.test(userAgent)) return rule.name\n }\n return undefined\n}\n\nexport interface AgentHit {\n /** Full request URL (or at minimum a path). */\n url: string\n /** The visitor's User-Agent header. */\n userAgent: string | null | undefined\n /** Optional Referer header. */\n referrer?: string | null\n /** HTTP method — non-GET/HEAD requests are skipped. Defaults to 'GET'. */\n method?: string\n}\n\nexport interface AgentTrackerOptions {\n /**\n * Skip requests for static assets (css/js/images/fonts/…). Default: true.\n */\n skipStaticAssets?: boolean\n /**\n * Extra paths to ignore (exact match or prefix ending with '*'),\n * e.g. ['/health', '/api/*'].\n */\n ignorePaths?: string[]\n}\n\nfunction pathFromUrl(url: string): string {\n try {\n return new URL(url, 'http://localhost').pathname\n } catch {\n return url.split('?')[0] ?? url\n }\n}\n\nfunction isIgnored(path: string, options?: AgentTrackerOptions): boolean {\n if (options?.skipStaticAssets !== false && STATIC_ASSET_PATTERN.test(path)) return true\n for (const rule of options?.ignorePaths ?? []) {\n if (rule.endsWith('*') ? path.startsWith(rule.slice(0, -1)) : path === rule) return true\n }\n return false\n}\n\n/**\n * Core capture: if the request comes from a known AI agent, forward it to\n * Metrone as a pageview with the original UA. Fire-and-forget — never\n * throws, never blocks the response.\n *\n * Returns the matched agent name, or undefined when the request was not an\n * AI agent (or was filtered out).\n *\n * Serverless note: the SDK batches by default; in short-lived environments\n * construct the client with `batchSize: 0` so hits are sent immediately.\n */\nexport function captureAgentHit(\n metrone: MetroneServer,\n hit: AgentHit,\n options?: AgentTrackerOptions,\n): string | undefined {\n try {\n const method = (hit.method ?? 'GET').toUpperCase()\n if (method !== 'GET' && method !== 'HEAD') return undefined\n\n const agent = matchAiAgent(hit.userAgent)\n if (!agent) return undefined\n\n const path = pathFromUrl(hit.url)\n if (isIgnored(path, options)) return undefined\n\n metrone.track('pageview', {\n source: 'web',\n page_url: hit.url,\n page_path: path,\n referrer: hit.referrer ?? undefined,\n user_agent: hit.userAgent ?? undefined,\n properties: { agent_capture: 'server' },\n })\n return agent\n } catch {\n // Capture must never break the customer's request handling.\n return undefined\n }\n}\n\n/**\n * Express / Connect middleware. Mount early, before routing:\n *\n * ```ts\n * import { MetroneServer, agentMiddleware } from '@metrone-io/server'\n *\n * const metrone = new MetroneServer({ apiKey: process.env.METRONE_API_KEY! })\n * app.use(agentMiddleware(metrone))\n * ```\n */\nexport function agentMiddleware(metrone: MetroneServer, options?: AgentTrackerOptions) {\n return function metroneAgentMiddleware(\n req: { method?: string; originalUrl?: string; url?: string; headers: Record<string, string | string[] | undefined>; protocol?: string; get?: (name: string) => string | undefined },\n _res: unknown,\n next: (err?: unknown) => void,\n ): void {\n try {\n const header = (name: string): string | undefined => {\n const v = req.headers[name]\n return Array.isArray(v) ? v[0] : v\n }\n const host = header('host') ?? 'localhost'\n const proto = req.protocol ?? (header('x-forwarded-proto') ?? 'https')\n const rawPath = req.originalUrl ?? req.url ?? '/'\n\n captureAgentHit(metrone, {\n url: `${proto}://${host}${rawPath}`,\n userAgent: header('user-agent'),\n referrer: header('referer') ?? header('referrer'),\n method: req.method,\n }, options)\n } catch {\n // never interfere with the request\n }\n next()\n }\n}\n\n/**\n * WHATWG-fetch helper for Hono, Next.js middleware/route handlers, Bun,\n * Deno, and Cloudflare Workers:\n *\n * ```ts\n * trackAgentRequest(metrone, request)\n * ```\n */\nexport function trackAgentRequest(\n metrone: MetroneServer,\n request: { url: string; method?: string; headers: { get(name: string): string | null } },\n options?: AgentTrackerOptions,\n): string | undefined {\n return captureAgentHit(metrone, {\n url: request.url,\n userAgent: request.headers.get('user-agent'),\n referrer: request.headers.get('referer'),\n method: request.method,\n }, options)\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
* These types mirror the Worker's RawEventPayload exactly so payloads pass
|
|
5
5
|
* validation on the ingestion side without translation.
|
|
6
6
|
*/
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Canonical source values. Mirrors the analytics_events.source CHECK
|
|
9
|
+
* constraint (expanded in v1.1 by migration _069).
|
|
10
|
+
* v1.1 SPEC-01 adds: campaign, social, review, ad, agent_action.
|
|
11
|
+
*/
|
|
12
|
+
type EventSource = 'web' | 'voice' | 'chat' | 'assistant' | 'sms' | 'listing' | 'campaign' | 'social' | 'review' | 'ad' | 'agent_action';
|
|
8
13
|
interface MetroneServerConfig {
|
|
9
14
|
/** API key (required). Format: metrone_live_* or metrone_test_* */
|
|
10
15
|
apiKey: string;
|
|
@@ -52,6 +57,13 @@ interface EventPayload {
|
|
|
52
57
|
agent_type?: string;
|
|
53
58
|
/** Idempotency key to prevent duplicate processing */
|
|
54
59
|
idempotency_key?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Original visitor User-Agent, for middleware/edge senders forwarding
|
|
62
|
+
* traffic they observed. The worker parses it for device and visitor
|
|
63
|
+
* classification (human vs ai_agent vs bot) and then discards it — the
|
|
64
|
+
* raw UA is never stored. Without it, the ingest request's own UA is used.
|
|
65
|
+
*/
|
|
66
|
+
user_agent?: string;
|
|
55
67
|
properties?: Record<string, unknown>;
|
|
56
68
|
timestamp?: string;
|
|
57
69
|
}
|
|
@@ -271,6 +283,67 @@ declare class MetroneServer {
|
|
|
271
283
|
* Track an AI session lifecycle event (start, end, timeout).
|
|
272
284
|
*/
|
|
273
285
|
trackAISession(data: AISessionData): void;
|
|
286
|
+
/**
|
|
287
|
+
* Track a campaign lifecycle event (email / SMS sends, opens, clicks…).
|
|
288
|
+
* Recommended event_type values: email_sent | email_delivered |
|
|
289
|
+
* email_opened | email_clicked | email_bounced | email_unsubscribed |
|
|
290
|
+
* sms_sent | sms_delivered | sms_replied | sms_opt_out
|
|
291
|
+
*/
|
|
292
|
+
trackCampaignEvent(data: {
|
|
293
|
+
event_type: string;
|
|
294
|
+
campaign_id: string;
|
|
295
|
+
recipient_id_hash: string;
|
|
296
|
+
} & Record<string, unknown>): void;
|
|
297
|
+
/**
|
|
298
|
+
* Track a social media event. Recommended event_type values:
|
|
299
|
+
* post_published | impression | engagement | click | share
|
|
300
|
+
*/
|
|
301
|
+
trackSocialEvent(data: {
|
|
302
|
+
event_type: string;
|
|
303
|
+
platform: string;
|
|
304
|
+
post_id: string;
|
|
305
|
+
} & Record<string, unknown>): void;
|
|
306
|
+
/**
|
|
307
|
+
* Track a review event. Recommended event_type values: received | responded
|
|
308
|
+
*/
|
|
309
|
+
trackReviewEvent(data: {
|
|
310
|
+
event_type: string;
|
|
311
|
+
platform: string;
|
|
312
|
+
review_id: string;
|
|
313
|
+
rating: number;
|
|
314
|
+
} & Record<string, unknown>): void;
|
|
315
|
+
/**
|
|
316
|
+
* Track a paid-ad metrics event. Recommended event_type values:
|
|
317
|
+
* impression | click | spend_daily | conversion
|
|
318
|
+
*/
|
|
319
|
+
trackAdEvent(data: {
|
|
320
|
+
event_type: string;
|
|
321
|
+
platform: string;
|
|
322
|
+
campaign_id: string;
|
|
323
|
+
} & Record<string, unknown>): void;
|
|
324
|
+
/**
|
|
325
|
+
* Track an SMS conversation event. Recommended event_type values:
|
|
326
|
+
* received | replied. agent_id is promoted to the top-level column so
|
|
327
|
+
* existing per-agent dashboard breakdowns keep working.
|
|
328
|
+
*/
|
|
329
|
+
trackSmsEvent(data: {
|
|
330
|
+
event_type: string;
|
|
331
|
+
agent_id: string;
|
|
332
|
+
} & Record<string, unknown>): void;
|
|
333
|
+
/**
|
|
334
|
+
* Track an autonomous agent action (audit-log style). event_type is the
|
|
335
|
+
* action itself (free-form vocabulary). agent_type / agent_id are written
|
|
336
|
+
* BOTH to the top-level columns (for existing dashboard breakdowns) AND
|
|
337
|
+
* into properties (for the JSONB-first get_agent_activity_feed RPC) —
|
|
338
|
+
* decision B4 of the v1.1 build.
|
|
339
|
+
*/
|
|
340
|
+
trackAgentAction(data: {
|
|
341
|
+
agent_type: string;
|
|
342
|
+
action_taken: string;
|
|
343
|
+
agent_id?: string;
|
|
344
|
+
target?: string;
|
|
345
|
+
outcome?: string;
|
|
346
|
+
} & Record<string, unknown>): void;
|
|
274
347
|
/**
|
|
275
348
|
* Get aggregated analytics stats for a time period.
|
|
276
349
|
*/
|
|
@@ -309,6 +382,98 @@ declare class MetroneServer {
|
|
|
309
382
|
private buildDateParams;
|
|
310
383
|
}
|
|
311
384
|
|
|
385
|
+
/**
|
|
386
|
+
* agent-tracker.ts — server-side capture of AI agent traffic.
|
|
387
|
+
*
|
|
388
|
+
* AI agents (GPTBot, ChatGPT-User, PerplexityBot, ClaudeBot, …) fetch pages
|
|
389
|
+
* without executing JavaScript, so the browser tracker never sees them. This
|
|
390
|
+
* module runs where the request is actually served — an Express/Connect
|
|
391
|
+
* middleware, a fetch-style handler wrapper, or a manual call — and forwards
|
|
392
|
+
* matched requests to Metrone with the original User-Agent in the
|
|
393
|
+
* `user_agent` payload field. The ingestion worker performs the
|
|
394
|
+
* authoritative classification (visitor_type = 'ai_agent') and the raw UA
|
|
395
|
+
* is never stored.
|
|
396
|
+
*
|
|
397
|
+
* Only requests whose UA matches the AI-agent list are forwarded — normal
|
|
398
|
+
* human traffic is never sent from here (the browser tracker handles it),
|
|
399
|
+
* so this adds zero overhead and zero quota usage for regular visitors.
|
|
400
|
+
*
|
|
401
|
+
* Keep the UA list in sync with AI_AGENT_RULES in
|
|
402
|
+
* packages/worker/src/lib/device-parser.ts (the authoritative matcher).
|
|
403
|
+
*/
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Returns the canonical AI agent name for a User-Agent, or undefined when
|
|
407
|
+
* the UA does not belong to a known AI agent.
|
|
408
|
+
*/
|
|
409
|
+
declare function matchAiAgent(userAgent: string | null | undefined): string | undefined;
|
|
410
|
+
interface AgentHit {
|
|
411
|
+
/** Full request URL (or at minimum a path). */
|
|
412
|
+
url: string;
|
|
413
|
+
/** The visitor's User-Agent header. */
|
|
414
|
+
userAgent: string | null | undefined;
|
|
415
|
+
/** Optional Referer header. */
|
|
416
|
+
referrer?: string | null;
|
|
417
|
+
/** HTTP method — non-GET/HEAD requests are skipped. Defaults to 'GET'. */
|
|
418
|
+
method?: string;
|
|
419
|
+
}
|
|
420
|
+
interface AgentTrackerOptions {
|
|
421
|
+
/**
|
|
422
|
+
* Skip requests for static assets (css/js/images/fonts/…). Default: true.
|
|
423
|
+
*/
|
|
424
|
+
skipStaticAssets?: boolean;
|
|
425
|
+
/**
|
|
426
|
+
* Extra paths to ignore (exact match or prefix ending with '*'),
|
|
427
|
+
* e.g. ['/health', '/api/*'].
|
|
428
|
+
*/
|
|
429
|
+
ignorePaths?: string[];
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Core capture: if the request comes from a known AI agent, forward it to
|
|
433
|
+
* Metrone as a pageview with the original UA. Fire-and-forget — never
|
|
434
|
+
* throws, never blocks the response.
|
|
435
|
+
*
|
|
436
|
+
* Returns the matched agent name, or undefined when the request was not an
|
|
437
|
+
* AI agent (or was filtered out).
|
|
438
|
+
*
|
|
439
|
+
* Serverless note: the SDK batches by default; in short-lived environments
|
|
440
|
+
* construct the client with `batchSize: 0` so hits are sent immediately.
|
|
441
|
+
*/
|
|
442
|
+
declare function captureAgentHit(metrone: MetroneServer, hit: AgentHit, options?: AgentTrackerOptions): string | undefined;
|
|
443
|
+
/**
|
|
444
|
+
* Express / Connect middleware. Mount early, before routing:
|
|
445
|
+
*
|
|
446
|
+
* ```ts
|
|
447
|
+
* import { MetroneServer, agentMiddleware } from '@metrone-io/server'
|
|
448
|
+
*
|
|
449
|
+
* const metrone = new MetroneServer({ apiKey: process.env.METRONE_API_KEY! })
|
|
450
|
+
* app.use(agentMiddleware(metrone))
|
|
451
|
+
* ```
|
|
452
|
+
*/
|
|
453
|
+
declare function agentMiddleware(metrone: MetroneServer, options?: AgentTrackerOptions): (req: {
|
|
454
|
+
method?: string;
|
|
455
|
+
originalUrl?: string;
|
|
456
|
+
url?: string;
|
|
457
|
+
headers: Record<string, string | string[] | undefined>;
|
|
458
|
+
protocol?: string;
|
|
459
|
+
get?: (name: string) => string | undefined;
|
|
460
|
+
}, _res: unknown, next: (err?: unknown) => void) => void;
|
|
461
|
+
/**
|
|
462
|
+
* WHATWG-fetch helper for Hono, Next.js middleware/route handlers, Bun,
|
|
463
|
+
* Deno, and Cloudflare Workers:
|
|
464
|
+
*
|
|
465
|
+
* ```ts
|
|
466
|
+
* trackAgentRequest(metrone, request)
|
|
467
|
+
* ```
|
|
468
|
+
*/
|
|
469
|
+
declare function trackAgentRequest(metrone: MetroneServer, request: {
|
|
470
|
+
url: string;
|
|
471
|
+
method?: string;
|
|
472
|
+
headers: {
|
|
473
|
+
get(name: string): string | null;
|
|
474
|
+
};
|
|
475
|
+
}, options?: AgentTrackerOptions): string | undefined;
|
|
476
|
+
|
|
312
477
|
/**
|
|
313
478
|
* Structured error types for the Metrone server SDK.
|
|
314
479
|
*
|
|
@@ -354,4 +519,4 @@ declare class MetroneServerError extends MetroneError {
|
|
|
354
519
|
constructor(status: number, message?: string);
|
|
355
520
|
}
|
|
356
521
|
|
|
357
|
-
export { type AICallData, type AIChatData, type AIIntentData, type AISessionData, type ApiError, type ApiResponse, type ChannelRow, type ConversionData, type EventPayload, type EventRow, type EventSource, type EventsParams, type EventsResponse, type FlushResult, type LiveResponse, MetroneAuthError, MetroneConfigError, MetroneError, MetroneNetworkError, MetroneQuotaError, MetroneRateLimitError, MetroneServer, type MetroneServerConfig, MetroneServerError, MetroneTimeoutError, MetroneValidationError, type PageRow, type PagesParams, type PagesResponse, type ReferrerRow, type SourcesParams, type SourcesResponse, type StatsParams, type StatsResponse };
|
|
522
|
+
export { type AICallData, type AIChatData, type AIIntentData, type AISessionData, type AgentHit, type AgentTrackerOptions, type ApiError, type ApiResponse, type ChannelRow, type ConversionData, type EventPayload, type EventRow, type EventSource, type EventsParams, type EventsResponse, type FlushResult, type LiveResponse, MetroneAuthError, MetroneConfigError, MetroneError, MetroneNetworkError, MetroneQuotaError, MetroneRateLimitError, MetroneServer, type MetroneServerConfig, MetroneServerError, MetroneTimeoutError, MetroneValidationError, type PageRow, type PagesParams, type PagesResponse, type ReferrerRow, type SourcesParams, type SourcesResponse, type StatsParams, type StatsResponse, agentMiddleware, captureAgentHit, matchAiAgent, trackAgentRequest };
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
* These types mirror the Worker's RawEventPayload exactly so payloads pass
|
|
5
5
|
* validation on the ingestion side without translation.
|
|
6
6
|
*/
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Canonical source values. Mirrors the analytics_events.source CHECK
|
|
9
|
+
* constraint (expanded in v1.1 by migration _069).
|
|
10
|
+
* v1.1 SPEC-01 adds: campaign, social, review, ad, agent_action.
|
|
11
|
+
*/
|
|
12
|
+
type EventSource = 'web' | 'voice' | 'chat' | 'assistant' | 'sms' | 'listing' | 'campaign' | 'social' | 'review' | 'ad' | 'agent_action';
|
|
8
13
|
interface MetroneServerConfig {
|
|
9
14
|
/** API key (required). Format: metrone_live_* or metrone_test_* */
|
|
10
15
|
apiKey: string;
|
|
@@ -52,6 +57,13 @@ interface EventPayload {
|
|
|
52
57
|
agent_type?: string;
|
|
53
58
|
/** Idempotency key to prevent duplicate processing */
|
|
54
59
|
idempotency_key?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Original visitor User-Agent, for middleware/edge senders forwarding
|
|
62
|
+
* traffic they observed. The worker parses it for device and visitor
|
|
63
|
+
* classification (human vs ai_agent vs bot) and then discards it — the
|
|
64
|
+
* raw UA is never stored. Without it, the ingest request's own UA is used.
|
|
65
|
+
*/
|
|
66
|
+
user_agent?: string;
|
|
55
67
|
properties?: Record<string, unknown>;
|
|
56
68
|
timestamp?: string;
|
|
57
69
|
}
|
|
@@ -271,6 +283,67 @@ declare class MetroneServer {
|
|
|
271
283
|
* Track an AI session lifecycle event (start, end, timeout).
|
|
272
284
|
*/
|
|
273
285
|
trackAISession(data: AISessionData): void;
|
|
286
|
+
/**
|
|
287
|
+
* Track a campaign lifecycle event (email / SMS sends, opens, clicks…).
|
|
288
|
+
* Recommended event_type values: email_sent | email_delivered |
|
|
289
|
+
* email_opened | email_clicked | email_bounced | email_unsubscribed |
|
|
290
|
+
* sms_sent | sms_delivered | sms_replied | sms_opt_out
|
|
291
|
+
*/
|
|
292
|
+
trackCampaignEvent(data: {
|
|
293
|
+
event_type: string;
|
|
294
|
+
campaign_id: string;
|
|
295
|
+
recipient_id_hash: string;
|
|
296
|
+
} & Record<string, unknown>): void;
|
|
297
|
+
/**
|
|
298
|
+
* Track a social media event. Recommended event_type values:
|
|
299
|
+
* post_published | impression | engagement | click | share
|
|
300
|
+
*/
|
|
301
|
+
trackSocialEvent(data: {
|
|
302
|
+
event_type: string;
|
|
303
|
+
platform: string;
|
|
304
|
+
post_id: string;
|
|
305
|
+
} & Record<string, unknown>): void;
|
|
306
|
+
/**
|
|
307
|
+
* Track a review event. Recommended event_type values: received | responded
|
|
308
|
+
*/
|
|
309
|
+
trackReviewEvent(data: {
|
|
310
|
+
event_type: string;
|
|
311
|
+
platform: string;
|
|
312
|
+
review_id: string;
|
|
313
|
+
rating: number;
|
|
314
|
+
} & Record<string, unknown>): void;
|
|
315
|
+
/**
|
|
316
|
+
* Track a paid-ad metrics event. Recommended event_type values:
|
|
317
|
+
* impression | click | spend_daily | conversion
|
|
318
|
+
*/
|
|
319
|
+
trackAdEvent(data: {
|
|
320
|
+
event_type: string;
|
|
321
|
+
platform: string;
|
|
322
|
+
campaign_id: string;
|
|
323
|
+
} & Record<string, unknown>): void;
|
|
324
|
+
/**
|
|
325
|
+
* Track an SMS conversation event. Recommended event_type values:
|
|
326
|
+
* received | replied. agent_id is promoted to the top-level column so
|
|
327
|
+
* existing per-agent dashboard breakdowns keep working.
|
|
328
|
+
*/
|
|
329
|
+
trackSmsEvent(data: {
|
|
330
|
+
event_type: string;
|
|
331
|
+
agent_id: string;
|
|
332
|
+
} & Record<string, unknown>): void;
|
|
333
|
+
/**
|
|
334
|
+
* Track an autonomous agent action (audit-log style). event_type is the
|
|
335
|
+
* action itself (free-form vocabulary). agent_type / agent_id are written
|
|
336
|
+
* BOTH to the top-level columns (for existing dashboard breakdowns) AND
|
|
337
|
+
* into properties (for the JSONB-first get_agent_activity_feed RPC) —
|
|
338
|
+
* decision B4 of the v1.1 build.
|
|
339
|
+
*/
|
|
340
|
+
trackAgentAction(data: {
|
|
341
|
+
agent_type: string;
|
|
342
|
+
action_taken: string;
|
|
343
|
+
agent_id?: string;
|
|
344
|
+
target?: string;
|
|
345
|
+
outcome?: string;
|
|
346
|
+
} & Record<string, unknown>): void;
|
|
274
347
|
/**
|
|
275
348
|
* Get aggregated analytics stats for a time period.
|
|
276
349
|
*/
|
|
@@ -309,6 +382,98 @@ declare class MetroneServer {
|
|
|
309
382
|
private buildDateParams;
|
|
310
383
|
}
|
|
311
384
|
|
|
385
|
+
/**
|
|
386
|
+
* agent-tracker.ts — server-side capture of AI agent traffic.
|
|
387
|
+
*
|
|
388
|
+
* AI agents (GPTBot, ChatGPT-User, PerplexityBot, ClaudeBot, …) fetch pages
|
|
389
|
+
* without executing JavaScript, so the browser tracker never sees them. This
|
|
390
|
+
* module runs where the request is actually served — an Express/Connect
|
|
391
|
+
* middleware, a fetch-style handler wrapper, or a manual call — and forwards
|
|
392
|
+
* matched requests to Metrone with the original User-Agent in the
|
|
393
|
+
* `user_agent` payload field. The ingestion worker performs the
|
|
394
|
+
* authoritative classification (visitor_type = 'ai_agent') and the raw UA
|
|
395
|
+
* is never stored.
|
|
396
|
+
*
|
|
397
|
+
* Only requests whose UA matches the AI-agent list are forwarded — normal
|
|
398
|
+
* human traffic is never sent from here (the browser tracker handles it),
|
|
399
|
+
* so this adds zero overhead and zero quota usage for regular visitors.
|
|
400
|
+
*
|
|
401
|
+
* Keep the UA list in sync with AI_AGENT_RULES in
|
|
402
|
+
* packages/worker/src/lib/device-parser.ts (the authoritative matcher).
|
|
403
|
+
*/
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Returns the canonical AI agent name for a User-Agent, or undefined when
|
|
407
|
+
* the UA does not belong to a known AI agent.
|
|
408
|
+
*/
|
|
409
|
+
declare function matchAiAgent(userAgent: string | null | undefined): string | undefined;
|
|
410
|
+
interface AgentHit {
|
|
411
|
+
/** Full request URL (or at minimum a path). */
|
|
412
|
+
url: string;
|
|
413
|
+
/** The visitor's User-Agent header. */
|
|
414
|
+
userAgent: string | null | undefined;
|
|
415
|
+
/** Optional Referer header. */
|
|
416
|
+
referrer?: string | null;
|
|
417
|
+
/** HTTP method — non-GET/HEAD requests are skipped. Defaults to 'GET'. */
|
|
418
|
+
method?: string;
|
|
419
|
+
}
|
|
420
|
+
interface AgentTrackerOptions {
|
|
421
|
+
/**
|
|
422
|
+
* Skip requests for static assets (css/js/images/fonts/…). Default: true.
|
|
423
|
+
*/
|
|
424
|
+
skipStaticAssets?: boolean;
|
|
425
|
+
/**
|
|
426
|
+
* Extra paths to ignore (exact match or prefix ending with '*'),
|
|
427
|
+
* e.g. ['/health', '/api/*'].
|
|
428
|
+
*/
|
|
429
|
+
ignorePaths?: string[];
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Core capture: if the request comes from a known AI agent, forward it to
|
|
433
|
+
* Metrone as a pageview with the original UA. Fire-and-forget — never
|
|
434
|
+
* throws, never blocks the response.
|
|
435
|
+
*
|
|
436
|
+
* Returns the matched agent name, or undefined when the request was not an
|
|
437
|
+
* AI agent (or was filtered out).
|
|
438
|
+
*
|
|
439
|
+
* Serverless note: the SDK batches by default; in short-lived environments
|
|
440
|
+
* construct the client with `batchSize: 0` so hits are sent immediately.
|
|
441
|
+
*/
|
|
442
|
+
declare function captureAgentHit(metrone: MetroneServer, hit: AgentHit, options?: AgentTrackerOptions): string | undefined;
|
|
443
|
+
/**
|
|
444
|
+
* Express / Connect middleware. Mount early, before routing:
|
|
445
|
+
*
|
|
446
|
+
* ```ts
|
|
447
|
+
* import { MetroneServer, agentMiddleware } from '@metrone-io/server'
|
|
448
|
+
*
|
|
449
|
+
* const metrone = new MetroneServer({ apiKey: process.env.METRONE_API_KEY! })
|
|
450
|
+
* app.use(agentMiddleware(metrone))
|
|
451
|
+
* ```
|
|
452
|
+
*/
|
|
453
|
+
declare function agentMiddleware(metrone: MetroneServer, options?: AgentTrackerOptions): (req: {
|
|
454
|
+
method?: string;
|
|
455
|
+
originalUrl?: string;
|
|
456
|
+
url?: string;
|
|
457
|
+
headers: Record<string, string | string[] | undefined>;
|
|
458
|
+
protocol?: string;
|
|
459
|
+
get?: (name: string) => string | undefined;
|
|
460
|
+
}, _res: unknown, next: (err?: unknown) => void) => void;
|
|
461
|
+
/**
|
|
462
|
+
* WHATWG-fetch helper for Hono, Next.js middleware/route handlers, Bun,
|
|
463
|
+
* Deno, and Cloudflare Workers:
|
|
464
|
+
*
|
|
465
|
+
* ```ts
|
|
466
|
+
* trackAgentRequest(metrone, request)
|
|
467
|
+
* ```
|
|
468
|
+
*/
|
|
469
|
+
declare function trackAgentRequest(metrone: MetroneServer, request: {
|
|
470
|
+
url: string;
|
|
471
|
+
method?: string;
|
|
472
|
+
headers: {
|
|
473
|
+
get(name: string): string | null;
|
|
474
|
+
};
|
|
475
|
+
}, options?: AgentTrackerOptions): string | undefined;
|
|
476
|
+
|
|
312
477
|
/**
|
|
313
478
|
* Structured error types for the Metrone server SDK.
|
|
314
479
|
*
|
|
@@ -354,4 +519,4 @@ declare class MetroneServerError extends MetroneError {
|
|
|
354
519
|
constructor(status: number, message?: string);
|
|
355
520
|
}
|
|
356
521
|
|
|
357
|
-
export { type AICallData, type AIChatData, type AIIntentData, type AISessionData, type ApiError, type ApiResponse, type ChannelRow, type ConversionData, type EventPayload, type EventRow, type EventSource, type EventsParams, type EventsResponse, type FlushResult, type LiveResponse, MetroneAuthError, MetroneConfigError, MetroneError, MetroneNetworkError, MetroneQuotaError, MetroneRateLimitError, MetroneServer, type MetroneServerConfig, MetroneServerError, MetroneTimeoutError, MetroneValidationError, type PageRow, type PagesParams, type PagesResponse, type ReferrerRow, type SourcesParams, type SourcesResponse, type StatsParams, type StatsResponse };
|
|
522
|
+
export { type AICallData, type AIChatData, type AIIntentData, type AISessionData, type AgentHit, type AgentTrackerOptions, type ApiError, type ApiResponse, type ChannelRow, type ConversionData, type EventPayload, type EventRow, type EventSource, type EventsParams, type EventsResponse, type FlushResult, type LiveResponse, MetroneAuthError, MetroneConfigError, MetroneError, MetroneNetworkError, MetroneQuotaError, MetroneRateLimitError, MetroneServer, type MetroneServerConfig, MetroneServerError, MetroneTimeoutError, MetroneValidationError, type PageRow, type PagesParams, type PagesResponse, type ReferrerRow, type SourcesParams, type SourcesResponse, type StatsParams, type StatsResponse, agentMiddleware, captureAgentHit, matchAiAgent, trackAgentRequest };
|
package/dist/index.js
CHANGED
|
@@ -338,6 +338,68 @@ var MetroneServer = class {
|
|
|
338
338
|
properties: data.properties
|
|
339
339
|
});
|
|
340
340
|
}
|
|
341
|
+
// ─── v1.1 source helpers (SPEC-01) ─────────────────────────────────────────
|
|
342
|
+
// Thin ergonomic wrappers over track(). Required fields are typed; any
|
|
343
|
+
// extra keys flow into the JSONB `properties` payload (storage acceptance
|
|
344
|
+
// is permissive — see @metrone/schemas per-source recommended shapes).
|
|
345
|
+
/**
|
|
346
|
+
* Track a campaign lifecycle event (email / SMS sends, opens, clicks…).
|
|
347
|
+
* Recommended event_type values: email_sent | email_delivered |
|
|
348
|
+
* email_opened | email_clicked | email_bounced | email_unsubscribed |
|
|
349
|
+
* sms_sent | sms_delivered | sms_replied | sms_opt_out
|
|
350
|
+
*/
|
|
351
|
+
trackCampaignEvent(data) {
|
|
352
|
+
const { event_type, ...properties } = data;
|
|
353
|
+
this.track(event_type, { source: "campaign", properties });
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Track a social media event. Recommended event_type values:
|
|
357
|
+
* post_published | impression | engagement | click | share
|
|
358
|
+
*/
|
|
359
|
+
trackSocialEvent(data) {
|
|
360
|
+
const { event_type, ...properties } = data;
|
|
361
|
+
this.track(event_type, { source: "social", properties });
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Track a review event. Recommended event_type values: received | responded
|
|
365
|
+
*/
|
|
366
|
+
trackReviewEvent(data) {
|
|
367
|
+
const { event_type, ...properties } = data;
|
|
368
|
+
this.track(event_type, { source: "review", properties });
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Track a paid-ad metrics event. Recommended event_type values:
|
|
372
|
+
* impression | click | spend_daily | conversion
|
|
373
|
+
*/
|
|
374
|
+
trackAdEvent(data) {
|
|
375
|
+
const { event_type, ...properties } = data;
|
|
376
|
+
this.track(event_type, { source: "ad", properties });
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Track an SMS conversation event. Recommended event_type values:
|
|
380
|
+
* received | replied. agent_id is promoted to the top-level column so
|
|
381
|
+
* existing per-agent dashboard breakdowns keep working.
|
|
382
|
+
*/
|
|
383
|
+
trackSmsEvent(data) {
|
|
384
|
+
const { event_type, agent_id, ...properties } = data;
|
|
385
|
+
this.track(event_type, { source: "sms", agent_id, properties });
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Track an autonomous agent action (audit-log style). event_type is the
|
|
389
|
+
* action itself (free-form vocabulary). agent_type / agent_id are written
|
|
390
|
+
* BOTH to the top-level columns (for existing dashboard breakdowns) AND
|
|
391
|
+
* into properties (for the JSONB-first get_agent_activity_feed RPC) —
|
|
392
|
+
* decision B4 of the v1.1 build.
|
|
393
|
+
*/
|
|
394
|
+
trackAgentAction(data) {
|
|
395
|
+
const { agent_type, action_taken, agent_id, ...rest } = data;
|
|
396
|
+
this.track(action_taken, {
|
|
397
|
+
source: "agent_action",
|
|
398
|
+
agent_id,
|
|
399
|
+
agent_type,
|
|
400
|
+
properties: { agent_type, action_taken, ...rest }
|
|
401
|
+
});
|
|
402
|
+
}
|
|
341
403
|
// ─── Read API ──────────────────────────────────────────────────────────────
|
|
342
404
|
/**
|
|
343
405
|
* Get aggregated analytics stats for a time period.
|
|
@@ -503,6 +565,119 @@ var MetroneServer = class {
|
|
|
503
565
|
return result;
|
|
504
566
|
}
|
|
505
567
|
};
|
|
568
|
+
|
|
569
|
+
// src/agent-tracker.ts
|
|
570
|
+
var AI_AGENT_RULES = [
|
|
571
|
+
// OpenAI
|
|
572
|
+
{ pattern: /chatgpt-user/i, name: "ChatGPT-User" },
|
|
573
|
+
{ pattern: /oai-searchbot/i, name: "OAI-SearchBot" },
|
|
574
|
+
{ pattern: /gptbot/i, name: "GPTBot" },
|
|
575
|
+
// Anthropic
|
|
576
|
+
{ pattern: /claude-user/i, name: "Claude-User" },
|
|
577
|
+
{ pattern: /claude-searchbot/i, name: "Claude-SearchBot" },
|
|
578
|
+
{ pattern: /claudebot/i, name: "ClaudeBot" },
|
|
579
|
+
{ pattern: /claude-web/i, name: "Claude-Web" },
|
|
580
|
+
{ pattern: /anthropic-ai/i, name: "anthropic-ai" },
|
|
581
|
+
// Perplexity
|
|
582
|
+
{ pattern: /perplexity-user/i, name: "Perplexity-User" },
|
|
583
|
+
{ pattern: /perplexitybot/i, name: "PerplexityBot" },
|
|
584
|
+
// Google AI
|
|
585
|
+
{ pattern: /google-cloudvertexbot/i, name: "Google-CloudVertexBot" },
|
|
586
|
+
{ pattern: /googleother/i, name: "GoogleOther" },
|
|
587
|
+
{ pattern: /gemini-deep-research/i, name: "Gemini-Deep-Research" },
|
|
588
|
+
// Meta
|
|
589
|
+
{ pattern: /meta-externalagent/i, name: "Meta-ExternalAgent" },
|
|
590
|
+
{ pattern: /meta-externalfetcher/i, name: "Meta-ExternalFetcher" },
|
|
591
|
+
// Microsoft Copilot
|
|
592
|
+
{ pattern: /bingpreview/i, name: "BingPreview" },
|
|
593
|
+
// xAI
|
|
594
|
+
{ pattern: /grokbot/i, name: "GrokBot" },
|
|
595
|
+
{ pattern: /xai-crawler/i, name: "xAI-Crawler" },
|
|
596
|
+
// Others
|
|
597
|
+
{ pattern: /duckassistbot/i, name: "DuckAssistBot" },
|
|
598
|
+
{ pattern: /mistralai-user/i, name: "MistralAI-User" },
|
|
599
|
+
{ pattern: /cohere-training-data-crawler/i, name: "Cohere-Training-Crawler" },
|
|
600
|
+
{ pattern: /cohere-ai/i, name: "cohere-ai" },
|
|
601
|
+
{ pattern: /bytespider/i, name: "Bytespider" },
|
|
602
|
+
{ pattern: /amazonbot/i, name: "Amazonbot" },
|
|
603
|
+
{ pattern: /applebot-extended/i, name: "Applebot-Extended" },
|
|
604
|
+
{ pattern: /ccbot/i, name: "CCBot" },
|
|
605
|
+
{ pattern: /youbot/i, name: "YouBot" },
|
|
606
|
+
{ pattern: /timpibot/i, name: "TimpiBot" },
|
|
607
|
+
{ pattern: /diffbot/i, name: "Diffbot" }
|
|
608
|
+
];
|
|
609
|
+
var STATIC_ASSET_PATTERN = /\.(css|js|mjs|json|xml|txt|ico|png|jpe?g|gif|svg|webp|avif|woff2?|ttf|otf|eot|map|pdf|zip|gz|mp4|webm|mp3)$/i;
|
|
610
|
+
function matchAiAgent(userAgent) {
|
|
611
|
+
if (!userAgent) return void 0;
|
|
612
|
+
for (const rule of AI_AGENT_RULES) {
|
|
613
|
+
if (rule.pattern.test(userAgent)) return rule.name;
|
|
614
|
+
}
|
|
615
|
+
return void 0;
|
|
616
|
+
}
|
|
617
|
+
function pathFromUrl(url) {
|
|
618
|
+
try {
|
|
619
|
+
return new URL(url, "http://localhost").pathname;
|
|
620
|
+
} catch {
|
|
621
|
+
return url.split("?")[0] ?? url;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
function isIgnored(path, options) {
|
|
625
|
+
if (options?.skipStaticAssets !== false && STATIC_ASSET_PATTERN.test(path)) return true;
|
|
626
|
+
for (const rule of options?.ignorePaths ?? []) {
|
|
627
|
+
if (rule.endsWith("*") ? path.startsWith(rule.slice(0, -1)) : path === rule) return true;
|
|
628
|
+
}
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
function captureAgentHit(metrone, hit, options) {
|
|
632
|
+
try {
|
|
633
|
+
const method = (hit.method ?? "GET").toUpperCase();
|
|
634
|
+
if (method !== "GET" && method !== "HEAD") return void 0;
|
|
635
|
+
const agent = matchAiAgent(hit.userAgent);
|
|
636
|
+
if (!agent) return void 0;
|
|
637
|
+
const path = pathFromUrl(hit.url);
|
|
638
|
+
if (isIgnored(path, options)) return void 0;
|
|
639
|
+
metrone.track("pageview", {
|
|
640
|
+
source: "web",
|
|
641
|
+
page_url: hit.url,
|
|
642
|
+
page_path: path,
|
|
643
|
+
referrer: hit.referrer ?? void 0,
|
|
644
|
+
user_agent: hit.userAgent ?? void 0,
|
|
645
|
+
properties: { agent_capture: "server" }
|
|
646
|
+
});
|
|
647
|
+
return agent;
|
|
648
|
+
} catch {
|
|
649
|
+
return void 0;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
function agentMiddleware(metrone, options) {
|
|
653
|
+
return function metroneAgentMiddleware(req, _res, next) {
|
|
654
|
+
try {
|
|
655
|
+
const header = (name) => {
|
|
656
|
+
const v = req.headers[name];
|
|
657
|
+
return Array.isArray(v) ? v[0] : v;
|
|
658
|
+
};
|
|
659
|
+
const host = header("host") ?? "localhost";
|
|
660
|
+
const proto = req.protocol ?? (header("x-forwarded-proto") ?? "https");
|
|
661
|
+
const rawPath = req.originalUrl ?? req.url ?? "/";
|
|
662
|
+
captureAgentHit(metrone, {
|
|
663
|
+
url: `${proto}://${host}${rawPath}`,
|
|
664
|
+
userAgent: header("user-agent"),
|
|
665
|
+
referrer: header("referer") ?? header("referrer"),
|
|
666
|
+
method: req.method
|
|
667
|
+
}, options);
|
|
668
|
+
} catch {
|
|
669
|
+
}
|
|
670
|
+
next();
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
function trackAgentRequest(metrone, request, options) {
|
|
674
|
+
return captureAgentHit(metrone, {
|
|
675
|
+
url: request.url,
|
|
676
|
+
userAgent: request.headers.get("user-agent"),
|
|
677
|
+
referrer: request.headers.get("referer"),
|
|
678
|
+
method: request.method
|
|
679
|
+
}, options);
|
|
680
|
+
}
|
|
506
681
|
export {
|
|
507
682
|
MetroneAuthError,
|
|
508
683
|
MetroneConfigError,
|
|
@@ -513,6 +688,10 @@ export {
|
|
|
513
688
|
MetroneServer,
|
|
514
689
|
MetroneServerError,
|
|
515
690
|
MetroneTimeoutError,
|
|
516
|
-
MetroneValidationError
|
|
691
|
+
MetroneValidationError,
|
|
692
|
+
agentMiddleware,
|
|
693
|
+
captureAgentHit,
|
|
694
|
+
matchAiAgent,
|
|
695
|
+
trackAgentRequest
|
|
517
696
|
};
|
|
518
697
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/errors.ts","../src/http.ts","../src/client.ts"],"sourcesContent":["/**\n * Structured error types for the Metrone server SDK.\n *\n * All errors carry a machine-readable `code` that agents can switch on,\n * plus a human-readable `message` for logs.\n */\n\nexport class MetroneError extends Error {\n public readonly code: string\n public readonly status?: number\n public readonly retryable: boolean\n\n constructor(code: string, message: string, status?: number, retryable = false) {\n super(message)\n this.name = 'MetroneError'\n this.code = code\n this.status = status\n this.retryable = retryable\n }\n}\n\nexport class MetroneConfigError extends MetroneError {\n constructor(message: string) {\n super('CONFIG_INVALID', message)\n this.name = 'MetroneConfigError'\n }\n}\n\nexport class MetroneAuthError extends MetroneError {\n constructor(message = 'Invalid or missing API key') {\n super('AUTH_FAILED', message, 401, false)\n this.name = 'MetroneAuthError'\n }\n}\n\nexport class MetroneRateLimitError extends MetroneError {\n public readonly retryAfterMs: number\n\n constructor(retryAfterMs: number) {\n super('RATE_LIMITED', `Rate limit exceeded. Retry after ${retryAfterMs}ms.`, 429, true)\n this.name = 'MetroneRateLimitError'\n this.retryAfterMs = retryAfterMs\n }\n}\n\nexport class MetroneQuotaError extends MetroneError {\n constructor(message = 'Monthly event quota exceeded') {\n super('QUOTA_EXCEEDED', message, 429, false)\n this.name = 'MetroneQuotaError'\n }\n}\n\nexport class MetroneNetworkError extends MetroneError {\n constructor(message = 'Network request failed') {\n super('NETWORK_ERROR', message, undefined, true)\n this.name = 'MetroneNetworkError'\n }\n}\n\nexport class MetroneTimeoutError extends MetroneError {\n constructor(timeoutMs: number) {\n super('TIMEOUT', `Request timed out after ${timeoutMs}ms`, undefined, true)\n this.name = 'MetroneTimeoutError'\n }\n}\n\nexport class MetroneValidationError extends MetroneError {\n public readonly fields: Array<{ field: string; message: string }>\n\n constructor(fields: Array<{ field: string; message: string }>) {\n const summary = fields.map(f => `${f.field}: ${f.message}`).join('; ')\n super('VALIDATION_FAILED', `Validation failed: ${summary}`, 400, false)\n this.name = 'MetroneValidationError'\n this.fields = fields\n }\n}\n\nexport class MetroneServerError extends MetroneError {\n constructor(status: number, message = 'Internal server error') {\n super('SERVER_ERROR', message, status, true)\n this.name = 'MetroneServerError'\n }\n}\n\n/**\n * 4xx response that isn't one of the more specific subclasses (auth, quota,\n * validation, rate-limit). Always permanent — re-sending the same payload\n * will produce the same error, so callers must NOT requeue events that\n * triggered it. This breaks the retry-storm pattern where a permanent server\n * rejection (e.g. `BATCH_ALL_INVALID`) used to be re-queued every flush\n * interval forever.\n */\nexport class MetroneClientError extends MetroneError {\n constructor(status: number, message = 'Request rejected by server') {\n super('CLIENT_ERROR', message, status, false)\n this.name = 'MetroneClientError'\n }\n}\n","/**\n * Runtime-agnostic HTTP layer.\n *\n * Uses the global fetch() which is available in Node 18+, Deno, Bun,\n * Cloudflare Workers, and all modern edge runtimes. A custom fetch can\n * be injected via config for environments that need it.\n */\n\nimport type { ResolvedConfig, ApiResponse } from './types.js'\nimport {\n MetroneNetworkError,\n MetroneTimeoutError,\n MetroneAuthError,\n MetroneRateLimitError,\n MetroneServerError,\n MetroneClientError,\n} from './errors.js'\n\nexport async function httpPost(\n config: ResolvedConfig,\n path: string,\n body: unknown,\n headers?: Record<string, string>,\n): Promise<ApiResponse> {\n return httpRequest(config, 'POST', path, body, headers)\n}\n\nexport async function httpGet(\n config: ResolvedConfig,\n path: string,\n params?: Record<string, string | number | undefined>,\n headers?: Record<string, string>,\n): Promise<ApiResponse> {\n let url = path\n if (params) {\n const qs = new URLSearchParams()\n for (const [k, v] of Object.entries(params)) {\n if (v !== undefined) qs.set(k, String(v))\n }\n const str = qs.toString()\n if (str) url += `?${str}`\n }\n return httpRequest(config, 'GET', url, undefined, headers)\n}\n\nasync function httpRequest(\n config: ResolvedConfig,\n method: string,\n path: string,\n body?: unknown,\n extraHeaders?: Record<string, string>,\n): Promise<ApiResponse> {\n const url = `${config.endpoint}${path}`\n const fetchFn = config.fetch\n\n const headers: Record<string, string> = {\n 'X-Api-Key': config.apiKey,\n 'User-Agent': 'metrone-server-sdk/1.0.2',\n ...extraHeaders,\n }\n\n if (body !== undefined) {\n headers['Content-Type'] = 'application/json'\n }\n\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), config.timeoutMs)\n\n try {\n const response = await fetchFn(url, {\n method,\n headers,\n body: body !== undefined ? JSON.stringify(body) : undefined,\n signal: controller.signal,\n })\n\n clearTimeout(timer)\n\n let data: unknown\n const ct = response.headers.get('content-type') ?? ''\n if (ct.includes('application/json')) {\n data = await response.json()\n } else {\n data = await response.text()\n }\n\n if (response.ok) {\n return { ok: true, status: response.status, data }\n }\n\n const errBody = data as Record<string, unknown> | undefined\n const errMsg = typeof errBody?.error === 'string'\n ? errBody.error\n : typeof errBody?.error === 'object' && errBody.error !== null\n ? (errBody.error as Record<string, unknown>).message as string ?? response.statusText\n : response.statusText\n\n if (response.status === 401) throw new MetroneAuthError(errMsg)\n if (response.status === 429) {\n const retryMs = typeof errBody?.retry_after_ms === 'number'\n ? errBody.retry_after_ms\n : 60_000\n throw new MetroneRateLimitError(retryMs)\n }\n if (response.status >= 500) throw new MetroneServerError(response.status, errMsg)\n // Any other 4xx (validation failure, quota exceeded as 4xx, etc.) is a\n // permanent rejection. Throw a typed error so `withRetry` doesn't loop\n // and `flush()` knows to drop the events instead of re-queueing them.\n if (response.status >= 400 && response.status < 500) {\n throw new MetroneClientError(response.status, errMsg)\n }\n\n return {\n ok: false,\n status: response.status,\n error: { code: 'API_ERROR', message: errMsg, details: errBody },\n }\n } catch (err) {\n clearTimeout(timer)\n\n if (err instanceof MetroneAuthError) throw err\n if (err instanceof MetroneRateLimitError) throw err\n if (err instanceof MetroneServerError) throw err\n if (err instanceof MetroneClientError) throw err\n\n if (err instanceof DOMException && err.name === 'AbortError') {\n throw new MetroneTimeoutError(config.timeoutMs)\n }\n if (err instanceof TypeError) {\n throw new MetroneNetworkError(err.message)\n }\n\n throw new MetroneNetworkError(\n err instanceof Error ? err.message : 'Unknown network error',\n )\n }\n}\n\n/**\n * Retry wrapper with exponential backoff.\n * Retries on MetroneNetworkError, MetroneTimeoutError, MetroneServerError,\n * and MetroneRateLimitError. Stops on auth or validation errors.\n */\nexport async function withRetry<T>(\n config: ResolvedConfig,\n fn: () => Promise<T>,\n): Promise<T> {\n let lastError: Error | undefined\n\n for (let attempt = 0; attempt <= config.maxRetries; attempt++) {\n try {\n return await fn()\n } catch (err) {\n lastError = err instanceof Error ? err : new Error(String(err))\n\n const isRetryable =\n err instanceof MetroneNetworkError ||\n err instanceof MetroneTimeoutError ||\n err instanceof MetroneServerError ||\n err instanceof MetroneRateLimitError\n\n if (!isRetryable || attempt >= config.maxRetries) throw err\n\n const baseDelay = err instanceof MetroneRateLimitError\n ? err.retryAfterMs\n : config.retryBaseMs * Math.pow(2, attempt)\n\n const jitter = Math.random() * baseDelay * 0.1\n const delay = baseDelay + jitter\n\n if (config.debug) {\n const errLabel = err instanceof Error ? err.message : String(err)\n console.error(\n `[metrone] Retry ${attempt + 1}/${config.maxRetries} after ${Math.round(delay)}ms: ${errLabel}`,\n )\n }\n\n await sleep(delay)\n }\n }\n\n throw lastError\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms))\n}\n","/**\n * MetroneServer — the main server-side SDK client.\n *\n * Zero dependencies. Uses global fetch (Node 18+, Deno, Bun, edge runtimes).\n * Runtime-agnostic: no DOM, no window, no navigator, no localStorage.\n *\n * Features:\n * - Event ingestion (single + batch) with automatic batching\n * - AI tracking (calls, chats, intents, sessions)\n * - Read API (stats, events, pages, sources, live)\n * - Retry with exponential backoff + jitter\n * - Idempotency key support\n * - Agent identity tracking\n * - Graceful shutdown with flush()\n */\n\nimport type {\n MetroneServerConfig,\n ResolvedConfig,\n EventPayload,\n AICallData,\n AIChatData,\n AIIntentData,\n AISessionData,\n ConversionData,\n StatsParams,\n EventsParams,\n PagesParams,\n SourcesParams,\n FlushResult,\n StatsResponse,\n EventsResponse,\n PagesResponse,\n SourcesResponse,\n LiveResponse,\n ApiResponse,\n} from './types.js'\nimport {\n MetroneConfigError,\n MetroneValidationError,\n MetroneClientError,\n MetroneAuthError,\n MetroneError,\n} from './errors.js'\nimport { httpPost, httpGet, withRetry } from './http.js'\n\nconst API_KEY_PATTERN = /^(metrone_(live|test)_|mk_(live|test)_)[a-f0-9]{32}$/\n\nexport class MetroneServer {\n private readonly config: ResolvedConfig\n private queue: Array<EventPayload & { api_key: string }> = []\n private flushTimer: ReturnType<typeof setInterval> | null = null\n private flushing = false\n private destroyed = false\n\n constructor(config: MetroneServerConfig) {\n if (!config.apiKey) {\n throw new MetroneConfigError('apiKey is required')\n }\n if (!API_KEY_PATTERN.test(config.apiKey)) {\n throw new MetroneConfigError(\n 'Invalid apiKey format. Expected metrone_live_*, metrone_test_*, mk_live_*, or mk_test_* followed by 32 hex chars.',\n )\n }\n\n const endpoint = (config.endpoint ?? 'https://api.metrone.io').replace(/\\/+$/, '')\n\n this.config = {\n apiKey: config.apiKey,\n endpoint,\n batchSize: config.batchSize ?? 10,\n flushIntervalMs: config.flushIntervalMs ?? 5000,\n maxRetries: config.maxRetries ?? 3,\n retryBaseMs: config.retryBaseMs ?? 1000,\n timeoutMs: config.timeoutMs ?? 10_000,\n maxQueueSize: config.maxQueueSize ?? 1000,\n debug: config.debug ?? false,\n fetch: config.fetch ?? globalThis.fetch.bind(globalThis),\n }\n\n if (this.config.batchSize > 0 && this.config.flushIntervalMs > 0) {\n this.flushTimer = setInterval(() => {\n this.flush().catch(err => {\n if (this.config.debug) {\n console.error('[metrone] Auto-flush error:', err)\n }\n })\n }, this.config.flushIntervalMs)\n\n if (typeof this.flushTimer === 'object' && 'unref' in this.flushTimer) {\n (this.flushTimer as NodeJS.Timeout).unref()\n }\n }\n\n if (this.config.debug) {\n console.error(`[metrone] Server SDK initialized (endpoint: ${this.config.endpoint})`)\n }\n }\n\n // ─── Event Tracking ────────────────────────────────────────────────────────\n\n /**\n * Track a custom event. Queued for batch sending unless batchSize is 0.\n */\n track(eventType: string, data?: Partial<EventPayload>): void {\n this.assertNotDestroyed()\n if (!eventType) throw new MetroneValidationError([{ field: 'event_type', message: 'required' }])\n\n const event: EventPayload & { api_key: string } = {\n api_key: this.config.apiKey,\n event_type: eventType,\n timestamp: new Date().toISOString(),\n ...data,\n }\n\n if (this.config.batchSize <= 0) {\n withRetry(this.config, () => httpPost(this.config, '/v1/events', event)).catch(err => {\n if (this.config.debug) console.error('[metrone] Send error:', err)\n })\n return\n }\n\n this.queue.push(event)\n if (this.queue.length >= this.config.batchSize) {\n this.flush().catch(err => {\n if (this.config.debug) console.error('[metrone] Flush error:', err)\n })\n }\n }\n\n /**\n * Track a page view.\n */\n pageview(url: string, title?: string, data?: Partial<EventPayload>): void {\n this.track('pageview', {\n page_url: url,\n page_title: title,\n source: 'web',\n ...data,\n })\n }\n\n /**\n * Track a conversion event.\n */\n conversion(data: ConversionData): void {\n this.track('conversion', {\n source: 'web',\n properties: {\n conversion_type: data.conversion_type,\n value: data.value,\n currency: data.currency,\n ...data.properties,\n },\n })\n }\n\n // ─── AI Tracking ───────────────────────────────────────────────────────────\n\n /**\n * Track an AI voice call.\n */\n trackAICall(data: AICallData): void {\n this.track('ai_call', {\n source: 'voice',\n channel: 'phone',\n ai_provider: data.provider ?? 'unknown',\n ai_call_id: data.call_id,\n ai_intent: data.intent,\n ai_duration_sec: data.duration,\n session_id: data.session_id,\n properties: {\n transcript_snippet: data.transcript_snippet,\n outcome: data.outcome,\n ...data.properties,\n },\n })\n }\n\n /**\n * Track an AI chat interaction.\n */\n trackAIChat(data: AIChatData): void {\n this.track('ai_chat', {\n source: 'chat',\n channel: 'website',\n ai_provider: data.provider ?? 'unknown',\n ai_session_id: data.session_id,\n ai_intent: data.intent,\n ai_duration_sec: data.duration,\n properties: {\n message_count: data.message_count,\n resolved: data.resolved,\n ...data.properties,\n },\n })\n }\n\n /**\n * Track an AI intent detection.\n */\n trackAIIntent(data: AIIntentData): void {\n this.track('ai_intent', {\n source: data.source ?? 'assistant',\n ai_intent: data.intent,\n properties: {\n confidence: data.confidence,\n ...data.properties,\n },\n })\n }\n\n /**\n * Track an AI session lifecycle event (start, end, timeout).\n */\n trackAISession(data: AISessionData): void {\n this.track(`ai_session_${data.action}`, {\n source: 'assistant',\n ai_provider: data.provider ?? 'unknown',\n ai_session_id: data.session_id,\n ai_duration_sec: data.duration,\n properties: data.properties,\n })\n }\n\n // ─── Read API ──────────────────────────────────────────────────────────────\n\n /**\n * Get aggregated analytics stats for a time period.\n */\n async getStats(params?: StatsParams): Promise<StatsResponse> {\n this.assertNotDestroyed()\n const qp = this.buildDateParams(params)\n const res = await withRetry(this.config, () =>\n httpGet(this.config, '/v1/api/stats', qp),\n )\n return res.data as StatsResponse\n }\n\n /**\n * Get individual analytics events with pagination and filters.\n */\n async getEvents(params?: EventsParams): Promise<EventsResponse> {\n this.assertNotDestroyed()\n const qp: Record<string, string | number | undefined> = {\n ...this.buildDateParams(params),\n limit: params?.limit,\n offset: params?.offset,\n event_type: params?.event_type,\n source: params?.source,\n }\n const res = await withRetry(this.config, () =>\n httpGet(this.config, '/v1/api/events', qp),\n )\n return res.data as EventsResponse\n }\n\n /**\n * Get page analytics broken down by path.\n */\n async getPages(params?: PagesParams): Promise<PagesResponse> {\n this.assertNotDestroyed()\n const qp: Record<string, string | number | undefined> = {\n ...this.buildDateParams(params),\n limit: params?.limit,\n }\n const res = await withRetry(this.config, () =>\n httpGet(this.config, '/v1/api/pages', qp),\n )\n return res.data as PagesResponse\n }\n\n /**\n * Get traffic source and referrer analytics.\n */\n async getSources(params?: SourcesParams): Promise<SourcesResponse> {\n this.assertNotDestroyed()\n const qp = this.buildDateParams(params)\n const res = await withRetry(this.config, () =>\n httpGet(this.config, '/v1/api/sources', qp),\n )\n return res.data as SourcesResponse\n }\n\n /**\n * Get real-time live stats (active visitors, today's totals).\n */\n async getLive(): Promise<LiveResponse> {\n this.assertNotDestroyed()\n const res = await withRetry(this.config, () =>\n httpGet(this.config, '/v1/api/live'),\n )\n return res.data as LiveResponse\n }\n\n // ─── Batch & Lifecycle ─────────────────────────────────────────────────────\n\n /**\n * Immediately flush all queued events. Returns the result.\n * Safe to call multiple times concurrently — only one flush runs at a time.\n */\n async flush(): Promise<FlushResult> {\n if (this.flushing || this.queue.length === 0) {\n return { sent: 0, failed: 0, errors: [] }\n }\n\n this.flushing = true\n const batch = this.queue.splice(0, 100)\n\n try {\n const res = await withRetry(this.config, () =>\n httpPost(this.config, '/v1/events/batch', batch),\n )\n\n const body = res.data as Record<string, unknown> | undefined\n const accepted = typeof body?.accepted === 'number' ? body.accepted : batch.length\n\n if (this.config.debug) {\n console.error(`[metrone] Flushed ${accepted}/${batch.length} events`)\n }\n\n return { sent: accepted, failed: batch.length - accepted, errors: [] }\n } catch (err) {\n // Permanent failures (auth, validation, other 4xx) MUST NOT be\n // re-queued — re-sending the same payload will produce the same error,\n // turning every flushIntervalMs tick into a retry storm. Drop the\n // events and surface the failure to the caller instead.\n const isPermanent =\n err instanceof MetroneAuthError ||\n err instanceof MetroneClientError ||\n (err instanceof MetroneError && err.retryable === false)\n\n if (isPermanent) {\n if (this.config.debug) {\n console.error(\n `[metrone] Dropping ${batch.length} events — server rejected them permanently:`,\n err instanceof Error ? err.message : err,\n )\n }\n return {\n sent: 0,\n failed: batch.length,\n errors: [{ index: -1, error: err instanceof Error ? err.message : String(err) }],\n }\n }\n\n // Transient failures (network, timeout, 5xx, 429 after retries) — keep\n // events for the next flush attempt.\n this.queue.unshift(...batch)\n if (this.queue.length > this.config.maxQueueSize) {\n this.queue.length = this.config.maxQueueSize\n }\n\n if (this.config.debug) {\n console.error('[metrone] Flush failed (transient), re-queued:', err)\n }\n\n return {\n sent: 0,\n failed: batch.length,\n errors: [{ index: -1, error: err instanceof Error ? err.message : String(err) }],\n }\n } finally {\n this.flushing = false\n\n if (this.queue.length >= this.config.batchSize && this.config.batchSize > 0) {\n this.flush().catch(() => {})\n }\n }\n }\n\n /**\n * Flush remaining events, stop the auto-flush timer, and mark the\n * client as destroyed. Subsequent calls will throw.\n */\n async shutdown(): Promise<FlushResult> {\n if (this.destroyed) return { sent: 0, failed: 0, errors: [] }\n\n if (this.flushTimer) {\n clearInterval(this.flushTimer)\n this.flushTimer = null\n }\n\n const result = await this.flush()\n this.destroyed = true\n\n if (this.config.debug) {\n console.error('[metrone] Server SDK shut down')\n }\n\n return result\n }\n\n /** Number of events currently in the send queue. */\n get queueSize(): number {\n return this.queue.length\n }\n\n /** Whether shutdown() has been called. */\n get isDestroyed(): boolean {\n return this.destroyed\n }\n\n // ─── Internal ──────────────────────────────────────────────────────────────\n\n private assertNotDestroyed(): void {\n if (this.destroyed) {\n throw new MetroneConfigError('Client has been shut down. Create a new instance.')\n }\n }\n\n private buildDateParams(\n params?: { days?: number; from?: string | Date; to?: string | Date },\n ): Record<string, string | number | undefined> {\n const result: Record<string, string | number | undefined> = {}\n if (params?.days) result.days = params.days\n if (params?.from) result.from = params.from instanceof Date ? params.from.toISOString() : params.from\n if (params?.to) result.to = params.to instanceof Date ? params.to.toISOString() : params.to\n return result\n }\n}\n"],"mappings":";AAOO,IAAM,eAAN,cAA2B,MAAM;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EAEhB,YAAY,MAAc,SAAiB,QAAiB,YAAY,OAAO;AAC7E,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,YAAY;AAAA,EACnB;AACF;AAEO,IAAM,qBAAN,cAAiC,aAAa;AAAA,EACnD,YAAY,SAAiB;AAC3B,UAAM,kBAAkB,OAAO;AAC/B,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,mBAAN,cAA+B,aAAa;AAAA,EACjD,YAAY,UAAU,8BAA8B;AAClD,UAAM,eAAe,SAAS,KAAK,KAAK;AACxC,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,wBAAN,cAAoC,aAAa;AAAA,EACtC;AAAA,EAEhB,YAAY,cAAsB;AAChC,UAAM,gBAAgB,oCAAoC,YAAY,OAAO,KAAK,IAAI;AACtF,SAAK,OAAO;AACZ,SAAK,eAAe;AAAA,EACtB;AACF;AAEO,IAAM,oBAAN,cAAgC,aAAa;AAAA,EAClD,YAAY,UAAU,gCAAgC;AACpD,UAAM,kBAAkB,SAAS,KAAK,KAAK;AAC3C,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,sBAAN,cAAkC,aAAa;AAAA,EACpD,YAAY,UAAU,0BAA0B;AAC9C,UAAM,iBAAiB,SAAS,QAAW,IAAI;AAC/C,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,sBAAN,cAAkC,aAAa;AAAA,EACpD,YAAY,WAAmB;AAC7B,UAAM,WAAW,2BAA2B,SAAS,MAAM,QAAW,IAAI;AAC1E,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,yBAAN,cAAqC,aAAa;AAAA,EACvC;AAAA,EAEhB,YAAY,QAAmD;AAC7D,UAAM,UAAU,OAAO,IAAI,OAAK,GAAG,EAAE,KAAK,KAAK,EAAE,OAAO,EAAE,EAAE,KAAK,IAAI;AACrE,UAAM,qBAAqB,sBAAsB,OAAO,IAAI,KAAK,KAAK;AACtE,SAAK,OAAO;AACZ,SAAK,SAAS;AAAA,EAChB;AACF;AAEO,IAAM,qBAAN,cAAiC,aAAa;AAAA,EACnD,YAAY,QAAgB,UAAU,yBAAyB;AAC7D,UAAM,gBAAgB,SAAS,QAAQ,IAAI;AAC3C,SAAK,OAAO;AAAA,EACd;AACF;AAUO,IAAM,qBAAN,cAAiC,aAAa;AAAA,EACnD,YAAY,QAAgB,UAAU,8BAA8B;AAClE,UAAM,gBAAgB,SAAS,QAAQ,KAAK;AAC5C,SAAK,OAAO;AAAA,EACd;AACF;;;AC/EA,eAAsB,SACpB,QACA,MACA,MACA,SACsB;AACtB,SAAO,YAAY,QAAQ,QAAQ,MAAM,MAAM,OAAO;AACxD;AAEA,eAAsB,QACpB,QACA,MACA,QACA,SACsB;AACtB,MAAI,MAAM;AACV,MAAI,QAAQ;AACV,UAAM,KAAK,IAAI,gBAAgB;AAC/B,eAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,MAAM,GAAG;AAC3C,UAAI,MAAM,OAAW,IAAG,IAAI,GAAG,OAAO,CAAC,CAAC;AAAA,IAC1C;AACA,UAAM,MAAM,GAAG,SAAS;AACxB,QAAI,IAAK,QAAO,IAAI,GAAG;AAAA,EACzB;AACA,SAAO,YAAY,QAAQ,OAAO,KAAK,QAAW,OAAO;AAC3D;AAEA,eAAe,YACb,QACA,QACA,MACA,MACA,cACsB;AACtB,QAAM,MAAM,GAAG,OAAO,QAAQ,GAAG,IAAI;AACrC,QAAM,UAAU,OAAO;AAEvB,QAAM,UAAkC;AAAA,IACtC,aAAa,OAAO;AAAA,IACpB,cAAc;AAAA,IACd,GAAG;AAAA,EACL;AAEA,MAAI,SAAS,QAAW;AACtB,YAAQ,cAAc,IAAI;AAAA,EAC5B;AAEA,QAAM,aAAa,IAAI,gBAAgB;AACvC,QAAM,QAAQ,WAAW,MAAM,WAAW,MAAM,GAAG,OAAO,SAAS;AAEnE,MAAI;AACF,UAAM,WAAW,MAAM,QAAQ,KAAK;AAAA,MAClC;AAAA,MACA;AAAA,MACA,MAAM,SAAS,SAAY,KAAK,UAAU,IAAI,IAAI;AAAA,MAClD,QAAQ,WAAW;AAAA,IACrB,CAAC;AAED,iBAAa,KAAK;AAElB,QAAI;AACJ,UAAM,KAAK,SAAS,QAAQ,IAAI,cAAc,KAAK;AACnD,QAAI,GAAG,SAAS,kBAAkB,GAAG;AACnC,aAAO,MAAM,SAAS,KAAK;AAAA,IAC7B,OAAO;AACL,aAAO,MAAM,SAAS,KAAK;AAAA,IAC7B;AAEA,QAAI,SAAS,IAAI;AACf,aAAO,EAAE,IAAI,MAAM,QAAQ,SAAS,QAAQ,KAAK;AAAA,IACnD;AAEA,UAAM,UAAU;AAChB,UAAM,SAAS,OAAO,SAAS,UAAU,WACrC,QAAQ,QACR,OAAO,SAAS,UAAU,YAAY,QAAQ,UAAU,OACrD,QAAQ,MAAkC,WAAqB,SAAS,aACzE,SAAS;AAEf,QAAI,SAAS,WAAW,IAAK,OAAM,IAAI,iBAAiB,MAAM;AAC9D,QAAI,SAAS,WAAW,KAAK;AAC3B,YAAM,UAAU,OAAO,SAAS,mBAAmB,WAC/C,QAAQ,iBACR;AACJ,YAAM,IAAI,sBAAsB,OAAO;AAAA,IACzC;AACA,QAAI,SAAS,UAAU,IAAK,OAAM,IAAI,mBAAmB,SAAS,QAAQ,MAAM;AAIhF,QAAI,SAAS,UAAU,OAAO,SAAS,SAAS,KAAK;AACnD,YAAM,IAAI,mBAAmB,SAAS,QAAQ,MAAM;AAAA,IACtD;AAEA,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,QAAQ,SAAS;AAAA,MACjB,OAAO,EAAE,MAAM,aAAa,SAAS,QAAQ,SAAS,QAAQ;AAAA,IAChE;AAAA,EACF,SAAS,KAAK;AACZ,iBAAa,KAAK;AAElB,QAAI,eAAe,iBAAkB,OAAM;AAC3C,QAAI,eAAe,sBAAuB,OAAM;AAChD,QAAI,eAAe,mBAAoB,OAAM;AAC7C,QAAI,eAAe,mBAAoB,OAAM;AAE7C,QAAI,eAAe,gBAAgB,IAAI,SAAS,cAAc;AAC5D,YAAM,IAAI,oBAAoB,OAAO,SAAS;AAAA,IAChD;AACA,QAAI,eAAe,WAAW;AAC5B,YAAM,IAAI,oBAAoB,IAAI,OAAO;AAAA,IAC3C;AAEA,UAAM,IAAI;AAAA,MACR,eAAe,QAAQ,IAAI,UAAU;AAAA,IACvC;AAAA,EACF;AACF;AAOA,eAAsB,UACpB,QACA,IACY;AACZ,MAAI;AAEJ,WAAS,UAAU,GAAG,WAAW,OAAO,YAAY,WAAW;AAC7D,QAAI;AACF,aAAO,MAAM,GAAG;AAAA,IAClB,SAAS,KAAK;AACZ,kBAAY,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;AAE9D,YAAM,cACJ,eAAe,uBACf,eAAe,uBACf,eAAe,sBACf,eAAe;AAEjB,UAAI,CAAC,eAAe,WAAW,OAAO,WAAY,OAAM;AAExD,YAAM,YAAY,eAAe,wBAC7B,IAAI,eACJ,OAAO,cAAc,KAAK,IAAI,GAAG,OAAO;AAE5C,YAAM,SAAS,KAAK,OAAO,IAAI,YAAY;AAC3C,YAAM,QAAQ,YAAY;AAE1B,UAAI,OAAO,OAAO;AAChB,cAAM,WAAW,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAChE,gBAAQ;AAAA,UACN,mBAAmB,UAAU,CAAC,IAAI,OAAO,UAAU,UAAU,KAAK,MAAM,KAAK,CAAC,OAAO,QAAQ;AAAA,QAC/F;AAAA,MACF;AAEA,YAAM,MAAM,KAAK;AAAA,IACnB;AAAA,EACF;AAEA,QAAM;AACR;AAEA,SAAS,MAAM,IAA2B;AACxC,SAAO,IAAI,QAAQ,aAAW,WAAW,SAAS,EAAE,CAAC;AACvD;;;AC5IA,IAAM,kBAAkB;AAEjB,IAAM,gBAAN,MAAoB;AAAA,EACR;AAAA,EACT,QAAmD,CAAC;AAAA,EACpD,aAAoD;AAAA,EACpD,WAAW;AAAA,EACX,YAAY;AAAA,EAEpB,YAAY,QAA6B;AACvC,QAAI,CAAC,OAAO,QAAQ;AAClB,YAAM,IAAI,mBAAmB,oBAAoB;AAAA,IACnD;AACA,QAAI,CAAC,gBAAgB,KAAK,OAAO,MAAM,GAAG;AACxC,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAEA,UAAM,YAAY,OAAO,YAAY,0BAA0B,QAAQ,QAAQ,EAAE;AAEjF,SAAK,SAAS;AAAA,MACZ,QAAQ,OAAO;AAAA,MACf;AAAA,MACA,WAAW,OAAO,aAAa;AAAA,MAC/B,iBAAiB,OAAO,mBAAmB;AAAA,MAC3C,YAAY,OAAO,cAAc;AAAA,MACjC,aAAa,OAAO,eAAe;AAAA,MACnC,WAAW,OAAO,aAAa;AAAA,MAC/B,cAAc,OAAO,gBAAgB;AAAA,MACrC,OAAO,OAAO,SAAS;AAAA,MACvB,OAAO,OAAO,SAAS,WAAW,MAAM,KAAK,UAAU;AAAA,IACzD;AAEA,QAAI,KAAK,OAAO,YAAY,KAAK,KAAK,OAAO,kBAAkB,GAAG;AAChE,WAAK,aAAa,YAAY,MAAM;AAClC,aAAK,MAAM,EAAE,MAAM,SAAO;AACxB,cAAI,KAAK,OAAO,OAAO;AACrB,oBAAQ,MAAM,+BAA+B,GAAG;AAAA,UAClD;AAAA,QACF,CAAC;AAAA,MACH,GAAG,KAAK,OAAO,eAAe;AAE9B,UAAI,OAAO,KAAK,eAAe,YAAY,WAAW,KAAK,YAAY;AACrE,QAAC,KAAK,WAA8B,MAAM;AAAA,MAC5C;AAAA,IACF;AAEA,QAAI,KAAK,OAAO,OAAO;AACrB,cAAQ,MAAM,+CAA+C,KAAK,OAAO,QAAQ,GAAG;AAAA,IACtF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,WAAmB,MAAoC;AAC3D,SAAK,mBAAmB;AACxB,QAAI,CAAC,UAAW,OAAM,IAAI,uBAAuB,CAAC,EAAE,OAAO,cAAc,SAAS,WAAW,CAAC,CAAC;AAE/F,UAAM,QAA4C;AAAA,MAChD,SAAS,KAAK,OAAO;AAAA,MACrB,YAAY;AAAA,MACZ,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MAClC,GAAG;AAAA,IACL;AAEA,QAAI,KAAK,OAAO,aAAa,GAAG;AAC9B,gBAAU,KAAK,QAAQ,MAAM,SAAS,KAAK,QAAQ,cAAc,KAAK,CAAC,EAAE,MAAM,SAAO;AACpF,YAAI,KAAK,OAAO,MAAO,SAAQ,MAAM,yBAAyB,GAAG;AAAA,MACnE,CAAC;AACD;AAAA,IACF;AAEA,SAAK,MAAM,KAAK,KAAK;AACrB,QAAI,KAAK,MAAM,UAAU,KAAK,OAAO,WAAW;AAC9C,WAAK,MAAM,EAAE,MAAM,SAAO;AACxB,YAAI,KAAK,OAAO,MAAO,SAAQ,MAAM,0BAA0B,GAAG;AAAA,MACpE,CAAC;AAAA,IACH;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,SAAS,KAAa,OAAgB,MAAoC;AACxE,SAAK,MAAM,YAAY;AAAA,MACrB,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,GAAG;AAAA,IACL,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,WAAW,MAA4B;AACrC,SAAK,MAAM,cAAc;AAAA,MACvB,QAAQ;AAAA,MACR,YAAY;AAAA,QACV,iBAAiB,KAAK;AAAA,QACtB,OAAO,KAAK;AAAA,QACZ,UAAU,KAAK;AAAA,QACf,GAAG,KAAK;AAAA,MACV;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,YAAY,MAAwB;AAClC,SAAK,MAAM,WAAW;AAAA,MACpB,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,aAAa,KAAK,YAAY;AAAA,MAC9B,YAAY,KAAK;AAAA,MACjB,WAAW,KAAK;AAAA,MAChB,iBAAiB,KAAK;AAAA,MACtB,YAAY,KAAK;AAAA,MACjB,YAAY;AAAA,QACV,oBAAoB,KAAK;AAAA,QACzB,SAAS,KAAK;AAAA,QACd,GAAG,KAAK;AAAA,MACV;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,YAAY,MAAwB;AAClC,SAAK,MAAM,WAAW;AAAA,MACpB,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,aAAa,KAAK,YAAY;AAAA,MAC9B,eAAe,KAAK;AAAA,MACpB,WAAW,KAAK;AAAA,MAChB,iBAAiB,KAAK;AAAA,MACtB,YAAY;AAAA,QACV,eAAe,KAAK;AAAA,QACpB,UAAU,KAAK;AAAA,QACf,GAAG,KAAK;AAAA,MACV;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,cAAc,MAA0B;AACtC,SAAK,MAAM,aAAa;AAAA,MACtB,QAAQ,KAAK,UAAU;AAAA,MACvB,WAAW,KAAK;AAAA,MAChB,YAAY;AAAA,QACV,YAAY,KAAK;AAAA,QACjB,GAAG,KAAK;AAAA,MACV;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,eAAe,MAA2B;AACxC,SAAK,MAAM,cAAc,KAAK,MAAM,IAAI;AAAA,MACtC,QAAQ;AAAA,MACR,aAAa,KAAK,YAAY;AAAA,MAC9B,eAAe,KAAK;AAAA,MACpB,iBAAiB,KAAK;AAAA,MACtB,YAAY,KAAK;AAAA,IACnB,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,SAAS,QAA8C;AAC3D,SAAK,mBAAmB;AACxB,UAAM,KAAK,KAAK,gBAAgB,MAAM;AACtC,UAAM,MAAM,MAAM;AAAA,MAAU,KAAK;AAAA,MAAQ,MACvC,QAAQ,KAAK,QAAQ,iBAAiB,EAAE;AAAA,IAC1C;AACA,WAAO,IAAI;AAAA,EACb;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,UAAU,QAAgD;AAC9D,SAAK,mBAAmB;AACxB,UAAM,KAAkD;AAAA,MACtD,GAAG,KAAK,gBAAgB,MAAM;AAAA,MAC9B,OAAO,QAAQ;AAAA,MACf,QAAQ,QAAQ;AAAA,MAChB,YAAY,QAAQ;AAAA,MACpB,QAAQ,QAAQ;AAAA,IAClB;AACA,UAAM,MAAM,MAAM;AAAA,MAAU,KAAK;AAAA,MAAQ,MACvC,QAAQ,KAAK,QAAQ,kBAAkB,EAAE;AAAA,IAC3C;AACA,WAAO,IAAI;AAAA,EACb;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,SAAS,QAA8C;AAC3D,SAAK,mBAAmB;AACxB,UAAM,KAAkD;AAAA,MACtD,GAAG,KAAK,gBAAgB,MAAM;AAAA,MAC9B,OAAO,QAAQ;AAAA,IACjB;AACA,UAAM,MAAM,MAAM;AAAA,MAAU,KAAK;AAAA,MAAQ,MACvC,QAAQ,KAAK,QAAQ,iBAAiB,EAAE;AAAA,IAC1C;AACA,WAAO,IAAI;AAAA,EACb;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,WAAW,QAAkD;AACjE,SAAK,mBAAmB;AACxB,UAAM,KAAK,KAAK,gBAAgB,MAAM;AACtC,UAAM,MAAM,MAAM;AAAA,MAAU,KAAK;AAAA,MAAQ,MACvC,QAAQ,KAAK,QAAQ,mBAAmB,EAAE;AAAA,IAC5C;AACA,WAAO,IAAI;AAAA,EACb;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,UAAiC;AACrC,SAAK,mBAAmB;AACxB,UAAM,MAAM,MAAM;AAAA,MAAU,KAAK;AAAA,MAAQ,MACvC,QAAQ,KAAK,QAAQ,cAAc;AAAA,IACrC;AACA,WAAO,IAAI;AAAA,EACb;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,QAA8B;AAClC,QAAI,KAAK,YAAY,KAAK,MAAM,WAAW,GAAG;AAC5C,aAAO,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC,EAAE;AAAA,IAC1C;AAEA,SAAK,WAAW;AAChB,UAAM,QAAQ,KAAK,MAAM,OAAO,GAAG,GAAG;AAEtC,QAAI;AACF,YAAM,MAAM,MAAM;AAAA,QAAU,KAAK;AAAA,QAAQ,MACvC,SAAS,KAAK,QAAQ,oBAAoB,KAAK;AAAA,MACjD;AAEA,YAAM,OAAO,IAAI;AACjB,YAAM,WAAW,OAAO,MAAM,aAAa,WAAW,KAAK,WAAW,MAAM;AAE5E,UAAI,KAAK,OAAO,OAAO;AACrB,gBAAQ,MAAM,qBAAqB,QAAQ,IAAI,MAAM,MAAM,SAAS;AAAA,MACtE;AAEA,aAAO,EAAE,MAAM,UAAU,QAAQ,MAAM,SAAS,UAAU,QAAQ,CAAC,EAAE;AAAA,IACvE,SAAS,KAAK;AAKZ,YAAM,cACJ,eAAe,oBACf,eAAe,sBACd,eAAe,gBAAgB,IAAI,cAAc;AAEpD,UAAI,aAAa;AACf,YAAI,KAAK,OAAO,OAAO;AACrB,kBAAQ;AAAA,YACN,sBAAsB,MAAM,MAAM;AAAA,YAClC,eAAe,QAAQ,IAAI,UAAU;AAAA,UACvC;AAAA,QACF;AACA,eAAO;AAAA,UACL,MAAM;AAAA,UACN,QAAQ,MAAM;AAAA,UACd,QAAQ,CAAC,EAAE,OAAO,IAAI,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE,CAAC;AAAA,QACjF;AAAA,MACF;AAIA,WAAK,MAAM,QAAQ,GAAG,KAAK;AAC3B,UAAI,KAAK,MAAM,SAAS,KAAK,OAAO,cAAc;AAChD,aAAK,MAAM,SAAS,KAAK,OAAO;AAAA,MAClC;AAEA,UAAI,KAAK,OAAO,OAAO;AACrB,gBAAQ,MAAM,kDAAkD,GAAG;AAAA,MACrE;AAEA,aAAO;AAAA,QACL,MAAM;AAAA,QACN,QAAQ,MAAM;AAAA,QACd,QAAQ,CAAC,EAAE,OAAO,IAAI,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE,CAAC;AAAA,MACjF;AAAA,IACF,UAAE;AACA,WAAK,WAAW;AAEhB,UAAI,KAAK,MAAM,UAAU,KAAK,OAAO,aAAa,KAAK,OAAO,YAAY,GAAG;AAC3E,aAAK,MAAM,EAAE,MAAM,MAAM;AAAA,QAAC,CAAC;AAAA,MAC7B;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,WAAiC;AACrC,QAAI,KAAK,UAAW,QAAO,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC,EAAE;AAE5D,QAAI,KAAK,YAAY;AACnB,oBAAc,KAAK,UAAU;AAC7B,WAAK,aAAa;AAAA,IACpB;AAEA,UAAM,SAAS,MAAM,KAAK,MAAM;AAChC,SAAK,YAAY;AAEjB,QAAI,KAAK,OAAO,OAAO;AACrB,cAAQ,MAAM,gCAAgC;AAAA,IAChD;AAEA,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,IAAI,YAAoB;AACtB,WAAO,KAAK,MAAM;AAAA,EACpB;AAAA;AAAA,EAGA,IAAI,cAAuB;AACzB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAIQ,qBAA2B;AACjC,QAAI,KAAK,WAAW;AAClB,YAAM,IAAI,mBAAmB,mDAAmD;AAAA,IAClF;AAAA,EACF;AAAA,EAEQ,gBACN,QAC6C;AAC7C,UAAM,SAAsD,CAAC;AAC7D,QAAI,QAAQ,KAAM,QAAO,OAAO,OAAO;AACvC,QAAI,QAAQ,KAAM,QAAO,OAAO,OAAO,gBAAgB,OAAO,OAAO,KAAK,YAAY,IAAI,OAAO;AACjG,QAAI,QAAQ,GAAI,QAAO,KAAK,OAAO,cAAc,OAAO,OAAO,GAAG,YAAY,IAAI,OAAO;AACzF,WAAO;AAAA,EACT;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/http.ts","../src/client.ts","../src/agent-tracker.ts"],"sourcesContent":["/**\n * Structured error types for the Metrone server SDK.\n *\n * All errors carry a machine-readable `code` that agents can switch on,\n * plus a human-readable `message` for logs.\n */\n\nexport class MetroneError extends Error {\n public readonly code: string\n public readonly status?: number\n public readonly retryable: boolean\n\n constructor(code: string, message: string, status?: number, retryable = false) {\n super(message)\n this.name = 'MetroneError'\n this.code = code\n this.status = status\n this.retryable = retryable\n }\n}\n\nexport class MetroneConfigError extends MetroneError {\n constructor(message: string) {\n super('CONFIG_INVALID', message)\n this.name = 'MetroneConfigError'\n }\n}\n\nexport class MetroneAuthError extends MetroneError {\n constructor(message = 'Invalid or missing API key') {\n super('AUTH_FAILED', message, 401, false)\n this.name = 'MetroneAuthError'\n }\n}\n\nexport class MetroneRateLimitError extends MetroneError {\n public readonly retryAfterMs: number\n\n constructor(retryAfterMs: number) {\n super('RATE_LIMITED', `Rate limit exceeded. Retry after ${retryAfterMs}ms.`, 429, true)\n this.name = 'MetroneRateLimitError'\n this.retryAfterMs = retryAfterMs\n }\n}\n\nexport class MetroneQuotaError extends MetroneError {\n constructor(message = 'Monthly event quota exceeded') {\n super('QUOTA_EXCEEDED', message, 429, false)\n this.name = 'MetroneQuotaError'\n }\n}\n\nexport class MetroneNetworkError extends MetroneError {\n constructor(message = 'Network request failed') {\n super('NETWORK_ERROR', message, undefined, true)\n this.name = 'MetroneNetworkError'\n }\n}\n\nexport class MetroneTimeoutError extends MetroneError {\n constructor(timeoutMs: number) {\n super('TIMEOUT', `Request timed out after ${timeoutMs}ms`, undefined, true)\n this.name = 'MetroneTimeoutError'\n }\n}\n\nexport class MetroneValidationError extends MetroneError {\n public readonly fields: Array<{ field: string; message: string }>\n\n constructor(fields: Array<{ field: string; message: string }>) {\n const summary = fields.map(f => `${f.field}: ${f.message}`).join('; ')\n super('VALIDATION_FAILED', `Validation failed: ${summary}`, 400, false)\n this.name = 'MetroneValidationError'\n this.fields = fields\n }\n}\n\nexport class MetroneServerError extends MetroneError {\n constructor(status: number, message = 'Internal server error') {\n super('SERVER_ERROR', message, status, true)\n this.name = 'MetroneServerError'\n }\n}\n\n/**\n * 4xx response that isn't one of the more specific subclasses (auth, quota,\n * validation, rate-limit). Always permanent — re-sending the same payload\n * will produce the same error, so callers must NOT requeue events that\n * triggered it. This breaks the retry-storm pattern where a permanent server\n * rejection (e.g. `BATCH_ALL_INVALID`) used to be re-queued every flush\n * interval forever.\n */\nexport class MetroneClientError extends MetroneError {\n constructor(status: number, message = 'Request rejected by server') {\n super('CLIENT_ERROR', message, status, false)\n this.name = 'MetroneClientError'\n }\n}\n","/**\n * Runtime-agnostic HTTP layer.\n *\n * Uses the global fetch() which is available in Node 18+, Deno, Bun,\n * Cloudflare Workers, and all modern edge runtimes. A custom fetch can\n * be injected via config for environments that need it.\n */\n\nimport type { ResolvedConfig, ApiResponse } from './types.js'\nimport {\n MetroneNetworkError,\n MetroneTimeoutError,\n MetroneAuthError,\n MetroneRateLimitError,\n MetroneServerError,\n MetroneClientError,\n} from './errors.js'\n\nexport async function httpPost(\n config: ResolvedConfig,\n path: string,\n body: unknown,\n headers?: Record<string, string>,\n): Promise<ApiResponse> {\n return httpRequest(config, 'POST', path, body, headers)\n}\n\nexport async function httpGet(\n config: ResolvedConfig,\n path: string,\n params?: Record<string, string | number | undefined>,\n headers?: Record<string, string>,\n): Promise<ApiResponse> {\n let url = path\n if (params) {\n const qs = new URLSearchParams()\n for (const [k, v] of Object.entries(params)) {\n if (v !== undefined) qs.set(k, String(v))\n }\n const str = qs.toString()\n if (str) url += `?${str}`\n }\n return httpRequest(config, 'GET', url, undefined, headers)\n}\n\nasync function httpRequest(\n config: ResolvedConfig,\n method: string,\n path: string,\n body?: unknown,\n extraHeaders?: Record<string, string>,\n): Promise<ApiResponse> {\n const url = `${config.endpoint}${path}`\n const fetchFn = config.fetch\n\n const headers: Record<string, string> = {\n 'X-Api-Key': config.apiKey,\n 'User-Agent': 'metrone-server-sdk/1.0.2',\n ...extraHeaders,\n }\n\n if (body !== undefined) {\n headers['Content-Type'] = 'application/json'\n }\n\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), config.timeoutMs)\n\n try {\n const response = await fetchFn(url, {\n method,\n headers,\n body: body !== undefined ? JSON.stringify(body) : undefined,\n signal: controller.signal,\n })\n\n clearTimeout(timer)\n\n let data: unknown\n const ct = response.headers.get('content-type') ?? ''\n if (ct.includes('application/json')) {\n data = await response.json()\n } else {\n data = await response.text()\n }\n\n if (response.ok) {\n return { ok: true, status: response.status, data }\n }\n\n const errBody = data as Record<string, unknown> | undefined\n const errMsg = typeof errBody?.error === 'string'\n ? errBody.error\n : typeof errBody?.error === 'object' && errBody.error !== null\n ? (errBody.error as Record<string, unknown>).message as string ?? response.statusText\n : response.statusText\n\n if (response.status === 401) throw new MetroneAuthError(errMsg)\n if (response.status === 429) {\n const retryMs = typeof errBody?.retry_after_ms === 'number'\n ? errBody.retry_after_ms\n : 60_000\n throw new MetroneRateLimitError(retryMs)\n }\n if (response.status >= 500) throw new MetroneServerError(response.status, errMsg)\n // Any other 4xx (validation failure, quota exceeded as 4xx, etc.) is a\n // permanent rejection. Throw a typed error so `withRetry` doesn't loop\n // and `flush()` knows to drop the events instead of re-queueing them.\n if (response.status >= 400 && response.status < 500) {\n throw new MetroneClientError(response.status, errMsg)\n }\n\n return {\n ok: false,\n status: response.status,\n error: { code: 'API_ERROR', message: errMsg, details: errBody },\n }\n } catch (err) {\n clearTimeout(timer)\n\n if (err instanceof MetroneAuthError) throw err\n if (err instanceof MetroneRateLimitError) throw err\n if (err instanceof MetroneServerError) throw err\n if (err instanceof MetroneClientError) throw err\n\n if (err instanceof DOMException && err.name === 'AbortError') {\n throw new MetroneTimeoutError(config.timeoutMs)\n }\n if (err instanceof TypeError) {\n throw new MetroneNetworkError(err.message)\n }\n\n throw new MetroneNetworkError(\n err instanceof Error ? err.message : 'Unknown network error',\n )\n }\n}\n\n/**\n * Retry wrapper with exponential backoff.\n * Retries on MetroneNetworkError, MetroneTimeoutError, MetroneServerError,\n * and MetroneRateLimitError. Stops on auth or validation errors.\n */\nexport async function withRetry<T>(\n config: ResolvedConfig,\n fn: () => Promise<T>,\n): Promise<T> {\n let lastError: Error | undefined\n\n for (let attempt = 0; attempt <= config.maxRetries; attempt++) {\n try {\n return await fn()\n } catch (err) {\n lastError = err instanceof Error ? err : new Error(String(err))\n\n const isRetryable =\n err instanceof MetroneNetworkError ||\n err instanceof MetroneTimeoutError ||\n err instanceof MetroneServerError ||\n err instanceof MetroneRateLimitError\n\n if (!isRetryable || attempt >= config.maxRetries) throw err\n\n const baseDelay = err instanceof MetroneRateLimitError\n ? err.retryAfterMs\n : config.retryBaseMs * Math.pow(2, attempt)\n\n const jitter = Math.random() * baseDelay * 0.1\n const delay = baseDelay + jitter\n\n if (config.debug) {\n const errLabel = err instanceof Error ? err.message : String(err)\n console.error(\n `[metrone] Retry ${attempt + 1}/${config.maxRetries} after ${Math.round(delay)}ms: ${errLabel}`,\n )\n }\n\n await sleep(delay)\n }\n }\n\n throw lastError\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms))\n}\n","/**\n * MetroneServer — the main server-side SDK client.\n *\n * Zero dependencies. Uses global fetch (Node 18+, Deno, Bun, edge runtimes).\n * Runtime-agnostic: no DOM, no window, no navigator, no localStorage.\n *\n * Features:\n * - Event ingestion (single + batch) with automatic batching\n * - AI tracking (calls, chats, intents, sessions)\n * - Read API (stats, events, pages, sources, live)\n * - Retry with exponential backoff + jitter\n * - Idempotency key support\n * - Agent identity tracking\n * - Graceful shutdown with flush()\n */\n\nimport type {\n MetroneServerConfig,\n ResolvedConfig,\n EventPayload,\n AICallData,\n AIChatData,\n AIIntentData,\n AISessionData,\n ConversionData,\n StatsParams,\n EventsParams,\n PagesParams,\n SourcesParams,\n FlushResult,\n StatsResponse,\n EventsResponse,\n PagesResponse,\n SourcesResponse,\n LiveResponse,\n ApiResponse,\n} from './types.js'\nimport {\n MetroneConfigError,\n MetroneValidationError,\n MetroneClientError,\n MetroneAuthError,\n MetroneError,\n} from './errors.js'\nimport { httpPost, httpGet, withRetry } from './http.js'\n\nconst API_KEY_PATTERN = /^(metrone_(live|test)_|mk_(live|test)_)[a-f0-9]{32}$/\n\nexport class MetroneServer {\n private readonly config: ResolvedConfig\n private queue: Array<EventPayload & { api_key: string }> = []\n private flushTimer: ReturnType<typeof setInterval> | null = null\n private flushing = false\n private destroyed = false\n\n constructor(config: MetroneServerConfig) {\n if (!config.apiKey) {\n throw new MetroneConfigError('apiKey is required')\n }\n if (!API_KEY_PATTERN.test(config.apiKey)) {\n throw new MetroneConfigError(\n 'Invalid apiKey format. Expected metrone_live_*, metrone_test_*, mk_live_*, or mk_test_* followed by 32 hex chars.',\n )\n }\n\n const endpoint = (config.endpoint ?? 'https://api.metrone.io').replace(/\\/+$/, '')\n\n this.config = {\n apiKey: config.apiKey,\n endpoint,\n batchSize: config.batchSize ?? 10,\n flushIntervalMs: config.flushIntervalMs ?? 5000,\n maxRetries: config.maxRetries ?? 3,\n retryBaseMs: config.retryBaseMs ?? 1000,\n timeoutMs: config.timeoutMs ?? 10_000,\n maxQueueSize: config.maxQueueSize ?? 1000,\n debug: config.debug ?? false,\n fetch: config.fetch ?? globalThis.fetch.bind(globalThis),\n }\n\n if (this.config.batchSize > 0 && this.config.flushIntervalMs > 0) {\n this.flushTimer = setInterval(() => {\n this.flush().catch(err => {\n if (this.config.debug) {\n console.error('[metrone] Auto-flush error:', err)\n }\n })\n }, this.config.flushIntervalMs)\n\n if (typeof this.flushTimer === 'object' && 'unref' in this.flushTimer) {\n (this.flushTimer as NodeJS.Timeout).unref()\n }\n }\n\n if (this.config.debug) {\n console.error(`[metrone] Server SDK initialized (endpoint: ${this.config.endpoint})`)\n }\n }\n\n // ─── Event Tracking ────────────────────────────────────────────────────────\n\n /**\n * Track a custom event. Queued for batch sending unless batchSize is 0.\n */\n track(eventType: string, data?: Partial<EventPayload>): void {\n this.assertNotDestroyed()\n if (!eventType) throw new MetroneValidationError([{ field: 'event_type', message: 'required' }])\n\n const event: EventPayload & { api_key: string } = {\n api_key: this.config.apiKey,\n event_type: eventType,\n timestamp: new Date().toISOString(),\n ...data,\n }\n\n if (this.config.batchSize <= 0) {\n withRetry(this.config, () => httpPost(this.config, '/v1/events', event)).catch(err => {\n if (this.config.debug) console.error('[metrone] Send error:', err)\n })\n return\n }\n\n this.queue.push(event)\n if (this.queue.length >= this.config.batchSize) {\n this.flush().catch(err => {\n if (this.config.debug) console.error('[metrone] Flush error:', err)\n })\n }\n }\n\n /**\n * Track a page view.\n */\n pageview(url: string, title?: string, data?: Partial<EventPayload>): void {\n this.track('pageview', {\n page_url: url,\n page_title: title,\n source: 'web',\n ...data,\n })\n }\n\n /**\n * Track a conversion event.\n */\n conversion(data: ConversionData): void {\n this.track('conversion', {\n source: 'web',\n properties: {\n conversion_type: data.conversion_type,\n value: data.value,\n currency: data.currency,\n ...data.properties,\n },\n })\n }\n\n // ─── AI Tracking ───────────────────────────────────────────────────────────\n\n /**\n * Track an AI voice call.\n */\n trackAICall(data: AICallData): void {\n this.track('ai_call', {\n source: 'voice',\n channel: 'phone',\n ai_provider: data.provider ?? 'unknown',\n ai_call_id: data.call_id,\n ai_intent: data.intent,\n ai_duration_sec: data.duration,\n session_id: data.session_id,\n properties: {\n transcript_snippet: data.transcript_snippet,\n outcome: data.outcome,\n ...data.properties,\n },\n })\n }\n\n /**\n * Track an AI chat interaction.\n */\n trackAIChat(data: AIChatData): void {\n this.track('ai_chat', {\n source: 'chat',\n channel: 'website',\n ai_provider: data.provider ?? 'unknown',\n ai_session_id: data.session_id,\n ai_intent: data.intent,\n ai_duration_sec: data.duration,\n properties: {\n message_count: data.message_count,\n resolved: data.resolved,\n ...data.properties,\n },\n })\n }\n\n /**\n * Track an AI intent detection.\n */\n trackAIIntent(data: AIIntentData): void {\n this.track('ai_intent', {\n source: data.source ?? 'assistant',\n ai_intent: data.intent,\n properties: {\n confidence: data.confidence,\n ...data.properties,\n },\n })\n }\n\n /**\n * Track an AI session lifecycle event (start, end, timeout).\n */\n trackAISession(data: AISessionData): void {\n this.track(`ai_session_${data.action}`, {\n source: 'assistant',\n ai_provider: data.provider ?? 'unknown',\n ai_session_id: data.session_id,\n ai_duration_sec: data.duration,\n properties: data.properties,\n })\n }\n\n // ─── v1.1 source helpers (SPEC-01) ─────────────────────────────────────────\n // Thin ergonomic wrappers over track(). Required fields are typed; any\n // extra keys flow into the JSONB `properties` payload (storage acceptance\n // is permissive — see @metrone/schemas per-source recommended shapes).\n\n /**\n * Track a campaign lifecycle event (email / SMS sends, opens, clicks…).\n * Recommended event_type values: email_sent | email_delivered |\n * email_opened | email_clicked | email_bounced | email_unsubscribed |\n * sms_sent | sms_delivered | sms_replied | sms_opt_out\n */\n trackCampaignEvent(data: { event_type: string; campaign_id: string; recipient_id_hash: string } & Record<string, unknown>): void {\n const { event_type, ...properties } = data\n this.track(event_type, { source: 'campaign', properties })\n }\n\n /**\n * Track a social media event. Recommended event_type values:\n * post_published | impression | engagement | click | share\n */\n trackSocialEvent(data: { event_type: string; platform: string; post_id: string } & Record<string, unknown>): void {\n const { event_type, ...properties } = data\n this.track(event_type, { source: 'social', properties })\n }\n\n /**\n * Track a review event. Recommended event_type values: received | responded\n */\n trackReviewEvent(data: { event_type: string; platform: string; review_id: string; rating: number } & Record<string, unknown>): void {\n const { event_type, ...properties } = data\n this.track(event_type, { source: 'review', properties })\n }\n\n /**\n * Track a paid-ad metrics event. Recommended event_type values:\n * impression | click | spend_daily | conversion\n */\n trackAdEvent(data: { event_type: string; platform: string; campaign_id: string } & Record<string, unknown>): void {\n const { event_type, ...properties } = data\n this.track(event_type, { source: 'ad', properties })\n }\n\n /**\n * Track an SMS conversation event. Recommended event_type values:\n * received | replied. agent_id is promoted to the top-level column so\n * existing per-agent dashboard breakdowns keep working.\n */\n trackSmsEvent(data: { event_type: string; agent_id: string } & Record<string, unknown>): void {\n const { event_type, agent_id, ...properties } = data\n this.track(event_type, { source: 'sms', agent_id: agent_id as string, properties })\n }\n\n /**\n * Track an autonomous agent action (audit-log style). event_type is the\n * action itself (free-form vocabulary). agent_type / agent_id are written\n * BOTH to the top-level columns (for existing dashboard breakdowns) AND\n * into properties (for the JSONB-first get_agent_activity_feed RPC) —\n * decision B4 of the v1.1 build.\n */\n trackAgentAction(data: { agent_type: string; action_taken: string; agent_id?: string; target?: string; outcome?: string } & Record<string, unknown>): void {\n const { agent_type, action_taken, agent_id, ...rest } = data\n this.track(action_taken, {\n source: 'agent_action',\n agent_id: agent_id as string | undefined,\n agent_type: agent_type as string,\n properties: { agent_type, action_taken, ...rest },\n })\n }\n\n // ─── Read API ──────────────────────────────────────────────────────────────\n\n /**\n * Get aggregated analytics stats for a time period.\n */\n async getStats(params?: StatsParams): Promise<StatsResponse> {\n this.assertNotDestroyed()\n const qp = this.buildDateParams(params)\n const res = await withRetry(this.config, () =>\n httpGet(this.config, '/v1/api/stats', qp),\n )\n return res.data as StatsResponse\n }\n\n /**\n * Get individual analytics events with pagination and filters.\n */\n async getEvents(params?: EventsParams): Promise<EventsResponse> {\n this.assertNotDestroyed()\n const qp: Record<string, string | number | undefined> = {\n ...this.buildDateParams(params),\n limit: params?.limit,\n offset: params?.offset,\n event_type: params?.event_type,\n source: params?.source,\n }\n const res = await withRetry(this.config, () =>\n httpGet(this.config, '/v1/api/events', qp),\n )\n return res.data as EventsResponse\n }\n\n /**\n * Get page analytics broken down by path.\n */\n async getPages(params?: PagesParams): Promise<PagesResponse> {\n this.assertNotDestroyed()\n const qp: Record<string, string | number | undefined> = {\n ...this.buildDateParams(params),\n limit: params?.limit,\n }\n const res = await withRetry(this.config, () =>\n httpGet(this.config, '/v1/api/pages', qp),\n )\n return res.data as PagesResponse\n }\n\n /**\n * Get traffic source and referrer analytics.\n */\n async getSources(params?: SourcesParams): Promise<SourcesResponse> {\n this.assertNotDestroyed()\n const qp = this.buildDateParams(params)\n const res = await withRetry(this.config, () =>\n httpGet(this.config, '/v1/api/sources', qp),\n )\n return res.data as SourcesResponse\n }\n\n /**\n * Get real-time live stats (active visitors, today's totals).\n */\n async getLive(): Promise<LiveResponse> {\n this.assertNotDestroyed()\n const res = await withRetry(this.config, () =>\n httpGet(this.config, '/v1/api/live'),\n )\n return res.data as LiveResponse\n }\n\n // ─── Batch & Lifecycle ─────────────────────────────────────────────────────\n\n /**\n * Immediately flush all queued events. Returns the result.\n * Safe to call multiple times concurrently — only one flush runs at a time.\n */\n async flush(): Promise<FlushResult> {\n if (this.flushing || this.queue.length === 0) {\n return { sent: 0, failed: 0, errors: [] }\n }\n\n this.flushing = true\n const batch = this.queue.splice(0, 100)\n\n try {\n const res = await withRetry(this.config, () =>\n httpPost(this.config, '/v1/events/batch', batch),\n )\n\n const body = res.data as Record<string, unknown> | undefined\n const accepted = typeof body?.accepted === 'number' ? body.accepted : batch.length\n\n if (this.config.debug) {\n console.error(`[metrone] Flushed ${accepted}/${batch.length} events`)\n }\n\n return { sent: accepted, failed: batch.length - accepted, errors: [] }\n } catch (err) {\n // Permanent failures (auth, validation, other 4xx) MUST NOT be\n // re-queued — re-sending the same payload will produce the same error,\n // turning every flushIntervalMs tick into a retry storm. Drop the\n // events and surface the failure to the caller instead.\n const isPermanent =\n err instanceof MetroneAuthError ||\n err instanceof MetroneClientError ||\n (err instanceof MetroneError && err.retryable === false)\n\n if (isPermanent) {\n if (this.config.debug) {\n console.error(\n `[metrone] Dropping ${batch.length} events — server rejected them permanently:`,\n err instanceof Error ? err.message : err,\n )\n }\n return {\n sent: 0,\n failed: batch.length,\n errors: [{ index: -1, error: err instanceof Error ? err.message : String(err) }],\n }\n }\n\n // Transient failures (network, timeout, 5xx, 429 after retries) — keep\n // events for the next flush attempt.\n this.queue.unshift(...batch)\n if (this.queue.length > this.config.maxQueueSize) {\n this.queue.length = this.config.maxQueueSize\n }\n\n if (this.config.debug) {\n console.error('[metrone] Flush failed (transient), re-queued:', err)\n }\n\n return {\n sent: 0,\n failed: batch.length,\n errors: [{ index: -1, error: err instanceof Error ? err.message : String(err) }],\n }\n } finally {\n this.flushing = false\n\n if (this.queue.length >= this.config.batchSize && this.config.batchSize > 0) {\n this.flush().catch(() => {})\n }\n }\n }\n\n /**\n * Flush remaining events, stop the auto-flush timer, and mark the\n * client as destroyed. Subsequent calls will throw.\n */\n async shutdown(): Promise<FlushResult> {\n if (this.destroyed) return { sent: 0, failed: 0, errors: [] }\n\n if (this.flushTimer) {\n clearInterval(this.flushTimer)\n this.flushTimer = null\n }\n\n const result = await this.flush()\n this.destroyed = true\n\n if (this.config.debug) {\n console.error('[metrone] Server SDK shut down')\n }\n\n return result\n }\n\n /** Number of events currently in the send queue. */\n get queueSize(): number {\n return this.queue.length\n }\n\n /** Whether shutdown() has been called. */\n get isDestroyed(): boolean {\n return this.destroyed\n }\n\n // ─── Internal ──────────────────────────────────────────────────────────────\n\n private assertNotDestroyed(): void {\n if (this.destroyed) {\n throw new MetroneConfigError('Client has been shut down. Create a new instance.')\n }\n }\n\n private buildDateParams(\n params?: { days?: number; from?: string | Date; to?: string | Date },\n ): Record<string, string | number | undefined> {\n const result: Record<string, string | number | undefined> = {}\n if (params?.days) result.days = params.days\n if (params?.from) result.from = params.from instanceof Date ? params.from.toISOString() : params.from\n if (params?.to) result.to = params.to instanceof Date ? params.to.toISOString() : params.to\n return result\n }\n}\n","/**\n * agent-tracker.ts — server-side capture of AI agent traffic.\n *\n * AI agents (GPTBot, ChatGPT-User, PerplexityBot, ClaudeBot, …) fetch pages\n * without executing JavaScript, so the browser tracker never sees them. This\n * module runs where the request is actually served — an Express/Connect\n * middleware, a fetch-style handler wrapper, or a manual call — and forwards\n * matched requests to Metrone with the original User-Agent in the\n * `user_agent` payload field. The ingestion worker performs the\n * authoritative classification (visitor_type = 'ai_agent') and the raw UA\n * is never stored.\n *\n * Only requests whose UA matches the AI-agent list are forwarded — normal\n * human traffic is never sent from here (the browser tracker handles it),\n * so this adds zero overhead and zero quota usage for regular visitors.\n *\n * Keep the UA list in sync with AI_AGENT_RULES in\n * packages/worker/src/lib/device-parser.ts (the authoritative matcher).\n */\n\nimport type { MetroneServer } from './client.js'\n\nconst AI_AGENT_RULES: Array<{ pattern: RegExp; name: string }> = [\n // OpenAI\n { pattern: /chatgpt-user/i, name: 'ChatGPT-User' },\n { pattern: /oai-searchbot/i, name: 'OAI-SearchBot' },\n { pattern: /gptbot/i, name: 'GPTBot' },\n // Anthropic\n { pattern: /claude-user/i, name: 'Claude-User' },\n { pattern: /claude-searchbot/i, name: 'Claude-SearchBot' },\n { pattern: /claudebot/i, name: 'ClaudeBot' },\n { pattern: /claude-web/i, name: 'Claude-Web' },\n { pattern: /anthropic-ai/i, name: 'anthropic-ai' },\n // Perplexity\n { pattern: /perplexity-user/i, name: 'Perplexity-User' },\n { pattern: /perplexitybot/i, name: 'PerplexityBot' },\n // Google AI\n { pattern: /google-cloudvertexbot/i, name: 'Google-CloudVertexBot' },\n { pattern: /googleother/i, name: 'GoogleOther' },\n { pattern: /gemini-deep-research/i, name: 'Gemini-Deep-Research' },\n // Meta\n { pattern: /meta-externalagent/i, name: 'Meta-ExternalAgent' },\n { pattern: /meta-externalfetcher/i, name: 'Meta-ExternalFetcher' },\n // Microsoft Copilot\n { pattern: /bingpreview/i, name: 'BingPreview' },\n // xAI\n { pattern: /grokbot/i, name: 'GrokBot' },\n { pattern: /xai-crawler/i, name: 'xAI-Crawler' },\n // Others\n { pattern: /duckassistbot/i, name: 'DuckAssistBot' },\n { pattern: /mistralai-user/i, name: 'MistralAI-User' },\n { pattern: /cohere-training-data-crawler/i, name: 'Cohere-Training-Crawler' },\n { pattern: /cohere-ai/i, name: 'cohere-ai' },\n { pattern: /bytespider/i, name: 'Bytespider' },\n { pattern: /amazonbot/i, name: 'Amazonbot' },\n { pattern: /applebot-extended/i, name: 'Applebot-Extended' },\n { pattern: /ccbot/i, name: 'CCBot' },\n { pattern: /youbot/i, name: 'YouBot' },\n { pattern: /timpibot/i, name: 'TimpiBot' },\n { pattern: /diffbot/i, name: 'Diffbot' },\n]\n\n/** Static-asset extensions that are noise even when fetched by an agent. */\nconst STATIC_ASSET_PATTERN = /\\.(css|js|mjs|json|xml|txt|ico|png|jpe?g|gif|svg|webp|avif|woff2?|ttf|otf|eot|map|pdf|zip|gz|mp4|webm|mp3)$/i\n\n/**\n * Returns the canonical AI agent name for a User-Agent, or undefined when\n * the UA does not belong to a known AI agent.\n */\nexport function matchAiAgent(userAgent: string | null | undefined): string | undefined {\n if (!userAgent) return undefined\n for (const rule of AI_AGENT_RULES) {\n if (rule.pattern.test(userAgent)) return rule.name\n }\n return undefined\n}\n\nexport interface AgentHit {\n /** Full request URL (or at minimum a path). */\n url: string\n /** The visitor's User-Agent header. */\n userAgent: string | null | undefined\n /** Optional Referer header. */\n referrer?: string | null\n /** HTTP method — non-GET/HEAD requests are skipped. Defaults to 'GET'. */\n method?: string\n}\n\nexport interface AgentTrackerOptions {\n /**\n * Skip requests for static assets (css/js/images/fonts/…). Default: true.\n */\n skipStaticAssets?: boolean\n /**\n * Extra paths to ignore (exact match or prefix ending with '*'),\n * e.g. ['/health', '/api/*'].\n */\n ignorePaths?: string[]\n}\n\nfunction pathFromUrl(url: string): string {\n try {\n return new URL(url, 'http://localhost').pathname\n } catch {\n return url.split('?')[0] ?? url\n }\n}\n\nfunction isIgnored(path: string, options?: AgentTrackerOptions): boolean {\n if (options?.skipStaticAssets !== false && STATIC_ASSET_PATTERN.test(path)) return true\n for (const rule of options?.ignorePaths ?? []) {\n if (rule.endsWith('*') ? path.startsWith(rule.slice(0, -1)) : path === rule) return true\n }\n return false\n}\n\n/**\n * Core capture: if the request comes from a known AI agent, forward it to\n * Metrone as a pageview with the original UA. Fire-and-forget — never\n * throws, never blocks the response.\n *\n * Returns the matched agent name, or undefined when the request was not an\n * AI agent (or was filtered out).\n *\n * Serverless note: the SDK batches by default; in short-lived environments\n * construct the client with `batchSize: 0` so hits are sent immediately.\n */\nexport function captureAgentHit(\n metrone: MetroneServer,\n hit: AgentHit,\n options?: AgentTrackerOptions,\n): string | undefined {\n try {\n const method = (hit.method ?? 'GET').toUpperCase()\n if (method !== 'GET' && method !== 'HEAD') return undefined\n\n const agent = matchAiAgent(hit.userAgent)\n if (!agent) return undefined\n\n const path = pathFromUrl(hit.url)\n if (isIgnored(path, options)) return undefined\n\n metrone.track('pageview', {\n source: 'web',\n page_url: hit.url,\n page_path: path,\n referrer: hit.referrer ?? undefined,\n user_agent: hit.userAgent ?? undefined,\n properties: { agent_capture: 'server' },\n })\n return agent\n } catch {\n // Capture must never break the customer's request handling.\n return undefined\n }\n}\n\n/**\n * Express / Connect middleware. Mount early, before routing:\n *\n * ```ts\n * import { MetroneServer, agentMiddleware } from '@metrone-io/server'\n *\n * const metrone = new MetroneServer({ apiKey: process.env.METRONE_API_KEY! })\n * app.use(agentMiddleware(metrone))\n * ```\n */\nexport function agentMiddleware(metrone: MetroneServer, options?: AgentTrackerOptions) {\n return function metroneAgentMiddleware(\n req: { method?: string; originalUrl?: string; url?: string; headers: Record<string, string | string[] | undefined>; protocol?: string; get?: (name: string) => string | undefined },\n _res: unknown,\n next: (err?: unknown) => void,\n ): void {\n try {\n const header = (name: string): string | undefined => {\n const v = req.headers[name]\n return Array.isArray(v) ? v[0] : v\n }\n const host = header('host') ?? 'localhost'\n const proto = req.protocol ?? (header('x-forwarded-proto') ?? 'https')\n const rawPath = req.originalUrl ?? req.url ?? '/'\n\n captureAgentHit(metrone, {\n url: `${proto}://${host}${rawPath}`,\n userAgent: header('user-agent'),\n referrer: header('referer') ?? header('referrer'),\n method: req.method,\n }, options)\n } catch {\n // never interfere with the request\n }\n next()\n }\n}\n\n/**\n * WHATWG-fetch helper for Hono, Next.js middleware/route handlers, Bun,\n * Deno, and Cloudflare Workers:\n *\n * ```ts\n * trackAgentRequest(metrone, request)\n * ```\n */\nexport function trackAgentRequest(\n metrone: MetroneServer,\n request: { url: string; method?: string; headers: { get(name: string): string | null } },\n options?: AgentTrackerOptions,\n): string | undefined {\n return captureAgentHit(metrone, {\n url: request.url,\n userAgent: request.headers.get('user-agent'),\n referrer: request.headers.get('referer'),\n method: request.method,\n }, options)\n}\n"],"mappings":"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","names":[]}
|