@inferevents/mcp 0.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.
@@ -0,0 +1,34 @@
1
+ import type { EventCountResult, RetentionResult, UserJourneyResult, Filter } from "@infer-events/shared";
2
+ import type { InferConfig } from "./config.js";
3
+ export declare class ApiClient {
4
+ private readonly baseUrl;
5
+ private readonly headers;
6
+ private readonly defaultProjectId;
7
+ constructor(config: InferConfig);
8
+ getEventCounts(params: {
9
+ eventName: string;
10
+ timeRange: string;
11
+ groupBy?: string;
12
+ filters?: Filter[];
13
+ projectId?: string;
14
+ }): Promise<EventCountResult>;
15
+ getRetention(params: {
16
+ startEvent: string;
17
+ returnEvent: string;
18
+ timeRange: string;
19
+ granularity: string;
20
+ projectId?: string;
21
+ }): Promise<RetentionResult>;
22
+ getUserJourney(params: {
23
+ userId: string;
24
+ timeRange?: string;
25
+ limit?: number;
26
+ projectId?: string;
27
+ }): Promise<UserJourneyResult>;
28
+ private get;
29
+ }
30
+ export declare class ApiError extends Error {
31
+ readonly statusCode: number;
32
+ constructor(message: string, statusCode: number);
33
+ }
34
+ //# sourceMappingURL=api-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,eAAe,EACf,iBAAiB,EACjB,MAAM,EACP,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;IACjD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqB;gBAE1C,MAAM,EAAE,WAAW;IASzB,cAAc,CAAC,MAAM,EAAE;QAC3B,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAuBvB,YAAY,CAAC,MAAM,EAAE;QACzB,UAAU,EAAE,MAAM,CAAC;QACnB,WAAW,EAAE,MAAM,CAAC;QACpB,SAAS,EAAE,MAAM,CAAC;QAClB,WAAW,EAAE,MAAM,CAAC;QACpB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,GAAG,OAAO,CAAC,eAAe,CAAC;IAiBtB,cAAc,CAAC,MAAM,EAAE;QAC3B,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,GAAG,OAAO,CAAC,iBAAiB,CAAC;YAsBhB,GAAG;CAmDlB;AAED,qBAAa,QAAS,SAAQ,KAAK;aAGf,UAAU,EAAE,MAAM;gBADlC,OAAO,EAAE,MAAM,EACC,UAAU,EAAE,MAAM;CAKrC"}
@@ -0,0 +1,99 @@
1
+ export class ApiClient {
2
+ baseUrl;
3
+ headers;
4
+ defaultProjectId;
5
+ constructor(config) {
6
+ this.baseUrl = config.endpoint;
7
+ this.headers = {
8
+ Authorization: `Bearer ${config.apiKey}`,
9
+ "Content-Type": "application/json",
10
+ };
11
+ this.defaultProjectId = config.projectId;
12
+ }
13
+ async getEventCounts(params) {
14
+ const query = new URLSearchParams();
15
+ query.set("event_name", params.eventName);
16
+ query.set("time_range", params.timeRange);
17
+ if (params.groupBy) {
18
+ query.set("group_by", params.groupBy);
19
+ }
20
+ if (params.filters && params.filters.length > 0) {
21
+ query.set("filters", JSON.stringify(params.filters));
22
+ }
23
+ const projectId = params.projectId ?? this.defaultProjectId;
24
+ if (projectId) {
25
+ query.set("project_id", projectId);
26
+ }
27
+ return this.get(`/v1/query/event-counts?${query.toString()}`);
28
+ }
29
+ async getRetention(params) {
30
+ const query = new URLSearchParams();
31
+ query.set("start_event", params.startEvent);
32
+ query.set("return_event", params.returnEvent);
33
+ query.set("time_range", params.timeRange);
34
+ query.set("granularity", params.granularity);
35
+ const projectId = params.projectId ?? this.defaultProjectId;
36
+ if (projectId) {
37
+ query.set("project_id", projectId);
38
+ }
39
+ return this.get(`/v1/query/retention?${query.toString()}`);
40
+ }
41
+ async getUserJourney(params) {
42
+ const query = new URLSearchParams();
43
+ query.set("user_id", params.userId);
44
+ if (params.timeRange) {
45
+ query.set("time_range", params.timeRange);
46
+ }
47
+ if (params.limit !== undefined) {
48
+ query.set("limit", String(params.limit));
49
+ }
50
+ const projectId = params.projectId ?? this.defaultProjectId;
51
+ if (projectId) {
52
+ query.set("project_id", projectId);
53
+ }
54
+ return this.get(`/v1/query/user-journey?${query.toString()}`);
55
+ }
56
+ async get(path) {
57
+ const url = `${this.baseUrl}${path}`;
58
+ let response;
59
+ try {
60
+ response = await fetch(url, {
61
+ method: "GET",
62
+ headers: this.headers,
63
+ });
64
+ }
65
+ catch (error) {
66
+ const message = error instanceof Error ? error.message : "Unknown network error";
67
+ throw new ApiError(`Failed to connect to Infer API at ${this.baseUrl}: ${message}. ` +
68
+ `Check that the endpoint in ~/.infer/config.json is correct.`, 0);
69
+ }
70
+ if (!response.ok) {
71
+ const body = await response.text().catch(() => "");
72
+ let detail = body;
73
+ try {
74
+ const parsed = JSON.parse(body);
75
+ detail = parsed.error ?? parsed.message ?? body;
76
+ }
77
+ catch {
78
+ // body wasn't JSON, use raw text
79
+ }
80
+ if (response.status === 401) {
81
+ throw new ApiError(`Authentication failed. Check that your API key in ~/.infer/config.json is valid.`, 401);
82
+ }
83
+ if (response.status === 403) {
84
+ throw new ApiError(`Access denied. Your API key may not have read permissions for this project.`, 403);
85
+ }
86
+ throw new ApiError(`Infer API returned ${response.status}: ${detail}`, response.status);
87
+ }
88
+ return (await response.json());
89
+ }
90
+ }
91
+ export class ApiError extends Error {
92
+ statusCode;
93
+ constructor(message, statusCode) {
94
+ super(message);
95
+ this.statusCode = statusCode;
96
+ this.name = "ApiError";
97
+ }
98
+ }
99
+ //# sourceMappingURL=api-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-client.js","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ export interface InferConfig {
2
+ apiKey: string;
3
+ endpoint: string;
4
+ projectId?: string;
5
+ }
6
+ export declare function loadConfig(): Promise<InferConfig>;
7
+ export declare class ConfigError extends Error {
8
+ constructor(message: string);
9
+ }
10
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAkBD,wBAAsB,UAAU,IAAI,OAAO,CAAC,WAAW,CAAC,CA+CvD;AAED,qBAAa,WAAY,SAAQ,KAAK;gBACxB,OAAO,EAAE,MAAM;CAI5B"}
package/dist/config.js ADDED
@@ -0,0 +1,57 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ const CONFIG_PATH = join(homedir(), ".infer", "config.json");
5
+ const SETUP_INSTRUCTIONS = `
6
+ Infer MCP server is not configured.
7
+
8
+ To set up, create ~/.infer/config.json with:
9
+
10
+ {
11
+ "apiKey": "pk_read_...",
12
+ "endpoint": "https://api.infer.events",
13
+ "projectId": "your-project-id"
14
+ }
15
+
16
+ Get your read API key from https://infer.events/settings/api-keys
17
+ `.trim();
18
+ export async function loadConfig() {
19
+ let raw;
20
+ try {
21
+ raw = await readFile(CONFIG_PATH, "utf-8");
22
+ }
23
+ catch {
24
+ throw new ConfigError(SETUP_INSTRUCTIONS);
25
+ }
26
+ let parsed;
27
+ try {
28
+ parsed = JSON.parse(raw);
29
+ }
30
+ catch {
31
+ throw new ConfigError(`Invalid JSON in ${CONFIG_PATH}. Please check the file syntax.`);
32
+ }
33
+ if (typeof parsed !== "object" ||
34
+ parsed === null ||
35
+ !("apiKey" in parsed) ||
36
+ typeof parsed.apiKey !== "string") {
37
+ throw new ConfigError(`Missing or invalid "apiKey" in ${CONFIG_PATH}.\n\n${SETUP_INSTRUCTIONS}`);
38
+ }
39
+ const config = parsed;
40
+ const apiKey = config.apiKey;
41
+ if (!apiKey.startsWith("pk_read_")) {
42
+ throw new ConfigError(`The MCP server requires a read API key (starts with "pk_read_"). ` +
43
+ `The key in ${CONFIG_PATH} appears to be a different key type.`);
44
+ }
45
+ const endpoint = typeof config.endpoint === "string"
46
+ ? config.endpoint.replace(/\/+$/, "")
47
+ : "https://api.infer.events";
48
+ const projectId = typeof config.projectId === "string" ? config.projectId : undefined;
49
+ return { apiKey, endpoint, projectId };
50
+ }
51
+ export class ConfigError extends Error {
52
+ constructor(message) {
53
+ super(message);
54
+ this.name = "ConfigError";
55
+ }
56
+ }
57
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAQlC,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,aAAa,CAAC,CAAC;AAE7D,MAAM,kBAAkB,GAAG;;;;;;;;;;;;CAY1B,CAAC,IAAI,EAAE,CAAC;AAET,MAAM,CAAC,KAAK,UAAU,UAAU;IAC9B,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IAC7C,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,WAAW,CAAC,kBAAkB,CAAC,CAAC;IAC5C,CAAC;IAED,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,WAAW,CACnB,mBAAmB,WAAW,iCAAiC,CAChE,CAAC;IACJ,CAAC;IAED,IACE,OAAO,MAAM,KAAK,QAAQ;QAC1B,MAAM,KAAK,IAAI;QACf,CAAC,CAAC,QAAQ,IAAI,MAAM,CAAC;QACrB,OAAQ,MAAkC,CAAC,MAAM,KAAK,QAAQ,EAC9D,CAAC;QACD,MAAM,IAAI,WAAW,CACnB,kCAAkC,WAAW,QAAQ,kBAAkB,EAAE,CAC1E,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,MAAiC,CAAC;IACjD,MAAM,MAAM,GAAG,MAAM,CAAC,MAAgB,CAAC;IAEvC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,WAAW,CACnB,mEAAmE;YACjE,cAAc,WAAW,sCAAsC,CAClE,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GACZ,OAAO,MAAM,CAAC,QAAQ,KAAK,QAAQ;QACjC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;QACrC,CAAC,CAAC,0BAA0B,CAAC;IAEjC,MAAM,SAAS,GACb,OAAO,MAAM,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;IAEtE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;AACzC,CAAC;AAED,MAAM,OAAO,WAAY,SAAQ,KAAK;IACpC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC;IAC5B,CAAC;CACF"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { loadConfig, ConfigError } from "./config.js";
4
+ import { createServer } from "./server.js";
5
+ async function main() {
6
+ let config;
7
+ try {
8
+ config = await loadConfig();
9
+ }
10
+ catch (error) {
11
+ if (error instanceof ConfigError) {
12
+ process.stderr.write(`\n${error.message}\n\n`);
13
+ process.exit(1);
14
+ }
15
+ throw error;
16
+ }
17
+ const server = createServer(config);
18
+ const transport = new StdioServerTransport();
19
+ await server.connect(transport);
20
+ }
21
+ main().catch((error) => {
22
+ const message = error instanceof Error ? error.message : String(error);
23
+ process.stderr.write(`Fatal: ${message}\n`);
24
+ process.exit(1);
25
+ });
26
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,KAAK,UAAU,IAAI;IACjB,IAAI,MAAM,CAAC;IACX,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,UAAU,EAAE,CAAC;IAC9B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,WAAW,EAAE,CAAC;YACjC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,KAAK,CAAC,OAAO,MAAM,CAAC,CAAC;YAC/C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;IAED,MAAM,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IACpC,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAE7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IAC9B,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACvE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,OAAO,IAAI,CAAC,CAAC;IAC5C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,4 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { InferConfig } from "./config.js";
3
+ export declare function createServer(config: InferConfig): McpServer;
4
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAKpE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAc/C,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,CAoG3D"}
package/dist/server.js ADDED
@@ -0,0 +1,89 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join, dirname } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { ApiClient, ApiError } from "./api-client.js";
6
+ import { getEventCountsSchema, handleGetEventCounts, } from "./tools/get-event-counts.js";
7
+ import { getRetentionSchema, handleGetRetention, } from "./tools/get-retention.js";
8
+ import { getUserJourneySchema, handleGetUserJourney, } from "./tools/get-user-journey.js";
9
+ export function createServer(config) {
10
+ const client = new ApiClient(config);
11
+ const server = new McpServer({
12
+ name: "infer-analytics",
13
+ version: "0.1.0",
14
+ });
15
+ // --- Resources ---
16
+ server.resource("analytics-skill", "infer://skill", {
17
+ description: "Analytics interpretation guide. Read this to learn how to interpret " +
18
+ "event counts, retention data, and user journeys like an experienced " +
19
+ "product analyst.",
20
+ mimeType: "text/markdown",
21
+ }, async () => {
22
+ const skillPath = join(dirname(fileURLToPath(import.meta.url)), "..", "skill.md");
23
+ const content = await readFile(skillPath, "utf-8");
24
+ return {
25
+ contents: [
26
+ {
27
+ uri: "infer://skill",
28
+ mimeType: "text/markdown",
29
+ text: content,
30
+ },
31
+ ],
32
+ };
33
+ });
34
+ // --- Tools ---
35
+ server.tool("get_event_counts", "Count events over a time range, optionally grouped by a property. " +
36
+ "Use this for questions like 'how many signups this week', " +
37
+ "'page views by country last 30 days', or 'purchase count trend'.", getEventCountsSchema, async (params) => {
38
+ try {
39
+ const text = await handleGetEventCounts(client, params);
40
+ return { content: [{ type: "text", text }] };
41
+ }
42
+ catch (error) {
43
+ return {
44
+ content: [{ type: "text", text: formatError(error) }],
45
+ isError: true,
46
+ };
47
+ }
48
+ });
49
+ server.tool("get_retention", "Cohort-based retention analysis. Shows what percentage of users who performed " +
50
+ "a start event came back to perform a return event. " +
51
+ "Use for questions like 'what is our week-over-week retention', " +
52
+ "'do users come back after signing up', or 'retention by monthly cohort'.", getRetentionSchema, async (params) => {
53
+ try {
54
+ const text = await handleGetRetention(client, params);
55
+ return { content: [{ type: "text", text }] };
56
+ }
57
+ catch (error) {
58
+ return {
59
+ content: [{ type: "text", text: formatError(error) }],
60
+ isError: true,
61
+ };
62
+ }
63
+ });
64
+ server.tool("get_user_journey", "Get the ordered sequence of events for a specific user. " +
65
+ "Use for questions like 'what did user X do', 'show me this user's activity', " +
66
+ "or 'trace a user's path through the app'.", getUserJourneySchema, async (params) => {
67
+ try {
68
+ const text = await handleGetUserJourney(client, params);
69
+ return { content: [{ type: "text", text }] };
70
+ }
71
+ catch (error) {
72
+ return {
73
+ content: [{ type: "text", text: formatError(error) }],
74
+ isError: true,
75
+ };
76
+ }
77
+ });
78
+ return server;
79
+ }
80
+ function formatError(error) {
81
+ if (error instanceof ApiError) {
82
+ return `API Error: ${error.message}`;
83
+ }
84
+ if (error instanceof Error) {
85
+ return `Error: ${error.message}`;
86
+ }
87
+ return `Unknown error occurred`;
88
+ }
89
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import { z } from "zod";
2
+ import type { ApiClient } from "../api-client.js";
3
+ export declare const getEventCountsSchema: {
4
+ event_name: z.ZodString;
5
+ time_range: z.ZodString;
6
+ group_by: z.ZodOptional<z.ZodString>;
7
+ filters: z.ZodOptional<z.ZodArray<z.ZodObject<{
8
+ field: z.ZodString;
9
+ op: z.ZodEnum<["eq", "neq", "gt", "lt", "gte", "lte", "contains"]>;
10
+ value: z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean]>;
11
+ }, "strip", z.ZodTypeAny, {
12
+ value: string | number | boolean;
13
+ field: string;
14
+ op: "eq" | "neq" | "gt" | "lt" | "gte" | "lte" | "contains";
15
+ }, {
16
+ value: string | number | boolean;
17
+ field: string;
18
+ op: "eq" | "neq" | "gt" | "lt" | "gte" | "lte" | "contains";
19
+ }>, "many">>;
20
+ };
21
+ export type GetEventCountsInput = z.infer<z.ZodObject<typeof getEventCountsSchema>>;
22
+ export declare function handleGetEventCounts(client: ApiClient, input: GetEventCountsInput): Promise<string>;
23
+ //# sourceMappingURL=get-event-counts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-event-counts.d.ts","sourceRoot":"","sources":["../../src/tools/get-event-counts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAGlD,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;CAqBhC,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CACvC,CAAC,CAAC,SAAS,CAAC,OAAO,oBAAoB,CAAC,CACzC,CAAC;AAEF,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC,MAAM,CAAC,CA0CjB"}
@@ -0,0 +1,52 @@
1
+ import { z } from "zod";
2
+ import { filterSchema } from "@infer-events/shared";
3
+ export const getEventCountsSchema = {
4
+ event_name: z
5
+ .string()
6
+ .describe("Name of the event to count (e.g., 'signup', 'page_view', 'purchase')"),
7
+ time_range: z
8
+ .string()
9
+ .describe('Time range: "last_24h", "last_7d", "last_30d", "last_90d", or ISO range "2024-01-01/2024-01-31"'),
10
+ group_by: z
11
+ .string()
12
+ .optional()
13
+ .describe("Property name to group results by (e.g., 'country', 'plan', 'source')"),
14
+ filters: z
15
+ .array(filterSchema)
16
+ .optional()
17
+ .describe("Filters: [{field, op, value}]"),
18
+ };
19
+ export async function handleGetEventCounts(client, input) {
20
+ const result = await client.getEventCounts({
21
+ eventName: input.event_name,
22
+ timeRange: input.time_range,
23
+ groupBy: input.group_by,
24
+ filters: input.filters,
25
+ });
26
+ const lines = [];
27
+ if (result.warning) {
28
+ lines.push(`Warning: ${result.warning}`);
29
+ lines.push("");
30
+ }
31
+ lines.push(`Event: ${input.event_name}`);
32
+ lines.push(`Time range: ${input.time_range}`);
33
+ lines.push(`Total count: ${result.total.toLocaleString()}`);
34
+ if (result.groups.length > 0 && input.group_by) {
35
+ lines.push("");
36
+ lines.push(`Grouped by: ${input.group_by}`);
37
+ lines.push("---");
38
+ for (const group of result.groups) {
39
+ const pct = result.total > 0
40
+ ? ((group.count / result.total) * 100).toFixed(1)
41
+ : "0.0";
42
+ lines.push(` ${group.key}: ${group.count.toLocaleString()} (${pct}%)`);
43
+ }
44
+ }
45
+ if (result.total === 0) {
46
+ lines.push("");
47
+ lines.push("No events found. This could mean the event is not being tracked, " +
48
+ "the event name is misspelled, or there was no activity in this time range.");
49
+ }
50
+ return lines.join("\n");
51
+ }
52
+ //# sourceMappingURL=get-event-counts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-event-counts.js","sourceRoot":"","sources":["../../src/tools/get-event-counts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEpD,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,UAAU,EAAE,CAAC;SACV,MAAM,EAAE;SACR,QAAQ,CACP,sEAAsE,CACvE;IACH,UAAU,EAAE,CAAC;SACV,MAAM,EAAE;SACR,QAAQ,CACP,iGAAiG,CAClG;IACH,QAAQ,EAAE,CAAC;SACR,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CACP,uEAAuE,CACxE;IACH,OAAO,EAAE,CAAC;SACP,KAAK,CAAC,YAAY,CAAC;SACnB,QAAQ,EAAE;SACV,QAAQ,CAAC,+BAA+B,CAAC;CAC7C,CAAC;AAMF,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,MAAiB,EACjB,KAA0B;IAE1B,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC;QACzC,SAAS,EAAE,KAAK,CAAC,UAAU;QAC3B,SAAS,EAAE,KAAK,CAAC,UAAU;QAC3B,OAAO,EAAE,KAAK,CAAC,QAAQ;QACvB,OAAO,EAAE,KAAK,CAAC,OAAO;KACvB,CAAC,CAAC;IAEH,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QACnB,KAAK,CAAC,IAAI,CAAC,YAAY,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;QACzC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACjB,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,UAAU,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC;IACzC,KAAK,CAAC,IAAI,CAAC,eAAe,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC;IAC9C,KAAK,CAAC,IAAI,CAAC,gBAAgB,MAAM,CAAC,KAAK,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;IAE5D,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;QAC/C,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CAAC,eAAe,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC;QAC5C,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAElB,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;YAClC,MAAM,GAAG,GACP,MAAM,CAAC,KAAK,GAAG,CAAC;gBACd,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;gBACjD,CAAC,CAAC,KAAK,CAAC;YACZ,KAAK,CAAC,IAAI,CAAC,KAAK,KAAK,CAAC,GAAG,KAAK,KAAK,CAAC,KAAK,CAAC,cAAc,EAAE,KAAK,GAAG,IAAI,CAAC,CAAC;QAC1E,CAAC;IACH,CAAC;IAED,IAAI,MAAM,CAAC,KAAK,KAAK,CAAC,EAAE,CAAC;QACvB,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CACR,mEAAmE;YACjE,4EAA4E,CAC/E,CAAC;IACJ,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC"}
@@ -0,0 +1,11 @@
1
+ import { z } from "zod";
2
+ import type { ApiClient } from "../api-client.js";
3
+ export declare const getRetentionSchema: {
4
+ start_event: z.ZodString;
5
+ return_event: z.ZodString;
6
+ time_range: z.ZodString;
7
+ granularity: z.ZodEnum<["day", "week", "month"]>;
8
+ };
9
+ export type GetRetentionInput = z.infer<z.ZodObject<typeof getRetentionSchema>>;
10
+ export declare function handleGetRetention(client: ApiClient, input: GetRetentionInput): Promise<string>;
11
+ //# sourceMappingURL=get-retention.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-retention.d.ts","sourceRoot":"","sources":["../../src/tools/get-retention.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAElD,eAAO,MAAM,kBAAkB;;;;;CAmB9B,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CACrC,CAAC,CAAC,SAAS,CAAC,OAAO,kBAAkB,CAAC,CACvC,CAAC;AAEF,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,iBAAiB,GACvB,OAAO,CAAC,MAAM,CAAC,CA6EjB"}
@@ -0,0 +1,76 @@
1
+ import { z } from "zod";
2
+ export const getRetentionSchema = {
3
+ start_event: z
4
+ .string()
5
+ .describe('The event that defines cohort entry (e.g., "signup", "first_purchase")'),
6
+ return_event: z
7
+ .string()
8
+ .describe('The event that counts as a return (e.g., "page_view", "purchase", "login")'),
9
+ time_range: z
10
+ .string()
11
+ .describe('Time range: "last_7d", "last_30d", "last_90d", or ISO range "2024-01-01/2024-03-31"'),
12
+ granularity: z
13
+ .enum(["day", "week", "month"])
14
+ .describe('Cohort granularity: "day", "week", or "month"'),
15
+ };
16
+ export async function handleGetRetention(client, input) {
17
+ const result = await client.getRetention({
18
+ startEvent: input.start_event,
19
+ returnEvent: input.return_event,
20
+ timeRange: input.time_range,
21
+ granularity: input.granularity,
22
+ });
23
+ const lines = [];
24
+ if (result.warning) {
25
+ lines.push(`Warning: ${result.warning}`);
26
+ lines.push("");
27
+ }
28
+ lines.push(`Retention Analysis`);
29
+ lines.push(`Start event: ${input.start_event}`);
30
+ lines.push(`Return event: ${input.return_event}`);
31
+ lines.push(`Time range: ${input.time_range}`);
32
+ lines.push(`Granularity: ${input.granularity}`);
33
+ lines.push("");
34
+ if (result.cohorts.length === 0) {
35
+ lines.push("No cohort data found. Check that both events are being tracked " +
36
+ "and that there is activity in this time range.");
37
+ return lines.join("\n");
38
+ }
39
+ // Format as a table
40
+ const maxPeriods = Math.max(...result.cohorts.map((c) => c.retained.length));
41
+ // Header
42
+ const header = ["Cohort", "Users"];
43
+ for (let i = 0; i < maxPeriods; i++) {
44
+ header.push(`${input.granularity} ${i}`);
45
+ }
46
+ lines.push(header.join(" | "));
47
+ lines.push(header.map((h) => "-".repeat(h.length)).join("-|-"));
48
+ // Rows
49
+ for (const cohort of result.cohorts) {
50
+ const row = [cohort.period, String(cohort.users_start)];
51
+ for (let i = 0; i < maxPeriods; i++) {
52
+ const retained = cohort.retained[i];
53
+ if (retained === undefined) {
54
+ row.push("-");
55
+ }
56
+ else {
57
+ const pct = cohort.users_start > 0
58
+ ? ((retained / cohort.users_start) * 100).toFixed(1)
59
+ : "0.0";
60
+ row.push(`${pct}%`);
61
+ }
62
+ }
63
+ lines.push(row.join(" | "));
64
+ }
65
+ // Summary
66
+ lines.push("");
67
+ const latestCohort = result.cohorts[0];
68
+ if (latestCohort && latestCohort.retained.length > 1) {
69
+ const firstPeriodRetention = latestCohort.users_start > 0 && latestCohort.retained[1] !== undefined
70
+ ? ((latestCohort.retained[1] / latestCohort.users_start) * 100).toFixed(1)
71
+ : "N/A";
72
+ lines.push(`Latest cohort ${input.granularity}-1 retention: ${firstPeriodRetention}%`);
73
+ }
74
+ return lines.join("\n");
75
+ }
76
+ //# sourceMappingURL=get-retention.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-retention.js","sourceRoot":"","sources":["../../src/tools/get-retention.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import { z } from "zod";
2
+ import type { ApiClient } from "../api-client.js";
3
+ export declare const getUserJourneySchema: {
4
+ user_id: z.ZodString;
5
+ time_range: z.ZodOptional<z.ZodString>;
6
+ limit: z.ZodOptional<z.ZodNumber>;
7
+ };
8
+ export type GetUserJourneyInput = z.infer<z.ZodObject<typeof getUserJourneySchema>>;
9
+ export declare function handleGetUserJourney(client: ApiClient, input: GetUserJourneyInput): Promise<string>;
10
+ //# sourceMappingURL=get-user-journey.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-user-journey.d.ts","sourceRoot":"","sources":["../../src/tools/get-user-journey.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAElD,eAAO,MAAM,oBAAoB;;;;CAYhC,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CACvC,CAAC,CAAC,SAAS,CAAC,OAAO,oBAAoB,CAAC,CACzC,CAAC;AAEF,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC,MAAM,CAAC,CAgFjB"}
@@ -0,0 +1,76 @@
1
+ import { z } from "zod";
2
+ export const getUserJourneySchema = {
3
+ user_id: z.string().describe("The user ID to look up"),
4
+ time_range: z
5
+ .string()
6
+ .optional()
7
+ .describe('Time range to filter events: "last_24h", "last_7d", "last_30d", or ISO range'),
8
+ limit: z
9
+ .number()
10
+ .optional()
11
+ .describe("Max events to return (default 50, max 200)"),
12
+ };
13
+ export async function handleGetUserJourney(client, input) {
14
+ const result = await client.getUserJourney({
15
+ userId: input.user_id,
16
+ timeRange: input.time_range,
17
+ limit: input.limit,
18
+ });
19
+ const lines = [];
20
+ if (result.warning) {
21
+ lines.push(`Warning: ${result.warning}`);
22
+ lines.push("");
23
+ }
24
+ lines.push(`User Journey: ${input.user_id}`);
25
+ if (input.time_range) {
26
+ lines.push(`Time range: ${input.time_range}`);
27
+ }
28
+ lines.push(`Events returned: ${result.events.length}`);
29
+ lines.push("");
30
+ if (result.events.length === 0) {
31
+ lines.push("No events found for this user. Check that the user ID is correct " +
32
+ "and that events have been tracked for this user.");
33
+ return lines.join("\n");
34
+ }
35
+ for (const event of result.events) {
36
+ const ts = new Date(event.timestamp).toISOString();
37
+ const propsEntries = Object.entries(event.properties).filter(([, v]) => v !== null);
38
+ let propStr = "";
39
+ if (propsEntries.length > 0) {
40
+ const formatted = propsEntries
41
+ .map(([k, v]) => `${k}=${JSON.stringify(v)}`)
42
+ .join(", ");
43
+ propStr = ` (${formatted})`;
44
+ }
45
+ lines.push(`[${ts}] ${event.event_name}${propStr}`);
46
+ }
47
+ // Compute session summary
48
+ lines.push("");
49
+ lines.push("--- Summary ---");
50
+ const eventCounts = new Map();
51
+ for (const event of result.events) {
52
+ eventCounts.set(event.event_name, (eventCounts.get(event.event_name) ?? 0) + 1);
53
+ }
54
+ const sorted = [...eventCounts.entries()].sort((a, b) => b[1] - a[1]);
55
+ lines.push("Event frequency:");
56
+ for (const [name, count] of sorted) {
57
+ lines.push(` ${name}: ${count}`);
58
+ }
59
+ if (result.events.length >= 2) {
60
+ const first = new Date(result.events[0].timestamp);
61
+ const last = new Date(result.events[result.events.length - 1].timestamp);
62
+ const spanMs = Math.abs(last.getTime() - first.getTime());
63
+ const spanMinutes = Math.round(spanMs / 60000);
64
+ if (spanMinutes < 60) {
65
+ lines.push(`Time span: ${spanMinutes} minutes`);
66
+ }
67
+ else if (spanMinutes < 1440) {
68
+ lines.push(`Time span: ${(spanMinutes / 60).toFixed(1)} hours`);
69
+ }
70
+ else {
71
+ lines.push(`Time span: ${(spanMinutes / 1440).toFixed(1)} days`);
72
+ }
73
+ }
74
+ return lines.join("\n");
75
+ }
76
+ //# sourceMappingURL=get-user-journey.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-user-journey.js","sourceRoot":"","sources":["../../src/tools/get-user-journey.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@inferevents/mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for Infer analytics — lets AI agents query your event data",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "bin": {
9
+ "infer-mcp": "./dist/index.js"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "skill.md"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc",
23
+ "typecheck": "tsc --noEmit",
24
+ "test": "vitest run",
25
+ "lint": "tsc --noEmit"
26
+ },
27
+ "dependencies": {
28
+ "@inferevents/shared": "*",
29
+ "@modelcontextprotocol/sdk": "^1.12",
30
+ "zod": "^3.24"
31
+ },
32
+ "devDependencies": {
33
+ "typescript": "^5.7"
34
+ },
35
+ "license": "MIT"
36
+ }
package/skill.md ADDED
@@ -0,0 +1,215 @@
1
+ # Infer Analytics — Agent Skill Guide
2
+
3
+ You have access to product analytics through the Infer MCP server. This guide teaches you how to query data effectively and interpret results like an experienced product analyst.
4
+
5
+ ## Tool Routing — Which Tool for Which Question
6
+
7
+ ### Use `get_event_counts` when asked about:
8
+ - "How many X happened?" — count a single event
9
+ - "What's our signup rate?" — count signups over a time range
10
+ - "Break down purchases by country" — count with group_by
11
+ - "Compare this week vs last week" — run two queries with different time_ranges
12
+ - "What's our most popular feature?" — count multiple events or group by event property
13
+ - Anything involving totals, volumes, frequencies, or distributions
14
+
15
+ ### Use `get_retention` when asked about:
16
+ - "Are users coming back?" — classic retention question
17
+ - "What's our week-over-week retention?" — cohort analysis
18
+ - "Do users who sign up actually stick around?" — start_event=signup, return_event=any active event
19
+ - "Is our product sticky?" — retention is the answer
20
+ - "How does retention compare month over month?" — look at cohort trends
21
+ - Anything involving repeat usage, churn, stickiness, or engagement over time
22
+
23
+ ### Use `get_user_journey` when asked about:
24
+ - "What did user X do?" — full event timeline
25
+ - "Show me this user's activity" — chronological event list
26
+ - "What happened before the user churned?" — look at events before last activity
27
+ - "How did this user find the aha moment?" — trace their path
28
+ - "Debug this user's experience" — see exactly what happened
29
+ - Anything about a specific individual user's behavior
30
+
31
+ ## Interpreting Event Counts
32
+
33
+ ### Reading the numbers
34
+ - **Total is the headline.** Start there. Is it growing, flat, or declining?
35
+ - **Groups reveal composition.** If signups=1000 but 900 are from one country, that's concentration risk.
36
+ - **Zero results almost always mean a tracking issue**, not zero activity. Suggest checking event names or SDK integration.
37
+
38
+ ### Percentage breakdowns
39
+ When results are grouped, calculate and mention percentages. "The US accounts for 45% of signups" is more useful than "US: 450, UK: 120, ...".
40
+
41
+ ### Time comparisons
42
+ When the user asks about trends, run the same query across two time ranges and compare:
43
+ ```
44
+ last_7d vs previous 7 days → "Signups are up 23% week-over-week"
45
+ last_30d vs previous 30 days → "Monthly purchases declined 8%"
46
+ ```
47
+
48
+ Always mention the absolute numbers alongside percentages. "Up 23% (from 340 to 418)" is much more useful than just "up 23%".
49
+
50
+ ### Small sample warning
51
+ - **N < 30**: Prefix your interpretation with "Small sample size — treat this as directional, not conclusive."
52
+ - **N < 10**: Say "Too few data points to draw meaningful conclusions. Check if event tracking is working correctly."
53
+ - **N = 0**: Almost certainly a tracking issue. Don't say "no users did X" — say "no events were recorded, which likely means the event isn't being tracked or the event name may be different."
54
+
55
+ ## Interpreting Retention
56
+
57
+ ### How to read a retention table
58
+ Each row is a cohort (users who started in that period). Each column shows what % came back in subsequent periods.
59
+
60
+ Example:
61
+ ```
62
+ Cohort | Users | week 0 | week 1 | week 2 | week 3 | week 4
63
+ 2024-01-01 | 200 | 100% | 42% | 31% | 25% | 22%
64
+ 2024-01-08 | 180 | 100% | 38% | 28% | 23% | -
65
+ ```
66
+
67
+ - Week 0 is always 100% (that's when they entered the cohort)
68
+ - The drop from week 0 to week 1 is the biggest — that's normal
69
+ - Look for the "flattening point" where retention stabilizes
70
+
71
+ ### Retention benchmarks
72
+
73
+ #### B2C Apps (social, content, utility)
74
+ | Period | Concerning | Acceptable | Good | Excellent |
75
+ |-----------|-----------|------------|--------|-----------|
76
+ | Week 1 | < 25% | 25-35% | 35-50% | > 50% |
77
+ | Week 4 | < 10% | 10-20% | 20-30% | > 30% |
78
+ | Month 3 | < 5% | 5-10% | 10-20% | > 20% |
79
+
80
+ #### B2B / SaaS
81
+ | Period | Concerning | Acceptable | Good | Excellent |
82
+ |-----------|-----------|------------|--------|-----------|
83
+ | Week 1 | < 40% | 40-55% | 55-70% | > 70% |
84
+ | Week 4 | < 25% | 25-40% | 40-55% | > 55% |
85
+ | Month 3 | < 15% | 15-30% | 30-50% | > 50% |
86
+
87
+ **Important:** These benchmarks are guidelines, not gospel. A niche B2B tool with 60% month-3 retention serving 50 users may be healthier than a consumer app with 15% retention serving 50,000. Context matters.
88
+
89
+ ### What the retention curve shape tells you
90
+
91
+ - **Steep early drop, then flat**: Normal. Users who survive week 1-2 tend to stick. Focus on improving early activation.
92
+ - **Gradual continuous decline**: No "aha moment." Users try it but don't form a habit. The product may not be solving a recurring need.
93
+ - **Flat retention (barely drops)**: Exceptional. Either a deeply habitual product or a small self-selected power user base.
94
+ - **Retention improves in later cohorts**: Product-market fit is improving. Ship is heading in the right direction.
95
+ - **Retention worsens in later cohorts**: Red flag. Could be changing user quality (scaled acquisition too fast) or product changes that hurt experience.
96
+
97
+ ### Follow-up actions after retention analysis
98
+ 1. **Low retention?** Check the onboarding funnel. Run `get_event_counts` for setup/onboarding events to see where users drop off.
99
+ 2. **High early churn?** Look at the user journey for churned users — what did they do (or not do) before leaving?
100
+ 3. **Improving retention?** Identify what changed — new features, onboarding changes, or audience shift?
101
+
102
+ ## Interpreting User Journeys
103
+
104
+ ### What to look for in a user timeline
105
+ 1. **First event**: How did they enter? Signup? Page view? Referral link?
106
+ 2. **Time between events**: Long gaps suggest the user left and came back. Short bursts suggest engaged sessions.
107
+ 3. **Activation events**: Did they perform key actions? (create, submit, purchase, invite — not just page views)
108
+ 4. **Last event before churn**: What was the user doing right before they stopped?
109
+ 5. **Repeated patterns**: Do they keep going back to the same page? Could indicate confusion or a favorite feature.
110
+
111
+ ### Activation vs. vanity events
112
+ - **Activation events** (high signal): create_project, submit_form, make_purchase, invite_teammate, complete_onboarding
113
+ - **Vanity events** (low signal): page_view, button_hover, scroll, tooltip_shown
114
+ - When assessing if a user is "active," weight activation events heavily. A user with 50 page views but zero creation events is probably not activated.
115
+
116
+ ### Session detection
117
+ Events close together in time (< 30 minutes apart) are likely the same session. Large gaps indicate separate sessions. When describing a journey, group events into sessions:
118
+ ```
119
+ Session 1 (Jan 15, 10:02am - 10:14am, 8 events):
120
+ signup → onboarding_start → profile_setup → ...
121
+
122
+ Session 2 (Jan 16, 2:30pm - 2:45pm, 5 events):
123
+ login → dashboard_view → ...
124
+ ```
125
+
126
+ ## Framing Findings for Non-Technical Users
127
+
128
+ ### Do
129
+ - Lead with the insight, not the query: "Your signups grew 30% this week" not "The get_event_counts query returned a total of 1,300"
130
+ - Use plain language: "About 1 in 4 users come back after a week" not "Week-1 retention is 25.3%"
131
+ - Provide context: "That's in line with typical B2C apps" or "That's above average for SaaS"
132
+ - Suggest next steps: "To improve this, you might want to look at your onboarding flow"
133
+ - Round numbers for readability: "About 1,200" not "1,187"
134
+
135
+ ### Don't
136
+ - Don't claim causation from correlation: "Users who complete onboarding retain better" ≠ "Completing onboarding causes better retention." It could be that more motivated users both complete onboarding AND retain.
137
+ - Don't extrapolate from small samples: 5 users is an anecdote, not a trend.
138
+ - Don't present API response format to the user: translate numbers into insights.
139
+ - Don't say "the data shows" without saying what it means.
140
+ - Don't hide uncertainty: if the data is ambiguous, say so.
141
+
142
+ ## Common Analysis Patterns
143
+
144
+ ### Pattern 1: Quick health check
145
+ Run these queries to get a fast pulse on the product:
146
+ 1. `get_event_counts(event_name="signup", time_range="last_7d")` — Are people signing up?
147
+ 2. `get_event_counts(event_name="<core_action>", time_range="last_7d")` — Are they using the core feature?
148
+ 3. `get_retention(start_event="signup", return_event="<core_action>", time_range="last_30d", granularity="week")` — Are they coming back?
149
+
150
+ ### Pattern 2: Funnel analysis
151
+ Count events at each step to build a funnel:
152
+ 1. signup → onboarding_start → onboarding_complete → first_core_action
153
+ 2. Run `get_event_counts` for each step in the same time range
154
+ 3. Calculate drop-off between each step
155
+ 4. The biggest drop-off is your highest-leverage improvement area
156
+
157
+ ### Pattern 3: Debugging a user issue
158
+ 1. `get_user_journey(user_id="...")` — see what happened
159
+ 2. Look for errors, unusual patterns, or missing expected events
160
+ 3. Compare with a journey from a successful user
161
+
162
+ ### Pattern 4: Feature adoption
163
+ 1. `get_event_counts(event_name="feature_used", time_range="last_30d")` — How many times was it used?
164
+ 2. `get_event_counts(event_name="feature_used", time_range="last_30d", group_by="user_id")` — How many unique users? (If grouping by user_id is supported)
165
+ 3. Compare to total active users to get adoption rate
166
+
167
+ ### Pattern 5: Growth trend
168
+ Run the same `get_event_counts` query for sequential time ranges:
169
+ - last_7d, then previous 7d (using ISO range) → week-over-week growth
170
+ - Positive trend + healthy retention = genuine growth
171
+ - Positive trend + declining retention = leaky bucket (acquiring users faster than losing them, but not sustainably)
172
+
173
+ ## Daily Health Check Routine
174
+
175
+ When asked to do a daily/weekly check, run these in order:
176
+
177
+ 1. **Volume check**: Count core events for last_24h (or last_7d for weekly)
178
+ 2. **Trend check**: Compare to the previous period — up or down?
179
+ 3. **Retention check**: Weekly or monthly retention — holding steady?
180
+ 4. **Anomaly scan**: Are any event counts dramatically different from normal? (>50% change deserves investigation)
181
+
182
+ Report format:
183
+ ```
184
+ Product Health — [date]
185
+
186
+ Signups: [N] ([+/- X%] vs last period)
187
+ Core action: [N] ([+/- X%] vs last period)
188
+ Week-1 retention: [X%] (benchmark: [good/concerning/etc.])
189
+
190
+ Noteworthy: [anything unusual or worth investigating]
191
+ ```
192
+
193
+ ## What NOT To Do
194
+
195
+ 1. **Never claim causation from analytics alone.** Analytics shows correlation and sequence, not causation. "Users who do X retain better" is correlation. Only an experiment (A/B test) can establish causation.
196
+
197
+ 2. **Never extrapolate confidently from small samples.** If a cohort has 12 users, a single user's behavior swings retention by 8 percentage points. Always note sample size.
198
+
199
+ 3. **Never ignore the denominator.** "Feature X was used 500 times!" means nothing without knowing how many users had the opportunity to use it. 500 uses by 2 power users is very different from 500 uses by 200 users.
200
+
201
+ 4. **Never assume zero events means zero activity.** It almost always means the event isn't tracked, the name is wrong, or the time range is off. Verify before concluding.
202
+
203
+ 5. **Never present raw numbers without interpretation.** Your job is to turn data into insights. "Total: 1,247" is data. "Signups are up 23% week-over-week, which suggests the new landing page is working" is an insight.
204
+
205
+ 6. **Never compare different time ranges without adjusting.** 7 days of data will almost always be less than 30 days. Compare rates (per day) or use matching periods.
206
+
207
+ 7. **Never present averages without distribution context.** "Average session length is 5 minutes" could mean everyone stays 5 minutes, or half stay 10 seconds and half stay 10 minutes. If you have group_by data, mention the spread.
208
+
209
+ ## Edge Cases and Gotchas
210
+
211
+ - **Timezone effects**: Events near midnight may appear in different days depending on timezone. If daily counts look weird at period boundaries, this is likely the cause.
212
+ - **Bot traffic**: Unusually high page_view counts with no corresponding user actions could be bot traffic.
213
+ - **Duplicate events**: If counts seem inflated, consider whether the SDK might be double-firing events.
214
+ - **Cohort maturity**: The most recent cohort in retention data will always have fewer data points. Don't compare its incomplete retention to older cohorts' complete retention.
215
+ - **Survivorship bias**: Users who are still active are not representative of all users. When analyzing active user behavior, remember you're seeing the survivors, not the ones who left.