@rawdash/connector-mixpanel 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,182 @@
1
+ # @rawdash/connector-mixpanel
2
+
3
+ Rawdash connector for [Mixpanel](https://mixpanel.com) — syncs daily/weekly/monthly active users, per-event volume, declared funnel conversion data, and cohort retention into the six-shape storage model via the [Mixpanel Query API](https://developer.mixpanel.com/reference/query-api).
4
+
5
+ ## Auth setup
6
+
7
+ The connector authenticates with a Mixpanel **service account** (recommended for server-to-server use). Service account credentials are project-scoped and survive user offboarding.
8
+
9
+ 1. Open your Mixpanel project and navigate to **Settings → Project Settings → Service Accounts** (Mixpanel docs: [Creating a Service Account](https://developer.mixpanel.com/reference/service-accounts)).
10
+ 2. Click **Add Service Account**.
11
+ 3. Give it a descriptive name (e.g. `rawdash-reader`).
12
+ 4. Choose a role of **Consumer** (read-only is sufficient for query access).
13
+ 5. Set an expiration (a long-lived secret is fine for server use; pick a date that fits your rotation policy).
14
+ 6. Click **Create**. Mixpanel will display the **Username** and **Secret** once; copy both before closing the dialog.
15
+ 7. Store the secret in your secrets manager under (for example) `MIXPANEL_SECRET`. The username is not sensitive and can live in plain configuration.
16
+ 8. Note your **Project ID** under **Settings → Project Settings → Overview**.
17
+
18
+ The connector calls the API with HTTP Basic auth (`Authorization: Basic base64(<username>:<secret>)`) and includes `project_id=<id>` on every request, which is the contract Mixpanel requires for service-account access.
19
+
20
+ ## Configuration
21
+
22
+ ```ts
23
+ import { secret } from '@rawdash/core';
24
+
25
+ const mixpanel = {
26
+ name: 'mixpanel',
27
+ connectorId: 'mixpanel',
28
+ config: {
29
+ projectId: '1234567',
30
+ username: 'rawdash-reader.abcdef.mp-service-account',
31
+ secret: secret('MIXPANEL_SECRET'),
32
+ region: 'us', // 'us' (default) or 'eu' for EU residency
33
+ events: ['Signed Up', 'Purchase'],
34
+ funnels: [
35
+ { id: 42, name: 'Activation' },
36
+ { id: 99, name: 'Checkout' },
37
+ ],
38
+ retentionEvent: 'Signed Up',
39
+ activeUserEvent: 'Signed Up',
40
+ lookbackDays: 90,
41
+ },
42
+ };
43
+ ```
44
+
45
+ Register the connector class when mounting the engine:
46
+
47
+ ```ts
48
+ import { MixpanelConnector } from '@rawdash/connector-mixpanel';
49
+ import { mountEngine } from '@rawdash/hono';
50
+
51
+ mountEngine(config, {
52
+ connectorRegistry: { mixpanel: MixpanelConnector },
53
+ });
54
+ ```
55
+
56
+ Then wire it into `defineConfig`:
57
+
58
+ ```ts
59
+ import { defineConfig, defineDashboard, defineMetric } from '@rawdash/core';
60
+
61
+ export default defineConfig({
62
+ connectors: [mixpanel],
63
+ dashboards: {
64
+ product: defineDashboard({
65
+ widgets: {
66
+ dau: {
67
+ kind: 'stat',
68
+ title: 'DAU',
69
+ metric: defineMetric({
70
+ connector: mixpanel,
71
+ shape: 'metric',
72
+ name: 'mixpanel_dau',
73
+ field: 'value',
74
+ fn: 'latest',
75
+ }),
76
+ },
77
+ dau_trend: {
78
+ kind: 'timeseries',
79
+ title: 'DAU over time',
80
+ window: '30d',
81
+ metric: defineMetric({
82
+ connector: mixpanel,
83
+ shape: 'metric',
84
+ name: 'mixpanel_dau',
85
+ field: 'value',
86
+ fn: 'sum',
87
+ window: '30d',
88
+ groupBy: { field: 'ts', granularity: 'day' },
89
+ }),
90
+ },
91
+ signups_per_day: {
92
+ kind: 'timeseries',
93
+ title: 'Sign-ups per day',
94
+ window: '30d',
95
+ metric: defineMetric({
96
+ connector: mixpanel,
97
+ shape: 'metric',
98
+ name: 'mixpanel_events_per_day',
99
+ field: 'count',
100
+ fn: 'sum',
101
+ window: '30d',
102
+ filter: [{ field: 'event', op: 'eq', value: 'Signed Up' }],
103
+ groupBy: { field: 'ts', granularity: 'day' },
104
+ }),
105
+ },
106
+ events_distribution: {
107
+ kind: 'distribution',
108
+ title: 'Top events (last 30d)',
109
+ metric: defineMetric({
110
+ connector: mixpanel,
111
+ shape: 'metric',
112
+ name: 'mixpanel_events_per_day',
113
+ field: 'count',
114
+ fn: 'sum',
115
+ window: '30d',
116
+ groupBy: { field: 'event' },
117
+ }),
118
+ },
119
+ },
120
+ }),
121
+ },
122
+ });
123
+ ```
124
+
125
+ ## Configuration reference
126
+
127
+ | Field | Required | Type | Default | Notes |
128
+ | ----------------- | -------- | ----------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
129
+ | `projectId` | yes | `string` (digits) | — | Mixpanel numeric project ID. |
130
+ | `username` | yes | `string` | — | Service-account username. |
131
+ | `secret` | yes | `Secret` | — | Service-account secret. Use `secret('MIXPANEL_SECRET')`. |
132
+ | `region` | no | `'us' \| 'eu'` | `'us'` | Switches the API host between `mixpanel.com` and `eu.mixpanel.com`. |
133
+ | `events` | no | `string[]` | — | Event names to fetch per-day volume + unique-user counts for. Skips the phase when empty/unset. |
134
+ | `funnels` | no | `{ id, name? }[]` | — | Mixpanel funnel IDs to track. Each entry produces daily funnel-step samples. Skips when empty/unset. |
135
+ | `retentionEvent` | no | `string` | — | Event used for the retention cohort phase. Skips when unset. |
136
+ | `activeUserEvent` | no | `string` | — | Event used for the DAU/WAU/MAU `unique`-type segmentation queries. Defaults to the first `events` entry. Both DAU/WAU/MAU phases skip when no event resolves. |
137
+ | `lookbackDays` | no | `number` | `90` | Window in days fetched on a full sync. Incremental syncs (`mode: 'latest'`) refetch the trailing 3 days regardless. |
138
+
139
+ ## Data model
140
+
141
+ All resources are stored as **metric samples** (`shape: 'metric'`). The `ts` field is the bucket date in Unix milliseconds. Mixpanel returns aggregated data, so the connector writes pre-aggregated metric rows — no event-stream backfill.
142
+
143
+ | Metric name | Bucket | `value` | Attributes |
144
+ | ------------------------- | ------------------ | ----------------------- | ----------------------------------------------------------------------------------------------- |
145
+ | `mixpanel_dau` | day | unique users that day | `unit='day'`, `event` |
146
+ | `mixpanel_wau` | week | unique users that week | `unit='week'`, `event` |
147
+ | `mixpanel_mau` | month | unique users that month | `unit='month'`, `event` |
148
+ | `mixpanel_events_per_day` | day | total event count | `event`, `count`, `uniqueUsers` |
149
+ | `mixpanel_funnel_results` | day, step | users at the step | `funnelId`, `funnelName?`, `step`, `stepLabel`, `users`, `conversionRate`, `stepConversionRate` |
150
+ | `mixpanel_retention` | cohort day, period | retained users | `event`, `period` (days since cohort), `cohortSize`, `retentionRate` |
151
+
152
+ ## Sync behaviour
153
+
154
+ - **Backfill** (`mode: 'full'`): fetches the rolling `lookbackDays` window (default 90 days) for every configured resource.
155
+ - **Incremental** (`mode: 'latest'`): refetches the trailing 3 days for every configured resource. Mixpanel can re-attribute late-arriving events, so a small overlap keeps the metrics accurate without re-syncing the full backfill.
156
+ - **Idempotency**: each phase writes via a single `storage.metrics(samples, { names: [<metric>] })` call, which replaces all prior samples for that metric. Re-running the sync against the same window converges on the same storage state.
157
+ - **Resumable**: the cursor captures `(phase, dateRange)` so an interrupted sync resumes at the next phase using the originally-computed window.
158
+ - **Resource allowlist**: `options.resources` filters which phases run. A resource not in the allowlist is skipped entirely, including its API calls.
159
+ - **Rate limits**: Mixpanel's Query API quota is 60 queries/hour per project (default). The connector batches each event/funnel into one query and reuses the result across active-user phases where possible. 429 responses fall through to the shared HTTP client's retry-with-backoff path.
160
+
161
+ ## Schemas
162
+
163
+ `MixpanelConnector.schemas` exposes the Zod schema for each `request()` resource — used by the cloud shape-drift pipeline to populate `connector_baselines` and by the package's property tests.
164
+
165
+ | Resource | Represents |
166
+ | --------------------- | ----------------------------------------------------------------------------------- |
167
+ | `dau` / `wau` / `mau` | `GET /api/2.0/segmentation?type=unique&unit={day,week,month}` per active-user event |
168
+ | `events_per_day` | `GET /api/2.0/segmentation?type={general,unique}&unit=day` per configured event |
169
+ | `funnel_results` | `GET /api/2.0/funnels?funnel_id=…&unit=day` per configured funnel |
170
+ | `retention` | `GET /api/2.0/retention?retention_type=birth&unit=day&born_event=…&event=…` |
171
+
172
+ ## Aggregates
173
+
174
+ No aggregates yet — `count` / `latest` widgets fall back to evaluating against synced storage rows. Mixpanel's Query API doesn't expose a cheaper single-scalar endpoint than the segmentation/funnel queries the connector already runs, so a future `aggregate()` implementation wouldn't materially shorten the round-trip. The metric rows the connector writes are already daily aggregates, which keeps local `count` / `latest` cheap.
175
+
176
+ ## Property tests
177
+
178
+ Resources in this connector have fast-check property tests under `src/property.test.ts` that:
179
+
180
+ 1. Generate N≥50 synthetic API payloads from a Zod schema mirroring the upstream API response.
181
+ 2. Pipe them through `connector.sync()` against an `InMemoryStorage` instance.
182
+ 3. Assert universal invariants (finite timestamps, no `undefined` leaking, no thrown errors) plus per-resource cardinality (one sample per unique date, one funnel sample per `(date, step)`, one retention sample per `(cohort, period)`).
@@ -0,0 +1,177 @@
1
+ import { BaseConnector, ConnectorContext, SyncOptions, StorageHandle, SyncResult, MetricSample } from '@rawdash/core';
2
+ import { z } from 'zod';
3
+
4
+ declare const configFields: z.ZodObject<{
5
+ username: z.ZodString;
6
+ secret: z.ZodObject<{
7
+ $secret: z.ZodString;
8
+ }, z.core.$strip>;
9
+ projectId: z.ZodString;
10
+ region: z.ZodOptional<z.ZodEnum<{
11
+ us: "us";
12
+ eu: "eu";
13
+ }>>;
14
+ events: z.ZodOptional<z.ZodArray<z.ZodString>>;
15
+ funnels: z.ZodOptional<z.ZodArray<z.ZodObject<{
16
+ id: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
17
+ name: z.ZodOptional<z.ZodString>;
18
+ }, z.core.$strip>>>;
19
+ retentionEvent: z.ZodOptional<z.ZodString>;
20
+ activeUserEvent: z.ZodOptional<z.ZodString>;
21
+ lookbackDays: z.ZodOptional<z.ZodNumber>;
22
+ }, z.core.$strip>;
23
+ interface MixpanelFunnelSpec {
24
+ id: string | number;
25
+ name?: string;
26
+ }
27
+ interface MixpanelSettings {
28
+ projectId: string;
29
+ region?: 'us' | 'eu';
30
+ events?: readonly string[];
31
+ funnels?: readonly MixpanelFunnelSpec[];
32
+ retentionEvent?: string;
33
+ activeUserEvent?: string;
34
+ lookbackDays?: number;
35
+ }
36
+ declare const mixpanelCredentials: {
37
+ username: {
38
+ description: string;
39
+ auth: "required";
40
+ };
41
+ secret: {
42
+ description: string;
43
+ auth: "required";
44
+ };
45
+ };
46
+ type MixpanelCredentials = typeof mixpanelCredentials;
47
+ declare const PHASE_ORDER: readonly ["dau", "wau", "mau", "events_per_day", "funnel_results", "retention"];
48
+ type MixpanelPhase = (typeof PHASE_ORDER)[number];
49
+ type MixpanelResource = MixpanelPhase;
50
+ interface MixpanelDateRange {
51
+ from: string;
52
+ to: string;
53
+ }
54
+ declare function getDateRange(options: SyncOptions, lookbackDays: number, now?: number): MixpanelDateRange;
55
+ interface SegmentationResponse {
56
+ legend_size?: number;
57
+ data: {
58
+ series: string[];
59
+ values: Record<string, Record<string, number>>;
60
+ };
61
+ }
62
+ interface FunnelStep {
63
+ step_label?: string;
64
+ goal?: string;
65
+ event?: string;
66
+ count: number;
67
+ overall_conv_ratio?: number;
68
+ step_conv_ratio?: number;
69
+ }
70
+ interface FunnelDateBucket {
71
+ steps: FunnelStep[];
72
+ analysis?: {
73
+ completion?: number;
74
+ starting_amount?: number;
75
+ steps?: number;
76
+ worst?: number;
77
+ };
78
+ }
79
+ interface FunnelResponse {
80
+ meta?: {
81
+ dates?: string[];
82
+ };
83
+ data: Record<string, FunnelDateBucket>;
84
+ }
85
+ interface RetentionCohort {
86
+ first: number;
87
+ counts: number[];
88
+ }
89
+ type RetentionResponse = Record<string, RetentionCohort>;
90
+ declare function buildActiveUserSamples(response: SegmentationResponse, metricName: string, unit: 'day' | 'week' | 'month', event: string): MetricSample[];
91
+ declare function buildEventsPerDaySamples(generalResponse: SegmentationResponse, uniqueResponse: SegmentationResponse, event: string): MetricSample[];
92
+ declare function buildFunnelSamples(response: FunnelResponse, funnel: MixpanelFunnelSpec): MetricSample[];
93
+ declare function buildRetentionSamples(response: RetentionResponse, event: string): MetricSample[];
94
+ declare class MixpanelConnector extends BaseConnector<MixpanelSettings, MixpanelCredentials> {
95
+ static readonly id = "mixpanel";
96
+ static readonly schemas: {
97
+ readonly dau: z.ZodObject<{
98
+ legend_size: z.ZodOptional<z.ZodNumber>;
99
+ data: z.ZodObject<{
100
+ series: z.ZodArray<z.ZodString>;
101
+ values: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodNumber>>;
102
+ }, z.core.$strip>;
103
+ }, z.core.$strip>;
104
+ readonly wau: z.ZodObject<{
105
+ legend_size: z.ZodOptional<z.ZodNumber>;
106
+ data: z.ZodObject<{
107
+ series: z.ZodArray<z.ZodString>;
108
+ values: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodNumber>>;
109
+ }, z.core.$strip>;
110
+ }, z.core.$strip>;
111
+ readonly mau: z.ZodObject<{
112
+ legend_size: z.ZodOptional<z.ZodNumber>;
113
+ data: z.ZodObject<{
114
+ series: z.ZodArray<z.ZodString>;
115
+ values: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodNumber>>;
116
+ }, z.core.$strip>;
117
+ }, z.core.$strip>;
118
+ readonly events_per_day: z.ZodObject<{
119
+ legend_size: z.ZodOptional<z.ZodNumber>;
120
+ data: z.ZodObject<{
121
+ series: z.ZodArray<z.ZodString>;
122
+ values: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodNumber>>;
123
+ }, z.core.$strip>;
124
+ }, z.core.$strip>;
125
+ readonly funnel_results: z.ZodObject<{
126
+ meta: z.ZodOptional<z.ZodObject<{
127
+ dates: z.ZodOptional<z.ZodArray<z.ZodString>>;
128
+ }, z.core.$strip>>;
129
+ data: z.ZodRecord<z.ZodString, z.ZodObject<{
130
+ steps: z.ZodArray<z.ZodObject<{
131
+ step_label: z.ZodOptional<z.ZodString>;
132
+ goal: z.ZodOptional<z.ZodString>;
133
+ event: z.ZodOptional<z.ZodString>;
134
+ count: z.ZodNumber;
135
+ overall_conv_ratio: z.ZodOptional<z.ZodNumber>;
136
+ step_conv_ratio: z.ZodOptional<z.ZodNumber>;
137
+ }, z.core.$strip>>;
138
+ analysis: z.ZodOptional<z.ZodObject<{
139
+ completion: z.ZodOptional<z.ZodNumber>;
140
+ starting_amount: z.ZodOptional<z.ZodNumber>;
141
+ steps: z.ZodOptional<z.ZodNumber>;
142
+ worst: z.ZodOptional<z.ZodNumber>;
143
+ }, z.core.$strip>>;
144
+ }, z.core.$strip>>;
145
+ }, z.core.$strip>;
146
+ readonly retention: z.ZodRecord<z.ZodString, z.ZodObject<{
147
+ first: z.ZodNumber;
148
+ counts: z.ZodArray<z.ZodNumber>;
149
+ }, z.core.$strip>>;
150
+ };
151
+ static create(input: unknown, ctx?: ConnectorContext): MixpanelConnector;
152
+ readonly id = "mixpanel";
153
+ readonly credentials: {
154
+ username: {
155
+ description: string;
156
+ auth: "required";
157
+ };
158
+ secret: {
159
+ description: string;
160
+ auth: "required";
161
+ };
162
+ };
163
+ private get apiBase();
164
+ private authHeaders;
165
+ private buildQuery;
166
+ private getSegmentation;
167
+ private getFunnel;
168
+ private getRetention;
169
+ private resolveActiveUserEvent;
170
+ private runActiveUserPhase;
171
+ private runEventsPerDayPhase;
172
+ private runFunnelPhase;
173
+ private runRetentionPhase;
174
+ sync(options: SyncOptions, storage: StorageHandle, signal?: AbortSignal): Promise<SyncResult>;
175
+ }
176
+
177
+ export { MixpanelConnector, type MixpanelFunnelSpec, type MixpanelPhase, type MixpanelResource, type MixpanelSettings, buildActiveUserSamples, buildEventsPerDaySamples, buildFunnelSamples, buildRetentionSamples, configFields, MixpanelConnector as default, getDateRange };
package/dist/index.js ADDED
@@ -0,0 +1,573 @@
1
+ // ../../connector-shared/dist/index.js
2
+ var HTTP_CLIENT_VERSION = "0.0.0";
3
+ var DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
4
+ function connectorUserAgent(connectorId) {
5
+ return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
6
+ }
7
+
8
+ // src/mixpanel.ts
9
+ import {
10
+ BaseConnector,
11
+ defineConfigFields
12
+ } from "@rawdash/core";
13
+ import { z } from "zod";
14
+ var funnelSpec = z.object({
15
+ id: z.union([z.string().min(1), z.number().int().positive()]).meta({
16
+ label: "Funnel ID",
17
+ description: "Numeric funnel ID (as shown in Mixpanel report URLs)."
18
+ }),
19
+ name: z.string().min(1).optional().meta({
20
+ label: "Funnel display name",
21
+ description: "Optional label attached to each metric sample."
22
+ })
23
+ });
24
+ var configFields = defineConfigFields(
25
+ z.object({
26
+ username: z.string().min(1).meta({
27
+ label: "Service account username",
28
+ description: "Mixpanel service account username (e.g. `rawdash-reader.abcdef.mp-service-account`). Create one at Project settings \u2192 Service Accounts."
29
+ }),
30
+ secret: z.object({ $secret: z.string() }).meta({
31
+ label: "Service account secret",
32
+ description: "Mixpanel service account secret, paired with the username via HTTP Basic auth.",
33
+ secret: true
34
+ }),
35
+ projectId: z.string().trim().regex(/^\d+$/, "projectId must be a Mixpanel numeric project ID").meta({
36
+ label: "Project ID",
37
+ description: "Numeric Mixpanel project ID. Found under Project settings \u2192 Overview.",
38
+ placeholder: "1234567"
39
+ }),
40
+ region: z.enum(["us", "eu"]).optional().meta({
41
+ label: "Data residency region",
42
+ description: "Mixpanel API region. Defaults to `us`."
43
+ }),
44
+ events: z.array(z.string().min(1)).optional().meta({
45
+ label: "Events to track",
46
+ description: "Event names to fetch per-day volume and unique-user counts for. Each event runs one segmentation query per sync per type."
47
+ }),
48
+ funnels: z.array(funnelSpec).optional().meta({
49
+ label: "Funnels",
50
+ description: "Mixpanel funnels to sync per-day conversion data for. Add one entry per funnel ID."
51
+ }),
52
+ retentionEvent: z.string().min(1).optional().meta({
53
+ label: "Retention event",
54
+ description: "Event name to use for cohort retention. When set, the connector runs a single retention query per sync."
55
+ }),
56
+ activeUserEvent: z.string().min(1).optional().meta({
57
+ label: "Active-user event",
58
+ description: "Event name used for DAU/WAU/MAU unique-user counts. Defaults to the first entry in `events` when unset."
59
+ }),
60
+ lookbackDays: z.number().int().positive().optional().meta({
61
+ label: "Backfill window (days)",
62
+ description: "How many days to fetch on a full sync. Defaults to 90.",
63
+ placeholder: "90"
64
+ })
65
+ })
66
+ );
67
+ var mixpanelCredentials = {
68
+ username: {
69
+ description: "Mixpanel service account username",
70
+ auth: "required"
71
+ },
72
+ secret: {
73
+ description: "Mixpanel service account secret",
74
+ auth: "required"
75
+ }
76
+ };
77
+ var DEFAULT_LOOKBACK_DAYS = 90;
78
+ var INCREMENTAL_LOOKBACK_DAYS = 3;
79
+ var MS_PER_DAY = 864e5;
80
+ var PHASE_ORDER = [
81
+ "dau",
82
+ "wau",
83
+ "mau",
84
+ "events_per_day",
85
+ "funnel_results",
86
+ "retention"
87
+ ];
88
+ var METRIC_NAMES = {
89
+ dau: "mixpanel_dau",
90
+ wau: "mixpanel_wau",
91
+ mau: "mixpanel_mau",
92
+ events_per_day: "mixpanel_events_per_day",
93
+ funnel_results: "mixpanel_funnel_results",
94
+ retention: "mixpanel_retention"
95
+ };
96
+ var PHASE_UNIT = {
97
+ dau: "day",
98
+ wau: "week",
99
+ mau: "month"
100
+ };
101
+ var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
102
+ function isDateString(value) {
103
+ return typeof value === "string" && DATE_RE.test(value);
104
+ }
105
+ function isDateRange(value) {
106
+ if (typeof value !== "object" || value === null) {
107
+ return false;
108
+ }
109
+ const v = value;
110
+ return isDateString(v.from) && isDateString(v.to);
111
+ }
112
+ function isMixpanelSyncCursor(value) {
113
+ if (typeof value !== "object" || value === null) {
114
+ return false;
115
+ }
116
+ const v = value;
117
+ if (typeof v.phase !== "string") {
118
+ return false;
119
+ }
120
+ if (!PHASE_ORDER.includes(v.phase)) {
121
+ return false;
122
+ }
123
+ return isDateRange(v.dateRange);
124
+ }
125
+ function pad2(n) {
126
+ return String(n).padStart(2, "0");
127
+ }
128
+ function toMixpanelDate(ms) {
129
+ const d = new Date(ms);
130
+ return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;
131
+ }
132
+ function mixpanelDateToMs(date) {
133
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
134
+ if (!m) {
135
+ return NaN;
136
+ }
137
+ return Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
138
+ }
139
+ function getDateRange(options, lookbackDays, now = Date.now()) {
140
+ const to = toMixpanelDate(now);
141
+ if (options.mode === "latest") {
142
+ return {
143
+ from: toMixpanelDate(now - (INCREMENTAL_LOOKBACK_DAYS - 1) * MS_PER_DAY),
144
+ to
145
+ };
146
+ }
147
+ if (options.since !== void 0) {
148
+ const sinceMs = Date.parse(options.since);
149
+ if (Number.isFinite(sinceMs)) {
150
+ const elapsed = Math.max(1, Math.ceil((now - sinceMs) / MS_PER_DAY));
151
+ const days = Math.min(elapsed, lookbackDays);
152
+ return { from: toMixpanelDate(now - (days - 1) * MS_PER_DAY), to };
153
+ }
154
+ }
155
+ return {
156
+ from: toMixpanelDate(now - (lookbackDays - 1) * MS_PER_DAY),
157
+ to
158
+ };
159
+ }
160
+ var dateString = z.string().regex(DATE_RE);
161
+ var finiteNumber = z.number();
162
+ var segmentationSchema = z.object({
163
+ legend_size: z.number().optional(),
164
+ data: z.object({
165
+ series: z.array(dateString),
166
+ values: z.record(z.string(), z.record(dateString, finiteNumber))
167
+ })
168
+ });
169
+ var funnelStepSchema = z.object({
170
+ step_label: z.string().optional(),
171
+ goal: z.string().optional(),
172
+ event: z.string().optional(),
173
+ count: finiteNumber,
174
+ overall_conv_ratio: finiteNumber.optional(),
175
+ step_conv_ratio: finiteNumber.optional()
176
+ });
177
+ var funnelSchema = z.object({
178
+ meta: z.object({ dates: z.array(dateString).optional() }).optional(),
179
+ data: z.record(
180
+ dateString,
181
+ z.object({
182
+ steps: z.array(funnelStepSchema),
183
+ analysis: z.object({
184
+ completion: finiteNumber.optional(),
185
+ starting_amount: finiteNumber.optional(),
186
+ steps: finiteNumber.optional(),
187
+ worst: finiteNumber.optional()
188
+ }).optional()
189
+ })
190
+ )
191
+ });
192
+ var retentionSchema = z.record(
193
+ dateString,
194
+ z.object({
195
+ first: finiteNumber,
196
+ counts: z.array(finiteNumber)
197
+ })
198
+ );
199
+ function buildActiveUserSamples(response, metricName, unit, event) {
200
+ const samples = [];
201
+ const seriesByEvent = response.data.values;
202
+ const dateTotals = /* @__PURE__ */ new Map();
203
+ for (const eventValues of Object.values(seriesByEvent)) {
204
+ for (const [date, value] of Object.entries(eventValues)) {
205
+ const ts = mixpanelDateToMs(date);
206
+ if (!Number.isFinite(ts)) {
207
+ continue;
208
+ }
209
+ const prior = dateTotals.get(date) ?? 0;
210
+ dateTotals.set(date, prior + value);
211
+ }
212
+ }
213
+ for (const [date, value] of dateTotals) {
214
+ const ts = mixpanelDateToMs(date);
215
+ samples.push({
216
+ name: metricName,
217
+ ts,
218
+ value,
219
+ attributes: { unit, event }
220
+ });
221
+ }
222
+ return samples;
223
+ }
224
+ function buildEventsPerDaySamples(generalResponse, uniqueResponse, event) {
225
+ const samples = [];
226
+ const generalValues = generalResponse.data.values[event] ?? {};
227
+ const uniqueValues = uniqueResponse.data.values[event] ?? {};
228
+ const allDates = /* @__PURE__ */ new Set([
229
+ ...Object.keys(generalValues),
230
+ ...Object.keys(uniqueValues)
231
+ ]);
232
+ for (const date of allDates) {
233
+ const ts = mixpanelDateToMs(date);
234
+ if (!Number.isFinite(ts)) {
235
+ continue;
236
+ }
237
+ const count = generalValues[date] ?? 0;
238
+ const uniqueUsers = uniqueValues[date] ?? 0;
239
+ samples.push({
240
+ name: METRIC_NAMES.events_per_day,
241
+ ts,
242
+ value: count,
243
+ attributes: {
244
+ event,
245
+ count,
246
+ uniqueUsers
247
+ }
248
+ });
249
+ }
250
+ return samples;
251
+ }
252
+ function buildFunnelSamples(response, funnel) {
253
+ const samples = [];
254
+ const funnelIdAttr = typeof funnel.id === "number" ? funnel.id : String(funnel.id);
255
+ for (const [date, bucket] of Object.entries(response.data)) {
256
+ const ts = mixpanelDateToMs(date);
257
+ if (!Number.isFinite(ts)) {
258
+ continue;
259
+ }
260
+ bucket.steps.forEach((step, stepIdx) => {
261
+ const attributes = {
262
+ funnelId: funnelIdAttr,
263
+ step: stepIdx,
264
+ stepLabel: step.step_label ?? step.event ?? `step_${stepIdx}`,
265
+ users: step.count,
266
+ conversionRate: step.overall_conv_ratio ?? null,
267
+ stepConversionRate: step.step_conv_ratio ?? null
268
+ };
269
+ if (funnel.name !== void 0) {
270
+ attributes["funnelName"] = funnel.name;
271
+ }
272
+ samples.push({
273
+ name: METRIC_NAMES.funnel_results,
274
+ ts,
275
+ value: step.count,
276
+ attributes
277
+ });
278
+ });
279
+ }
280
+ return samples;
281
+ }
282
+ function buildRetentionSamples(response, event) {
283
+ const samples = [];
284
+ for (const [cohortDate, cohort] of Object.entries(response)) {
285
+ const ts = mixpanelDateToMs(cohortDate);
286
+ if (!Number.isFinite(ts)) {
287
+ continue;
288
+ }
289
+ cohort.counts.forEach((retained, period) => {
290
+ samples.push({
291
+ name: METRIC_NAMES.retention,
292
+ ts,
293
+ value: retained,
294
+ attributes: {
295
+ event,
296
+ period,
297
+ cohortSize: cohort.first,
298
+ retentionRate: cohort.first > 0 ? retained / cohort.first : 0
299
+ }
300
+ });
301
+ });
302
+ }
303
+ return samples;
304
+ }
305
+ function encodeBasicAuth(username, secret) {
306
+ const raw = `${username}:${secret}`;
307
+ if (typeof btoa === "function") {
308
+ return `Basic ${btoa(raw)}`;
309
+ }
310
+ const bufferCtor = globalThis.Buffer;
311
+ if (bufferCtor) {
312
+ return `Basic ${bufferCtor.from(raw).toString("base64")}`;
313
+ }
314
+ throw new Error("No base64 encoder available in this runtime");
315
+ }
316
+ function regionHost(region) {
317
+ return region === "eu" ? "eu.mixpanel.com" : "mixpanel.com";
318
+ }
319
+ var MixpanelConnector = class _MixpanelConnector extends BaseConnector {
320
+ static id = "mixpanel";
321
+ static schemas = {
322
+ dau: segmentationSchema,
323
+ wau: segmentationSchema,
324
+ mau: segmentationSchema,
325
+ events_per_day: segmentationSchema,
326
+ funnel_results: funnelSchema,
327
+ retention: retentionSchema
328
+ };
329
+ static create(input, ctx) {
330
+ const parsed = configFields.parse(input);
331
+ return new _MixpanelConnector(
332
+ {
333
+ projectId: parsed.projectId,
334
+ region: parsed.region,
335
+ events: parsed.events,
336
+ funnels: parsed.funnels,
337
+ retentionEvent: parsed.retentionEvent,
338
+ activeUserEvent: parsed.activeUserEvent,
339
+ lookbackDays: parsed.lookbackDays
340
+ },
341
+ {
342
+ username: parsed.username,
343
+ secret: parsed.secret
344
+ },
345
+ ctx
346
+ );
347
+ }
348
+ id = "mixpanel";
349
+ credentials = mixpanelCredentials;
350
+ get apiBase() {
351
+ return `https://${regionHost(this.settings.region)}/api/2.0`;
352
+ }
353
+ authHeaders() {
354
+ return {
355
+ Authorization: encodeBasicAuth(this.creds.username, this.creds.secret),
356
+ Accept: "application/json",
357
+ "User-Agent": connectorUserAgent("mixpanel")
358
+ };
359
+ }
360
+ buildQuery(extra) {
361
+ const params = new URLSearchParams({
362
+ project_id: this.settings.projectId,
363
+ ...extra
364
+ });
365
+ return params.toString();
366
+ }
367
+ async getSegmentation(resource, params, signal) {
368
+ const url = `${this.apiBase}/segmentation?${this.buildQuery(params)}`;
369
+ const res = await this.get(url, {
370
+ resource,
371
+ headers: this.authHeaders(),
372
+ signal
373
+ });
374
+ return segmentationSchema.parse(res.body);
375
+ }
376
+ async getFunnel(funnelId, range, signal) {
377
+ const url = `${this.apiBase}/funnels?${this.buildQuery({
378
+ funnel_id: String(funnelId),
379
+ from_date: range.from,
380
+ to_date: range.to,
381
+ unit: "day"
382
+ })}`;
383
+ const res = await this.get(url, {
384
+ resource: "funnel_results",
385
+ headers: this.authHeaders(),
386
+ signal
387
+ });
388
+ return funnelSchema.parse(res.body);
389
+ }
390
+ async getRetention(event, range, signal) {
391
+ const url = `${this.apiBase}/retention?${this.buildQuery({
392
+ from_date: range.from,
393
+ to_date: range.to,
394
+ retention_type: "birth",
395
+ unit: "day",
396
+ born_event: event,
397
+ event
398
+ })}`;
399
+ const res = await this.get(url, {
400
+ resource: "retention",
401
+ headers: this.authHeaders(),
402
+ signal
403
+ });
404
+ return retentionSchema.parse(res.body);
405
+ }
406
+ resolveActiveUserEvent() {
407
+ if (this.settings.activeUserEvent !== void 0) {
408
+ return this.settings.activeUserEvent;
409
+ }
410
+ const first = this.settings.events?.[0];
411
+ return first;
412
+ }
413
+ async runActiveUserPhase(phase, range, storage, signal) {
414
+ const metricName = METRIC_NAMES[phase];
415
+ const event = this.resolveActiveUserEvent();
416
+ if (event === void 0) {
417
+ await storage.metrics([], { names: [metricName] });
418
+ return;
419
+ }
420
+ const response = await this.getSegmentation(
421
+ phase,
422
+ {
423
+ event,
424
+ from_date: range.from,
425
+ to_date: range.to,
426
+ unit: PHASE_UNIT[phase],
427
+ type: "unique"
428
+ },
429
+ signal
430
+ );
431
+ const samples = buildActiveUserSamples(
432
+ response,
433
+ metricName,
434
+ PHASE_UNIT[phase],
435
+ event
436
+ );
437
+ await storage.metrics(samples, { names: [metricName] });
438
+ }
439
+ async runEventsPerDayPhase(range, storage, signal) {
440
+ const metricName = METRIC_NAMES.events_per_day;
441
+ const events = this.settings.events ?? [];
442
+ if (events.length === 0) {
443
+ await storage.metrics([], { names: [metricName] });
444
+ return;
445
+ }
446
+ const samples = [];
447
+ for (const event of events) {
448
+ if (signal?.aborted) {
449
+ throw new Error("aborted");
450
+ }
451
+ const [generalResponse, uniqueResponse] = await Promise.all([
452
+ this.getSegmentation(
453
+ "events_per_day",
454
+ {
455
+ event,
456
+ from_date: range.from,
457
+ to_date: range.to,
458
+ unit: "day",
459
+ type: "general"
460
+ },
461
+ signal
462
+ ),
463
+ this.getSegmentation(
464
+ "events_per_day",
465
+ {
466
+ event,
467
+ from_date: range.from,
468
+ to_date: range.to,
469
+ unit: "day",
470
+ type: "unique"
471
+ },
472
+ signal
473
+ )
474
+ ]);
475
+ samples.push(
476
+ ...buildEventsPerDaySamples(generalResponse, uniqueResponse, event)
477
+ );
478
+ }
479
+ await storage.metrics(samples, { names: [metricName] });
480
+ }
481
+ async runFunnelPhase(range, storage, signal) {
482
+ const metricName = METRIC_NAMES.funnel_results;
483
+ const funnels = this.settings.funnels ?? [];
484
+ if (funnels.length === 0) {
485
+ await storage.metrics([], { names: [metricName] });
486
+ return;
487
+ }
488
+ const samples = [];
489
+ for (const funnel of funnels) {
490
+ if (signal?.aborted) {
491
+ throw new Error("aborted");
492
+ }
493
+ const response = await this.getFunnel(funnel.id, range, signal);
494
+ samples.push(...buildFunnelSamples(response, funnel));
495
+ }
496
+ await storage.metrics(samples, { names: [metricName] });
497
+ }
498
+ async runRetentionPhase(range, storage, signal) {
499
+ const metricName = METRIC_NAMES.retention;
500
+ const event = this.settings.retentionEvent;
501
+ if (event === void 0) {
502
+ await storage.metrics([], { names: [metricName] });
503
+ return;
504
+ }
505
+ const response = await this.getRetention(event, range, signal);
506
+ const samples = buildRetentionSamples(response, event);
507
+ await storage.metrics(samples, { names: [metricName] });
508
+ }
509
+ async sync(options, storage, signal) {
510
+ const lookbackDays = this.settings.lookbackDays ?? DEFAULT_LOOKBACK_DAYS;
511
+ const cursor = isMixpanelSyncCursor(options.cursor) ? options.cursor : void 0;
512
+ const dateRange = cursor?.dateRange ?? getDateRange(options, lookbackDays);
513
+ const resumeIdx = cursor ? PHASE_ORDER.indexOf(cursor.phase) : -1;
514
+ const startIdx = resumeIdx >= 0 ? resumeIdx : 0;
515
+ const requested = options.resources;
516
+ for (let i = startIdx; i < PHASE_ORDER.length; i++) {
517
+ const phase = PHASE_ORDER[i];
518
+ if (signal?.aborted) {
519
+ return { done: false, cursor: { phase, dateRange } };
520
+ }
521
+ if (requested && requested.size > 0 && !requested.has(phase)) {
522
+ continue;
523
+ }
524
+ const phaseStart = Date.now();
525
+ try {
526
+ if (phase === "dau" || phase === "wau" || phase === "mau") {
527
+ await this.runActiveUserPhase(phase, dateRange, storage, signal);
528
+ } else if (phase === "events_per_day") {
529
+ await this.runEventsPerDayPhase(dateRange, storage, signal);
530
+ } else if (phase === "funnel_results") {
531
+ await this.runFunnelPhase(dateRange, storage, signal);
532
+ } else {
533
+ await this.runRetentionPhase(dateRange, storage, signal);
534
+ }
535
+ } catch (err) {
536
+ if (signal?.aborted || err instanceof Error && err.name === "AbortError") {
537
+ return { done: false, cursor: { phase, dateRange } };
538
+ }
539
+ this.logger.warn("fetch page failed", {
540
+ resource: phase,
541
+ page: 1,
542
+ error: err instanceof Error ? err.message : String(err)
543
+ });
544
+ return {
545
+ done: false,
546
+ cursor: { phase, dateRange },
547
+ transientError: err
548
+ };
549
+ }
550
+ this.logger.info("resource done", {
551
+ resource: phase,
552
+ pages: 1,
553
+ items: 0,
554
+ duration_ms: Date.now() - phaseStart
555
+ });
556
+ }
557
+ return { done: true };
558
+ }
559
+ };
560
+
561
+ // src/index.ts
562
+ var index_default = MixpanelConnector;
563
+ export {
564
+ MixpanelConnector,
565
+ buildActiveUserSamples,
566
+ buildEventsPerDaySamples,
567
+ buildFunnelSamples,
568
+ buildRetentionSamples,
569
+ configFields,
570
+ index_default as default,
571
+ getDateRange
572
+ };
573
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../connector-shared/src/errors.ts","../../../connector-shared/src/retry.ts","../../../connector-shared/src/version.ts","../../../connector-shared/src/request.ts","../../../connector-shared/src/rate-limit.ts","../../../connector-shared/src/sanitize.ts","../../../connector-shared/src/epoch.ts","../../../connector-shared/src/pagination.ts","../../../connector-shared/src/logger.ts","../src/mixpanel.ts","../src/index.ts"],"sourcesContent":["import type { HttpResponse } from './types';\n\nexport type HttpErrorKind =\n | 'transient'\n | 'rate_limit'\n | 'auth'\n | 'upstream_bug'\n | 'client_bug';\n\nexport abstract class HttpClientError extends Error {\n abstract readonly kind: HttpErrorKind;\n readonly response?: HttpResponse;\n\n constructor(message: string, response?: HttpResponse) {\n super(message);\n this.name = new.target.name;\n this.response = response;\n }\n}\n\nexport class TransientError extends HttpClientError {\n readonly kind = 'transient' as const;\n}\n\nexport class RateLimitError extends HttpClientError {\n readonly kind = 'rate_limit' as const;\n readonly retryAfter?: Date;\n\n constructor(message: string, response?: HttpResponse, retryAfter?: Date) {\n super(message, response);\n this.retryAfter = retryAfter;\n }\n}\n\nexport class AuthError extends HttpClientError {\n readonly kind = 'auth' as const;\n}\n\nexport class UpstreamBugError extends HttpClientError {\n readonly kind = 'upstream_bug' as const;\n}\n\nexport class ClientBugError extends HttpClientError {\n readonly kind = 'client_bug' as const;\n}\n\nexport function classifyStatus(status: number): HttpErrorKind {\n if (status === 429) {\n return 'rate_limit';\n }\n if (status === 401 || status === 403) {\n return 'auth';\n }\n if (status === 408) {\n return 'transient';\n }\n if (status >= 500) {\n return 'upstream_bug';\n }\n if (status >= 400) {\n return 'client_bug';\n }\n return 'client_bug';\n}\n\nexport function errorForStatus(\n message: string,\n response: HttpResponse,\n retryAfter?: Date,\n): HttpClientError {\n const kind = classifyStatus(response.status);\n switch (kind) {\n case 'rate_limit':\n return new RateLimitError(message, response, retryAfter);\n case 'auth':\n return new AuthError(message, response);\n case 'transient':\n return new TransientError(message, response);\n case 'upstream_bug':\n return new UpstreamBugError(message, response);\n case 'client_bug':\n return new ClientBugError(message, response);\n }\n}\n","import { HttpClientError, RateLimitError, TransientError } from './errors';\n\nexport interface RetryPolicy {\n maxAttempts?: number;\n initialDelayMs?: number;\n maxDelayMs?: number;\n retryOn?: (status: number | null, err?: Error) => boolean;\n}\n\nexport const defaultRetryOn = (status: number | null, err?: Error): boolean => {\n if (err instanceof RateLimitError) {\n return true;\n }\n if (err instanceof TransientError) {\n return true;\n }\n if (status === null) {\n return err instanceof Error && !(err instanceof HttpClientError);\n }\n if (status === 408 || status === 429) {\n return true;\n }\n if (status >= 500) {\n return true;\n }\n return false;\n};\n\nexport function backoffDelayMs(\n attempt: number,\n policy: Required<Pick<RetryPolicy, 'initialDelayMs' | 'maxDelayMs'>>,\n): number {\n const base = policy.initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, policy.maxDelayMs);\n}\n\nexport function parseRetryAfter(\n headerValue: string | null,\n now: Date = new Date(),\n): Date | undefined {\n if (!headerValue) {\n return undefined;\n }\n const trimmed = headerValue.trim();\n if (/^\\d+$/.test(trimmed)) {\n return new Date(now.getTime() + Number(trimmed) * 1000);\n }\n const parsed = Date.parse(trimmed);\n if (Number.isNaN(parsed)) {\n return undefined;\n }\n return new Date(parsed);\n}\n\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (signal?.aborted) {\n return Promise.reject(signal.reason ?? new Error('Aborted'));\n }\n return new Promise<void>((resolve, reject) => {\n const onAbort = () => {\n clearTimeout(timer);\n reject(signal!.reason ?? new Error('Aborted'));\n };\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n","export const HTTP_CLIENT_VERSION = '0.0.0';\n\nexport const DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n\nexport function connectorUserAgent(connectorId: string): string {\n return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n}\n","import {\n AuthError,\n ClientBugError,\n HttpClientError,\n RateLimitError,\n TransientError,\n UpstreamBugError,\n errorForStatus,\n} from './errors';\nimport { defaultRetryOn, parseRetryAfter, sleep } from './retry';\nimport type { FetchLike, HttpMethod, HttpRequest, HttpResponse } from './types';\nimport { DEFAULT_USER_AGENT } from './version';\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst DEFAULT_MAX_ATTEMPTS = 3;\nconst DEFAULT_INITIAL_DELAY_MS = 1000;\nconst DEFAULT_MAX_DELAY_MS = 60_000;\nconst OBSERVER_TIMEOUT_MS = 250;\n\nexport interface RequestObservation {\n url: string;\n method: HttpMethod;\n status: number;\n resource: string;\n requestId: string;\n body: unknown;\n}\n\nexport type RequestObserver = (\n event: RequestObservation,\n) => void | Promise<void>;\n\nexport interface RequestOptions {\n fetch?: FetchLike;\n observer?: RequestObserver;\n resource: string;\n requestId?: string;\n}\n\nasync function notifyObserver(\n observer: RequestObserver,\n event: RequestObservation,\n): Promise<void> {\n let result: void | Promise<void>;\n try {\n result = observer(event);\n } catch (err) {\n console.warn('[connector-shared] request observer threw:', err);\n return;\n }\n if (!(result instanceof Promise)) {\n return;\n }\n const guarded = result.catch((err) => {\n console.warn('[connector-shared] request observer rejected:', err);\n });\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, OBSERVER_TIMEOUT_MS);\n });\n try {\n await Promise.race([guarded, timeout]);\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n }\n}\n\nfunction newRequestId(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) {\n return c.randomUUID();\n }\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction mergeHeaders(\n defaults: Record<string, string>,\n overrides: Record<string, string> | undefined,\n): Record<string, string> {\n const merged: Record<string, string> = {};\n for (const [k, v] of Object.entries(defaults)) {\n merged[k.toLowerCase()] = v;\n }\n if (overrides) {\n for (const [k, v] of Object.entries(overrides)) {\n merged[k.toLowerCase()] = v;\n }\n }\n return merged;\n}\n\nfunction linkTimeoutSignal(\n parent: AbortSignal | undefined,\n timeoutMs: number,\n): { signal: AbortSignal; cancel: () => void } {\n const controller = new AbortController();\n const onParentAbort = () => {\n controller.abort(parent?.reason);\n };\n if (parent) {\n if (parent.aborted) {\n controller.abort(parent.reason);\n } else {\n parent.addEventListener('abort', onParentAbort, { once: true });\n }\n }\n const timer = setTimeout(() => {\n controller.abort(new Error(`Request timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n return {\n signal: controller.signal,\n cancel: () => {\n clearTimeout(timer);\n if (parent) {\n parent.removeEventListener('abort', onParentAbort);\n }\n },\n };\n}\n\nasync function readBody(res: Response, parseJson: boolean): Promise<unknown> {\n if (res.status === 204 || res.status === 205) {\n return null;\n }\n const contentType = res.headers.get('content-type') ?? '';\n if (parseJson && contentType.includes('application/json')) {\n const text = await res.text();\n if (text.length === 0) {\n return null;\n }\n return JSON.parse(text);\n }\n return res.text();\n}\n\nexport async function request<T = unknown>(\n req: HttpRequest,\n options: RequestOptions,\n): Promise<HttpResponse<T>> {\n const fetchImpl: FetchLike = options.fetch ?? (globalThis.fetch as FetchLike);\n const retry = req.retry ?? {};\n const maxAttempts = retry.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;\n const initialDelayMs = retry.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;\n const maxDelayMs = retry.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;\n const retryOn = retry.retryOn ?? defaultRetryOn;\n const timeoutMs = req.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const parseJson = req.parseJson ?? true;\n\n const headers = mergeHeaders(\n {\n 'User-Agent': DEFAULT_USER_AGENT,\n Accept: 'application/json',\n },\n req.headers,\n );\n\n let lastErr: Error | undefined;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n req.signal?.throwIfAborted();\n\n const { signal, cancel } = linkTimeoutSignal(req.signal, timeoutMs);\n let res: Response;\n try {\n res = await fetchImpl(req.url, {\n method: req.method ?? 'GET',\n headers,\n body: req.body as RequestInit['body'],\n signal,\n });\n } catch (err) {\n cancel();\n if (req.signal?.aborted) {\n throw req.signal.reason ?? err;\n }\n const error = err instanceof Error ? err : new Error(String(err));\n lastErr = error;\n if (attempt < maxAttempts - 1 && retryOn(null, error)) {\n const delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n await sleep(delay, req.signal);\n continue;\n }\n throw new TransientError(error.message);\n }\n cancel();\n\n const body = await readBody(res, parseJson);\n const httpResponse: HttpResponse<T> = {\n status: res.status,\n headers: res.headers,\n body: body as T,\n };\n if (req.rateLimit) {\n const state = req.rateLimit.parse(res.headers);\n if (state) {\n httpResponse.rateLimitState = state;\n }\n }\n\n if (options.observer) {\n await notifyObserver(options.observer, {\n url: req.url,\n method: req.method ?? 'GET',\n status: res.status,\n resource: options.resource,\n requestId: options.requestId ?? newRequestId(),\n body,\n });\n }\n\n if (res.ok) {\n return httpResponse;\n }\n\n const retryAfter = parseRetryAfter(res.headers.get('retry-after'));\n const message = `HTTP ${res.status} ${res.statusText} for ${req.method ?? 'GET'} ${req.url}`;\n const err = errorForStatus(message, httpResponse, retryAfter);\n\n if (\n attempt < maxAttempts - 1 &&\n retryOn(res.status, err) &&\n !(err instanceof AuthError) &&\n !(err instanceof ClientBugError)\n ) {\n lastErr = err;\n let delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n if (err instanceof RateLimitError && retryAfter) {\n const wait = retryAfter.getTime() - Date.now();\n if (wait > 0) {\n delay = Math.min(wait, maxDelayMs);\n }\n }\n await sleep(delay, req.signal);\n continue;\n }\n\n throw err;\n }\n\n throw lastErr ?? new UpstreamBugError('Exhausted retry attempts');\n}\n\nfunction computeDelay(\n attempt: number,\n initialDelayMs: number,\n maxDelayMs: number,\n): number {\n const base = initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, maxDelayMs);\n}\n\nexport { HttpClientError };\n","export interface RateLimitState {\n remaining: number;\n resetAt: Date;\n}\n\nexport interface RateLimitPolicy {\n parse(headers: Headers): RateLimitState | null;\n}\n\nexport interface StandardRateLimitPolicyConfig {\n remainingHeader: string;\n resetHeader: string;\n resetUnit: 's' | 'ms';\n resetFallbackMs?: number;\n}\n\nexport function standardRateLimitPolicy(\n config: StandardRateLimitPolicyConfig,\n): RateLimitPolicy {\n const { remainingHeader, resetHeader, resetUnit, resetFallbackMs } = config;\n const multiplier = resetUnit === 's' ? 1000 : 1;\n return {\n parse(h) {\n const remainingRaw = h.get(remainingHeader);\n if (remainingRaw === null || remainingRaw.trim() === '') {\n return null;\n }\n const remaining = Number(remainingRaw);\n if (!Number.isFinite(remaining)) {\n return null;\n }\n const resetRaw = h.get(resetHeader);\n if (resetRaw === null) {\n if (resetFallbackMs === undefined) {\n return null;\n }\n return {\n remaining,\n resetAt: new Date(Date.now() + resetFallbackMs),\n };\n }\n if (resetRaw.trim() === '') {\n return null;\n }\n const reset = Number(resetRaw);\n if (!Number.isFinite(reset) || reset < 0) {\n return null;\n }\n const resetMs = reset * multiplier;\n if (!Number.isFinite(resetMs)) {\n return null;\n }\n return { remaining, resetAt: new Date(resetMs) };\n },\n };\n}\n","export interface SanitizeAllowedUrlOptions {\n url: string | null;\n host: string;\n pathname: string;\n protocol?: 'https:' | 'http:';\n}\n\nexport function sanitizeAllowedUrl(\n options: SanitizeAllowedUrlOptions,\n): string | null {\n const { url, host, pathname, protocol = 'https:' } = options;\n if (url === null) {\n return null;\n }\n try {\n const u = new URL(url);\n if (u.protocol !== protocol || u.host !== host || u.pathname !== pathname) {\n return null;\n }\n return u.toString();\n } catch {\n return null;\n }\n}\n","export type EpochUnit = 'ms' | 's' | 'iso';\n\nexport function parseEpoch(\n value: number | string | null | undefined,\n unit: EpochUnit,\n): number | null {\n if (value === null || value === undefined) {\n return null;\n }\n if (unit === 'iso') {\n if (typeof value !== 'string') {\n return null;\n }\n const ms = new Date(value).getTime();\n return Number.isFinite(ms) ? ms : null;\n }\n if (typeof value === 'string' && value.trim() === '') {\n return null;\n }\n const n = typeof value === 'number' ? value : Number(value);\n if (!Number.isFinite(n)) {\n return null;\n }\n const result = unit === 's' ? n * 1000 : n;\n return Number.isFinite(result) ? result : null;\n}\n","import { request } from './request';\nimport type { HttpRequest } from './types';\n\nexport function parseLinkHeader(header: string | null): Record<string, string> {\n if (!header) {\n return {};\n }\n const result: Record<string, string> = {};\n for (const part of header.split(',')) {\n const match = part.match(/<([^>]+)>\\s*;\\s*rel=\"([^\"]+)\"/);\n if (match) {\n result[match[2]!] = match[1]!;\n }\n }\n return result;\n}\n\nexport async function* paginateLink<T>(\n initial: HttpRequest,\n parse: (body: unknown) => T[],\n options: { resource: string },\n): AsyncIterable<T> {\n let next: string | null = initial.url;\n while (next) {\n const res: Awaited<ReturnType<typeof request>> = await request(\n {\n ...initial,\n url: next,\n },\n { resource: options.resource },\n );\n for (const item of parse(res.body)) {\n yield item;\n }\n const links = parseLinkHeader(res.headers.get('link'));\n next = links['next'] ?? null;\n }\n}\n\nexport async function* paginateCursor<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; nextCursor: string | null },\n buildNext: (req: HttpRequest, cursor: string) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let req: HttpRequest = initial;\n while (true) {\n const res = await request(req, { resource: options.resource });\n const { items, nextCursor } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!nextCursor) {\n return;\n }\n req = buildNext(req, nextCursor);\n }\n}\n\nexport async function* paginatePage<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; hasMore: boolean },\n buildPage: (req: HttpRequest, page: number) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let page = 1;\n while (true) {\n const req = page === 1 ? initial : buildPage(initial, page);\n const res = await request(req, { resource: options.resource });\n const { items, hasMore } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!hasMore || items.length === 0) {\n return;\n }\n page++;\n }\n}\n","export type LogFields = Record<string, unknown>;\n\nexport interface ConnectorLogger {\n info(event: string, fields?: LogFields): void;\n warn(event: string, fields?: LogFields): void;\n}\n\nexport interface ConnectorLoggerOptions {\n scope: string;\n}\n\nconst MAX_VALUE_LEN = 120;\n\nfunction truncate(s: string, max = MAX_VALUE_LEN): string {\n if (s.length <= max) {\n return s;\n }\n return `${s.slice(0, max - 1)}…`;\n}\n\nfunction formatValue(value: unknown): string {\n if (value === null) {\n return 'null';\n }\n if (value === undefined) {\n return '';\n }\n if (typeof value === 'number' || typeof value === 'boolean') {\n return String(value);\n }\n if (typeof value === 'string') {\n const t = truncate(value);\n if (/[\\s\"=]/.test(t)) {\n return JSON.stringify(t);\n }\n return t;\n }\n if (typeof value === 'bigint') {\n return value.toString();\n }\n let json: string | undefined;\n try {\n json = JSON.stringify(value);\n } catch {\n json = undefined;\n }\n return truncate(json ?? String(value));\n}\n\nexport function formatLogFields(fields?: LogFields): string {\n if (!fields) {\n return '';\n }\n const parts: string[] = [];\n for (const [k, v] of Object.entries(fields)) {\n if (v === undefined) {\n continue;\n }\n parts.push(`${k}=${formatValue(v)}`);\n }\n return parts.length > 0 ? ` ${parts.join(' ')}` : '';\n}\n\nexport function formatLogLine(\n scope: string,\n event: string,\n fields?: LogFields,\n): string {\n return `[${scope}] ${event}${formatLogFields(fields)}`;\n}\n\nexport function createDefaultConnectorLogger(\n opts: ConnectorLoggerOptions,\n): ConnectorLogger {\n return {\n info(event, fields) {\n console.info(formatLogLine(opts.scope, event, fields));\n },\n warn(event, fields) {\n console.warn(formatLogLine(opts.scope, event, fields));\n },\n };\n}\n\nconst NOOP_LOGGER: ConnectorLogger = {\n info() {},\n warn() {},\n};\n\nexport function noopConnectorLogger(): ConnectorLogger {\n return NOOP_LOGGER;\n}\n","import { connectorUserAgent } from '@rawdash/connector-shared';\nimport {\n BaseConnector,\n type ConnectorContext,\n type CredentialsSchema,\n type JSONValue,\n type MetricSample,\n type StorageHandle,\n type SyncOptions,\n type SyncResult,\n defineConfigFields,\n} from '@rawdash/core';\nimport { z } from 'zod';\n\n// ---------------------------------------------------------------------------\n// Config\n// ---------------------------------------------------------------------------\n\nconst funnelSpec = z.object({\n id: z.union([z.string().min(1), z.number().int().positive()]).meta({\n label: 'Funnel ID',\n description: 'Numeric funnel ID (as shown in Mixpanel report URLs).',\n }),\n name: z.string().min(1).optional().meta({\n label: 'Funnel display name',\n description: 'Optional label attached to each metric sample.',\n }),\n});\n\nexport const configFields = defineConfigFields(\n z.object({\n username: z.string().min(1).meta({\n label: 'Service account username',\n description:\n 'Mixpanel service account username (e.g. `rawdash-reader.abcdef.mp-service-account`). Create one at Project settings → Service Accounts.',\n }),\n secret: z.object({ $secret: z.string() }).meta({\n label: 'Service account secret',\n description:\n 'Mixpanel service account secret, paired with the username via HTTP Basic auth.',\n secret: true,\n }),\n projectId: z\n .string()\n .trim()\n .regex(/^\\d+$/, 'projectId must be a Mixpanel numeric project ID')\n .meta({\n label: 'Project ID',\n description:\n 'Numeric Mixpanel project ID. Found under Project settings → Overview.',\n placeholder: '1234567',\n }),\n region: z.enum(['us', 'eu']).optional().meta({\n label: 'Data residency region',\n description: 'Mixpanel API region. Defaults to `us`.',\n }),\n events: z.array(z.string().min(1)).optional().meta({\n label: 'Events to track',\n description:\n 'Event names to fetch per-day volume and unique-user counts for. Each event runs one segmentation query per sync per type.',\n }),\n funnels: z.array(funnelSpec).optional().meta({\n label: 'Funnels',\n description:\n 'Mixpanel funnels to sync per-day conversion data for. Add one entry per funnel ID.',\n }),\n retentionEvent: z.string().min(1).optional().meta({\n label: 'Retention event',\n description:\n 'Event name to use for cohort retention. When set, the connector runs a single retention query per sync.',\n }),\n activeUserEvent: z.string().min(1).optional().meta({\n label: 'Active-user event',\n description:\n 'Event name used for DAU/WAU/MAU unique-user counts. Defaults to the first entry in `events` when unset.',\n }),\n lookbackDays: z.number().int().positive().optional().meta({\n label: 'Backfill window (days)',\n description: 'How many days to fetch on a full sync. Defaults to 90.',\n placeholder: '90',\n }),\n }),\n);\n\n// ---------------------------------------------------------------------------\n// Settings / credentials\n// ---------------------------------------------------------------------------\n\nexport interface MixpanelFunnelSpec {\n id: string | number;\n name?: string;\n}\n\nexport interface MixpanelSettings {\n projectId: string;\n region?: 'us' | 'eu';\n events?: readonly string[];\n funnels?: readonly MixpanelFunnelSpec[];\n retentionEvent?: string;\n activeUserEvent?: string;\n lookbackDays?: number;\n}\n\nconst mixpanelCredentials = {\n username: {\n description: 'Mixpanel service account username',\n auth: 'required' as const,\n },\n secret: {\n description: 'Mixpanel service account secret',\n auth: 'required' as const,\n },\n} satisfies CredentialsSchema;\n\ntype MixpanelCredentials = typeof mixpanelCredentials;\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_LOOKBACK_DAYS = 90;\nconst INCREMENTAL_LOOKBACK_DAYS = 3;\nconst MS_PER_DAY = 86_400_000;\n\nconst PHASE_ORDER = [\n 'dau',\n 'wau',\n 'mau',\n 'events_per_day',\n 'funnel_results',\n 'retention',\n] as const;\n\nexport type MixpanelPhase = (typeof PHASE_ORDER)[number];\nexport type MixpanelResource = MixpanelPhase;\n\nconst METRIC_NAMES: Record<MixpanelPhase, string> = {\n dau: 'mixpanel_dau',\n wau: 'mixpanel_wau',\n mau: 'mixpanel_mau',\n events_per_day: 'mixpanel_events_per_day',\n funnel_results: 'mixpanel_funnel_results',\n retention: 'mixpanel_retention',\n};\n\nconst PHASE_UNIT: Record<'dau' | 'wau' | 'mau', 'day' | 'week' | 'month'> = {\n dau: 'day',\n wau: 'week',\n mau: 'month',\n};\n\n// ---------------------------------------------------------------------------\n// Cursor + helpers\n// ---------------------------------------------------------------------------\n\ninterface MixpanelDateRange {\n from: string;\n to: string;\n}\n\ninterface MixpanelSyncCursor {\n phase: MixpanelPhase;\n dateRange: MixpanelDateRange;\n}\n\nconst DATE_RE = /^\\d{4}-\\d{2}-\\d{2}$/;\n\nfunction isDateString(value: unknown): value is string {\n return typeof value === 'string' && DATE_RE.test(value);\n}\n\nfunction isDateRange(value: unknown): value is MixpanelDateRange {\n if (typeof value !== 'object' || value === null) {\n return false;\n }\n const v = value as { from?: unknown; to?: unknown };\n return isDateString(v.from) && isDateString(v.to);\n}\n\nfunction isMixpanelSyncCursor(value: unknown): value is MixpanelSyncCursor {\n if (typeof value !== 'object' || value === null) {\n return false;\n }\n const v = value as { phase?: unknown; dateRange?: unknown };\n if (typeof v.phase !== 'string') {\n return false;\n }\n if (!(PHASE_ORDER as readonly string[]).includes(v.phase)) {\n return false;\n }\n return isDateRange(v.dateRange);\n}\n\nfunction pad2(n: number): string {\n return String(n).padStart(2, '0');\n}\n\nfunction toMixpanelDate(ms: number): string {\n const d = new Date(ms);\n return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;\n}\n\nfunction mixpanelDateToMs(date: string): number {\n const m = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(date);\n if (!m) {\n return NaN;\n }\n return Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]));\n}\n\nexport function getDateRange(\n options: SyncOptions,\n lookbackDays: number,\n now: number = Date.now(),\n): MixpanelDateRange {\n const to = toMixpanelDate(now);\n if (options.mode === 'latest') {\n return {\n from: toMixpanelDate(now - (INCREMENTAL_LOOKBACK_DAYS - 1) * MS_PER_DAY),\n to,\n };\n }\n if (options.since !== undefined) {\n const sinceMs = Date.parse(options.since);\n if (Number.isFinite(sinceMs)) {\n const elapsed = Math.max(1, Math.ceil((now - sinceMs) / MS_PER_DAY));\n const days = Math.min(elapsed, lookbackDays);\n return { from: toMixpanelDate(now - (days - 1) * MS_PER_DAY), to };\n }\n }\n return {\n from: toMixpanelDate(now - (lookbackDays - 1) * MS_PER_DAY),\n to,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Mixpanel response shapes (permissive — wire format is JSON, validated by\n// the per-resource Zod schemas below before sample extraction)\n// ---------------------------------------------------------------------------\n\nexport interface SegmentationResponse {\n legend_size?: number;\n data: {\n series: string[];\n values: Record<string, Record<string, number>>;\n };\n}\n\nexport interface FunnelStep {\n step_label?: string;\n goal?: string;\n event?: string;\n count: number;\n overall_conv_ratio?: number;\n step_conv_ratio?: number;\n}\n\nexport interface FunnelDateBucket {\n steps: FunnelStep[];\n analysis?: {\n completion?: number;\n starting_amount?: number;\n steps?: number;\n worst?: number;\n };\n}\n\nexport interface FunnelResponse {\n meta?: { dates?: string[] };\n data: Record<string, FunnelDateBucket>;\n}\n\nexport interface RetentionCohort {\n first: number;\n counts: number[];\n}\n\nexport type RetentionResponse = Record<string, RetentionCohort>;\n\n// ---------------------------------------------------------------------------\n// Zod schemas — describe each resource's wire shape\n// ---------------------------------------------------------------------------\n\nconst dateString = z.string().regex(DATE_RE);\nconst finiteNumber = z.number();\n\nconst segmentationSchema = z.object({\n legend_size: z.number().optional(),\n data: z.object({\n series: z.array(dateString),\n values: z.record(z.string(), z.record(dateString, finiteNumber)),\n }),\n});\n\nconst funnelStepSchema = z.object({\n step_label: z.string().optional(),\n goal: z.string().optional(),\n event: z.string().optional(),\n count: finiteNumber,\n overall_conv_ratio: finiteNumber.optional(),\n step_conv_ratio: finiteNumber.optional(),\n});\n\nconst funnelSchema = z.object({\n meta: z.object({ dates: z.array(dateString).optional() }).optional(),\n data: z.record(\n dateString,\n z.object({\n steps: z.array(funnelStepSchema),\n analysis: z\n .object({\n completion: finiteNumber.optional(),\n starting_amount: finiteNumber.optional(),\n steps: finiteNumber.optional(),\n worst: finiteNumber.optional(),\n })\n .optional(),\n }),\n ),\n});\n\nconst retentionSchema = z.record(\n dateString,\n z.object({\n first: finiteNumber,\n counts: z.array(finiteNumber),\n }),\n);\n\n// ---------------------------------------------------------------------------\n// Sample extraction (pure, exported for unit tests)\n// ---------------------------------------------------------------------------\n\nexport function buildActiveUserSamples(\n response: SegmentationResponse,\n metricName: string,\n unit: 'day' | 'week' | 'month',\n event: string,\n): MetricSample[] {\n const samples: MetricSample[] = [];\n const seriesByEvent = response.data.values;\n // Mixpanel returns values keyed by the queried event name; merge across keys\n // so a single sample per date is emitted regardless of how many event keys\n // appear in the response.\n const dateTotals = new Map<string, number>();\n for (const eventValues of Object.values(seriesByEvent)) {\n for (const [date, value] of Object.entries(eventValues)) {\n const ts = mixpanelDateToMs(date);\n if (!Number.isFinite(ts)) {\n continue;\n }\n const prior = dateTotals.get(date) ?? 0;\n dateTotals.set(date, prior + value);\n }\n }\n for (const [date, value] of dateTotals) {\n const ts = mixpanelDateToMs(date);\n samples.push({\n name: metricName,\n ts,\n value,\n attributes: { unit, event },\n });\n }\n return samples;\n}\n\nexport function buildEventsPerDaySamples(\n generalResponse: SegmentationResponse,\n uniqueResponse: SegmentationResponse,\n event: string,\n): MetricSample[] {\n const samples: MetricSample[] = [];\n const generalValues = generalResponse.data.values[event] ?? {};\n const uniqueValues = uniqueResponse.data.values[event] ?? {};\n const allDates = new Set<string>([\n ...Object.keys(generalValues),\n ...Object.keys(uniqueValues),\n ]);\n for (const date of allDates) {\n const ts = mixpanelDateToMs(date);\n if (!Number.isFinite(ts)) {\n continue;\n }\n const count = generalValues[date] ?? 0;\n const uniqueUsers = uniqueValues[date] ?? 0;\n samples.push({\n name: METRIC_NAMES.events_per_day,\n ts,\n value: count,\n attributes: {\n event,\n count,\n uniqueUsers,\n },\n });\n }\n return samples;\n}\n\nexport function buildFunnelSamples(\n response: FunnelResponse,\n funnel: MixpanelFunnelSpec,\n): MetricSample[] {\n const samples: MetricSample[] = [];\n const funnelIdAttr: JSONValue =\n typeof funnel.id === 'number' ? funnel.id : String(funnel.id);\n for (const [date, bucket] of Object.entries(response.data)) {\n const ts = mixpanelDateToMs(date);\n if (!Number.isFinite(ts)) {\n continue;\n }\n bucket.steps.forEach((step, stepIdx) => {\n const attributes: Record<string, JSONValue> = {\n funnelId: funnelIdAttr,\n step: stepIdx,\n stepLabel: step.step_label ?? step.event ?? `step_${stepIdx}`,\n users: step.count,\n conversionRate: step.overall_conv_ratio ?? null,\n stepConversionRate: step.step_conv_ratio ?? null,\n };\n if (funnel.name !== undefined) {\n attributes['funnelName'] = funnel.name;\n }\n samples.push({\n name: METRIC_NAMES.funnel_results,\n ts,\n value: step.count,\n attributes,\n });\n });\n }\n return samples;\n}\n\nexport function buildRetentionSamples(\n response: RetentionResponse,\n event: string,\n): MetricSample[] {\n const samples: MetricSample[] = [];\n for (const [cohortDate, cohort] of Object.entries(response)) {\n const ts = mixpanelDateToMs(cohortDate);\n if (!Number.isFinite(ts)) {\n continue;\n }\n cohort.counts.forEach((retained, period) => {\n samples.push({\n name: METRIC_NAMES.retention,\n ts,\n value: retained,\n attributes: {\n event,\n period,\n cohortSize: cohort.first,\n retentionRate: cohort.first > 0 ? retained / cohort.first : 0,\n },\n });\n });\n }\n return samples;\n}\n\n// ---------------------------------------------------------------------------\n// Auth helper\n// ---------------------------------------------------------------------------\n\nfunction encodeBasicAuth(username: string, secret: string): string {\n const raw = `${username}:${secret}`;\n if (typeof btoa === 'function') {\n return `Basic ${btoa(raw)}`;\n }\n const bufferCtor = (\n globalThis as {\n Buffer?: { from: (s: string) => { toString: (enc: string) => string } };\n }\n ).Buffer;\n if (bufferCtor) {\n return `Basic ${bufferCtor.from(raw).toString('base64')}`;\n }\n throw new Error('No base64 encoder available in this runtime');\n}\n\nfunction regionHost(region: 'us' | 'eu' | undefined): string {\n return region === 'eu' ? 'eu.mixpanel.com' : 'mixpanel.com';\n}\n\n// ---------------------------------------------------------------------------\n// MixpanelConnector\n// ---------------------------------------------------------------------------\n\nexport class MixpanelConnector extends BaseConnector<\n MixpanelSettings,\n MixpanelCredentials\n> {\n static readonly id = 'mixpanel';\n\n static readonly schemas = {\n dau: segmentationSchema,\n wau: segmentationSchema,\n mau: segmentationSchema,\n events_per_day: segmentationSchema,\n funnel_results: funnelSchema,\n retention: retentionSchema,\n } as const;\n\n static create(input: unknown, ctx?: ConnectorContext): MixpanelConnector {\n const parsed = configFields.parse(input);\n return new MixpanelConnector(\n {\n projectId: parsed.projectId,\n region: parsed.region,\n events: parsed.events,\n funnels: parsed.funnels,\n retentionEvent: parsed.retentionEvent,\n activeUserEvent: parsed.activeUserEvent,\n lookbackDays: parsed.lookbackDays,\n },\n {\n username: parsed.username,\n secret: parsed.secret,\n },\n ctx,\n );\n }\n\n readonly id = 'mixpanel';\n override readonly credentials = mixpanelCredentials;\n\n private get apiBase(): string {\n return `https://${regionHost(this.settings.region)}/api/2.0`;\n }\n\n private authHeaders(): Record<string, string> {\n return {\n Authorization: encodeBasicAuth(this.creds.username, this.creds.secret),\n Accept: 'application/json',\n 'User-Agent': connectorUserAgent('mixpanel'),\n };\n }\n\n private buildQuery(extra: Record<string, string>): string {\n const params = new URLSearchParams({\n project_id: this.settings.projectId,\n ...extra,\n });\n return params.toString();\n }\n\n private async getSegmentation(\n resource: MixpanelPhase,\n params: Record<string, string>,\n signal: AbortSignal | undefined,\n ): Promise<SegmentationResponse> {\n const url = `${this.apiBase}/segmentation?${this.buildQuery(params)}`;\n const res = await this.get<unknown>(url, {\n resource,\n headers: this.authHeaders(),\n signal,\n });\n return segmentationSchema.parse(res.body);\n }\n\n private async getFunnel(\n funnelId: string | number,\n range: MixpanelDateRange,\n signal: AbortSignal | undefined,\n ): Promise<FunnelResponse> {\n const url = `${this.apiBase}/funnels?${this.buildQuery({\n funnel_id: String(funnelId),\n from_date: range.from,\n to_date: range.to,\n unit: 'day',\n })}`;\n const res = await this.get<unknown>(url, {\n resource: 'funnel_results',\n headers: this.authHeaders(),\n signal,\n });\n return funnelSchema.parse(res.body);\n }\n\n private async getRetention(\n event: string,\n range: MixpanelDateRange,\n signal: AbortSignal | undefined,\n ): Promise<RetentionResponse> {\n const url = `${this.apiBase}/retention?${this.buildQuery({\n from_date: range.from,\n to_date: range.to,\n retention_type: 'birth',\n unit: 'day',\n born_event: event,\n event,\n })}`;\n const res = await this.get<unknown>(url, {\n resource: 'retention',\n headers: this.authHeaders(),\n signal,\n });\n return retentionSchema.parse(res.body);\n }\n\n private resolveActiveUserEvent(): string | undefined {\n if (this.settings.activeUserEvent !== undefined) {\n return this.settings.activeUserEvent;\n }\n const first = this.settings.events?.[0];\n return first;\n }\n\n private async runActiveUserPhase(\n phase: 'dau' | 'wau' | 'mau',\n range: MixpanelDateRange,\n storage: StorageHandle,\n signal: AbortSignal | undefined,\n ): Promise<void> {\n const metricName = METRIC_NAMES[phase];\n const event = this.resolveActiveUserEvent();\n if (event === undefined) {\n // No configured event to base active-user counts on; clear and skip.\n await storage.metrics([], { names: [metricName] });\n return;\n }\n const response = await this.getSegmentation(\n phase,\n {\n event,\n from_date: range.from,\n to_date: range.to,\n unit: PHASE_UNIT[phase],\n type: 'unique',\n },\n signal,\n );\n const samples = buildActiveUserSamples(\n response,\n metricName,\n PHASE_UNIT[phase],\n event,\n );\n await storage.metrics(samples, { names: [metricName] });\n }\n\n private async runEventsPerDayPhase(\n range: MixpanelDateRange,\n storage: StorageHandle,\n signal: AbortSignal | undefined,\n ): Promise<void> {\n const metricName = METRIC_NAMES.events_per_day;\n const events = this.settings.events ?? [];\n if (events.length === 0) {\n await storage.metrics([], { names: [metricName] });\n return;\n }\n const samples: MetricSample[] = [];\n for (const event of events) {\n if (signal?.aborted) {\n throw new Error('aborted');\n }\n const [generalResponse, uniqueResponse] = await Promise.all([\n this.getSegmentation(\n 'events_per_day',\n {\n event,\n from_date: range.from,\n to_date: range.to,\n unit: 'day',\n type: 'general',\n },\n signal,\n ),\n this.getSegmentation(\n 'events_per_day',\n {\n event,\n from_date: range.from,\n to_date: range.to,\n unit: 'day',\n type: 'unique',\n },\n signal,\n ),\n ]);\n samples.push(\n ...buildEventsPerDaySamples(generalResponse, uniqueResponse, event),\n );\n }\n await storage.metrics(samples, { names: [metricName] });\n }\n\n private async runFunnelPhase(\n range: MixpanelDateRange,\n storage: StorageHandle,\n signal: AbortSignal | undefined,\n ): Promise<void> {\n const metricName = METRIC_NAMES.funnel_results;\n const funnels = this.settings.funnels ?? [];\n if (funnels.length === 0) {\n await storage.metrics([], { names: [metricName] });\n return;\n }\n const samples: MetricSample[] = [];\n for (const funnel of funnels) {\n if (signal?.aborted) {\n throw new Error('aborted');\n }\n const response = await this.getFunnel(funnel.id, range, signal);\n samples.push(...buildFunnelSamples(response, funnel));\n }\n await storage.metrics(samples, { names: [metricName] });\n }\n\n private async runRetentionPhase(\n range: MixpanelDateRange,\n storage: StorageHandle,\n signal: AbortSignal | undefined,\n ): Promise<void> {\n const metricName = METRIC_NAMES.retention;\n const event = this.settings.retentionEvent;\n if (event === undefined) {\n await storage.metrics([], { names: [metricName] });\n return;\n }\n const response = await this.getRetention(event, range, signal);\n const samples = buildRetentionSamples(response, event);\n await storage.metrics(samples, { names: [metricName] });\n }\n\n async sync(\n options: SyncOptions,\n storage: StorageHandle,\n signal?: AbortSignal,\n ): Promise<SyncResult> {\n const lookbackDays = this.settings.lookbackDays ?? DEFAULT_LOOKBACK_DAYS;\n const cursor = isMixpanelSyncCursor(options.cursor)\n ? options.cursor\n : undefined;\n // Restore the originally-computed window on resume so phases stay aligned\n // across midnight rollovers and lookbackDays changes between runs.\n const dateRange = cursor?.dateRange ?? getDateRange(options, lookbackDays);\n\n const resumeIdx = cursor ? PHASE_ORDER.indexOf(cursor.phase) : -1;\n const startIdx = resumeIdx >= 0 ? resumeIdx : 0;\n const requested = options.resources;\n\n for (let i = startIdx; i < PHASE_ORDER.length; i++) {\n const phase = PHASE_ORDER[i]!;\n if (signal?.aborted) {\n return { done: false, cursor: { phase, dateRange } };\n }\n if (requested && requested.size > 0 && !requested.has(phase)) {\n continue;\n }\n const phaseStart = Date.now();\n try {\n if (phase === 'dau' || phase === 'wau' || phase === 'mau') {\n await this.runActiveUserPhase(phase, dateRange, storage, signal);\n } else if (phase === 'events_per_day') {\n await this.runEventsPerDayPhase(dateRange, storage, signal);\n } else if (phase === 'funnel_results') {\n await this.runFunnelPhase(dateRange, storage, signal);\n } else {\n await this.runRetentionPhase(dateRange, storage, signal);\n }\n } catch (err) {\n if (\n signal?.aborted ||\n (err instanceof Error && err.name === 'AbortError')\n ) {\n return { done: false, cursor: { phase, dateRange } };\n }\n this.logger.warn('fetch page failed', {\n resource: phase,\n page: 1,\n error: err instanceof Error ? err.message : String(err),\n });\n return {\n done: false,\n cursor: { phase, dateRange },\n transientError: err,\n };\n }\n this.logger.info('resource done', {\n resource: phase,\n pages: 1,\n items: 0,\n duration_ms: Date.now() - phaseStart,\n });\n }\n\n return { done: true };\n }\n}\n","import { MixpanelConnector } from './mixpanel';\n\nexport {\n buildActiveUserSamples,\n buildEventsPerDaySamples,\n buildFunnelSamples,\n buildRetentionSamples,\n configFields,\n getDateRange,\n MixpanelConnector,\n} from './mixpanel';\nexport type {\n MixpanelFunnelSpec,\n MixpanelPhase,\n MixpanelResource,\n MixpanelSettings,\n} from './mixpanel';\nexport default MixpanelConnector;\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@rawdash/connector-mixpanel",
3
+ "version": "0.0.1",
4
+ "description": "Rawdash connector for Mixpanel — syncs event volume, funnels, retention, and DAU/WAU/MAU into the six-shape storage model via the Mixpanel Query API",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/rawdash/rawdash.git",
10
+ "directory": "packages/connectors/mixpanel"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "exports": {
18
+ ".": {
19
+ "@rawdash/source": "./src/index.ts",
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js"
22
+ }
23
+ },
24
+ "scripts": {
25
+ "build": "tsup",
26
+ "typecheck": "tsc --noEmit",
27
+ "lint": "eslint src",
28
+ "test": "vitest run"
29
+ },
30
+ "dependencies": {
31
+ "@rawdash/core": "workspace:*",
32
+ "zod": "^4.4.3"
33
+ },
34
+ "devDependencies": {
35
+ "@rawdash/connector-shared": "workspace:*",
36
+ "@rawdash/connector-test-utils": "workspace:*",
37
+ "fast-check": "^4.8.0",
38
+ "tsup": "^8.0.0",
39
+ "typescript": "^5.7.2",
40
+ "vitest": "^4.1.4"
41
+ }
42
+ }