@rawdash/connector-mixpanel 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/index.d.ts +13 -12
- package/dist/index.js +47 -20
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -42,37 +42,37 @@ Authenticate with a Mixpanel service account (username + secret) over HTTP Basic
|
|
|
42
42
|
## Resources
|
|
43
43
|
|
|
44
44
|
- **`mixpanel_dau`** _(metric)_ - Daily active users - unique-user counts for the active-user event, one sample per day.
|
|
45
|
-
- Endpoint: `GET /api/
|
|
45
|
+
- Endpoint: `GET /api/query/events (type=unique, unit=day)`
|
|
46
46
|
- Unit: users
|
|
47
47
|
- Granularity: day
|
|
48
48
|
- Dimensions: `unit`, `event`
|
|
49
49
|
- Each metric is rewritten in full per sync (idempotent replace).
|
|
50
50
|
- **`mixpanel_wau`** _(metric)_ - Weekly active users - unique-user counts for the active-user event, one sample per week.
|
|
51
|
-
- Endpoint: `GET /api/
|
|
51
|
+
- Endpoint: `GET /api/query/events (type=unique, unit=week)`
|
|
52
52
|
- Unit: users
|
|
53
53
|
- Granularity: week
|
|
54
54
|
- Dimensions: `unit`, `event`
|
|
55
55
|
- Each metric is rewritten in full per sync (idempotent replace).
|
|
56
56
|
- **`mixpanel_mau`** _(metric)_ - Monthly active users - unique-user counts for the active-user event, one sample per month.
|
|
57
|
-
- Endpoint: `GET /api/
|
|
57
|
+
- Endpoint: `GET /api/query/events (type=unique, unit=month)`
|
|
58
58
|
- Unit: users
|
|
59
59
|
- Granularity: month
|
|
60
60
|
- Dimensions: `unit`, `event`
|
|
61
61
|
- Each metric is rewritten in full per sync (idempotent replace).
|
|
62
62
|
- **`mixpanel_events_per_day`** _(metric)_ - Per-day volume for each configured event. The sample value is the total event count; unique-user count is carried as an attribute.
|
|
63
|
-
- Endpoint: `GET /api/
|
|
63
|
+
- Endpoint: `GET /api/query/segmentation (type=general and type=unique)`
|
|
64
64
|
- Unit: events
|
|
65
65
|
- Granularity: day
|
|
66
66
|
- Dimensions: `event`, `count`, `uniqueUsers`
|
|
67
67
|
- Each metric is rewritten in full per sync (idempotent replace).
|
|
68
68
|
- **`mixpanel_funnel_results`** _(metric)_ - Per-day funnel conversion. One sample per (date, step); the value is the user count reaching that step.
|
|
69
|
-
- Endpoint: `GET /api/
|
|
69
|
+
- Endpoint: `GET /api/query/funnels (unit=day)`
|
|
70
70
|
- Unit: users
|
|
71
71
|
- Granularity: day
|
|
72
72
|
- Dimensions: `funnelId`, `funnelName`, `step`, `stepLabel`, `users`, `conversionRate`, `stepConversionRate`
|
|
73
73
|
- Each metric is rewritten in full per sync (idempotent replace).
|
|
74
74
|
- **`mixpanel_retention`** _(metric)_ - Cohort retention for the retention event. One sample per (cohort date, period); the value is the retained user count.
|
|
75
|
-
- Endpoint: `GET /api/
|
|
75
|
+
- Endpoint: `GET /api/query/retention (retention_type=birth, unit=day)`
|
|
76
76
|
- Unit: users
|
|
77
77
|
- Granularity: day
|
|
78
78
|
- Dimensions: `event`, `period`, `cohortSize`, `retentionRate`
|
package/dist/index.d.ts
CHANGED
|
@@ -93,7 +93,7 @@ declare const mixpanelResources: {
|
|
|
93
93
|
readonly mixpanel_dau: {
|
|
94
94
|
readonly shape: "metric";
|
|
95
95
|
readonly description: "Daily active users - unique-user counts for the active-user event, one sample per day.";
|
|
96
|
-
readonly endpoint: "GET /api/
|
|
96
|
+
readonly endpoint: "GET /api/query/events (type=unique, unit=day)";
|
|
97
97
|
readonly unit: "users";
|
|
98
98
|
readonly granularity: "day";
|
|
99
99
|
readonly notes: "Each metric is rewritten in full per sync (idempotent replace).";
|
|
@@ -117,7 +117,7 @@ declare const mixpanelResources: {
|
|
|
117
117
|
readonly mixpanel_wau: {
|
|
118
118
|
readonly shape: "metric";
|
|
119
119
|
readonly description: "Weekly active users - unique-user counts for the active-user event, one sample per week.";
|
|
120
|
-
readonly endpoint: "GET /api/
|
|
120
|
+
readonly endpoint: "GET /api/query/events (type=unique, unit=week)";
|
|
121
121
|
readonly unit: "users";
|
|
122
122
|
readonly granularity: "week";
|
|
123
123
|
readonly notes: "Each metric is rewritten in full per sync (idempotent replace).";
|
|
@@ -141,7 +141,7 @@ declare const mixpanelResources: {
|
|
|
141
141
|
readonly mixpanel_mau: {
|
|
142
142
|
readonly shape: "metric";
|
|
143
143
|
readonly description: "Monthly active users - unique-user counts for the active-user event, one sample per month.";
|
|
144
|
-
readonly endpoint: "GET /api/
|
|
144
|
+
readonly endpoint: "GET /api/query/events (type=unique, unit=month)";
|
|
145
145
|
readonly unit: "users";
|
|
146
146
|
readonly granularity: "month";
|
|
147
147
|
readonly notes: "Each metric is rewritten in full per sync (idempotent replace).";
|
|
@@ -165,7 +165,7 @@ declare const mixpanelResources: {
|
|
|
165
165
|
readonly mixpanel_events_per_day: {
|
|
166
166
|
readonly shape: "metric";
|
|
167
167
|
readonly description: "Per-day volume for each configured event. The sample value is the total event count; unique-user count is carried as an attribute.";
|
|
168
|
-
readonly endpoint: "GET /api/
|
|
168
|
+
readonly endpoint: "GET /api/query/segmentation (type=general and type=unique)";
|
|
169
169
|
readonly unit: "events";
|
|
170
170
|
readonly granularity: "day";
|
|
171
171
|
readonly notes: "Each metric is rewritten in full per sync (idempotent replace).";
|
|
@@ -192,7 +192,7 @@ declare const mixpanelResources: {
|
|
|
192
192
|
readonly mixpanel_funnel_results: {
|
|
193
193
|
readonly shape: "metric";
|
|
194
194
|
readonly description: "Per-day funnel conversion. One sample per (date, step); the value is the user count reaching that step.";
|
|
195
|
-
readonly endpoint: "GET /api/
|
|
195
|
+
readonly endpoint: "GET /api/query/funnels (unit=day)";
|
|
196
196
|
readonly unit: "users";
|
|
197
197
|
readonly granularity: "day";
|
|
198
198
|
readonly notes: "Each metric is rewritten in full per sync (idempotent replace).";
|
|
@@ -245,7 +245,7 @@ declare const mixpanelResources: {
|
|
|
245
245
|
readonly mixpanel_retention: {
|
|
246
246
|
readonly shape: "metric";
|
|
247
247
|
readonly description: "Cohort retention for the retention event. One sample per (cohort date, period); the value is the retained user count.";
|
|
248
|
-
readonly endpoint: "GET /api/
|
|
248
|
+
readonly endpoint: "GET /api/query/retention (retention_type=birth, unit=day)";
|
|
249
249
|
readonly unit: "users";
|
|
250
250
|
readonly granularity: "day";
|
|
251
251
|
readonly notes: "Each metric is rewritten in full per sync (idempotent replace).";
|
|
@@ -281,7 +281,7 @@ declare class MixpanelConnector extends BaseConnector<MixpanelSettings, Mixpanel
|
|
|
281
281
|
readonly mixpanel_dau: {
|
|
282
282
|
readonly shape: "metric";
|
|
283
283
|
readonly description: "Daily active users - unique-user counts for the active-user event, one sample per day.";
|
|
284
|
-
readonly endpoint: "GET /api/
|
|
284
|
+
readonly endpoint: "GET /api/query/events (type=unique, unit=day)";
|
|
285
285
|
readonly unit: "users";
|
|
286
286
|
readonly granularity: "day";
|
|
287
287
|
readonly notes: "Each metric is rewritten in full per sync (idempotent replace).";
|
|
@@ -305,7 +305,7 @@ declare class MixpanelConnector extends BaseConnector<MixpanelSettings, Mixpanel
|
|
|
305
305
|
readonly mixpanel_wau: {
|
|
306
306
|
readonly shape: "metric";
|
|
307
307
|
readonly description: "Weekly active users - unique-user counts for the active-user event, one sample per week.";
|
|
308
|
-
readonly endpoint: "GET /api/
|
|
308
|
+
readonly endpoint: "GET /api/query/events (type=unique, unit=week)";
|
|
309
309
|
readonly unit: "users";
|
|
310
310
|
readonly granularity: "week";
|
|
311
311
|
readonly notes: "Each metric is rewritten in full per sync (idempotent replace).";
|
|
@@ -329,7 +329,7 @@ declare class MixpanelConnector extends BaseConnector<MixpanelSettings, Mixpanel
|
|
|
329
329
|
readonly mixpanel_mau: {
|
|
330
330
|
readonly shape: "metric";
|
|
331
331
|
readonly description: "Monthly active users - unique-user counts for the active-user event, one sample per month.";
|
|
332
|
-
readonly endpoint: "GET /api/
|
|
332
|
+
readonly endpoint: "GET /api/query/events (type=unique, unit=month)";
|
|
333
333
|
readonly unit: "users";
|
|
334
334
|
readonly granularity: "month";
|
|
335
335
|
readonly notes: "Each metric is rewritten in full per sync (idempotent replace).";
|
|
@@ -353,7 +353,7 @@ declare class MixpanelConnector extends BaseConnector<MixpanelSettings, Mixpanel
|
|
|
353
353
|
readonly mixpanel_events_per_day: {
|
|
354
354
|
readonly shape: "metric";
|
|
355
355
|
readonly description: "Per-day volume for each configured event. The sample value is the total event count; unique-user count is carried as an attribute.";
|
|
356
|
-
readonly endpoint: "GET /api/
|
|
356
|
+
readonly endpoint: "GET /api/query/segmentation (type=general and type=unique)";
|
|
357
357
|
readonly unit: "events";
|
|
358
358
|
readonly granularity: "day";
|
|
359
359
|
readonly notes: "Each metric is rewritten in full per sync (idempotent replace).";
|
|
@@ -380,7 +380,7 @@ declare class MixpanelConnector extends BaseConnector<MixpanelSettings, Mixpanel
|
|
|
380
380
|
readonly mixpanel_funnel_results: {
|
|
381
381
|
readonly shape: "metric";
|
|
382
382
|
readonly description: "Per-day funnel conversion. One sample per (date, step); the value is the user count reaching that step.";
|
|
383
|
-
readonly endpoint: "GET /api/
|
|
383
|
+
readonly endpoint: "GET /api/query/funnels (unit=day)";
|
|
384
384
|
readonly unit: "users";
|
|
385
385
|
readonly granularity: "day";
|
|
386
386
|
readonly notes: "Each metric is rewritten in full per sync (idempotent replace).";
|
|
@@ -433,7 +433,7 @@ declare class MixpanelConnector extends BaseConnector<MixpanelSettings, Mixpanel
|
|
|
433
433
|
readonly mixpanel_retention: {
|
|
434
434
|
readonly shape: "metric";
|
|
435
435
|
readonly description: "Cohort retention for the retention event. One sample per (cohort date, period); the value is the retained user count.";
|
|
436
|
-
readonly endpoint: "GET /api/
|
|
436
|
+
readonly endpoint: "GET /api/query/retention (retention_type=birth, unit=day)";
|
|
437
437
|
readonly unit: "users";
|
|
438
438
|
readonly granularity: "day";
|
|
439
439
|
readonly notes: "Each metric is rewritten in full per sync (idempotent replace).";
|
|
@@ -535,6 +535,7 @@ declare class MixpanelConnector extends BaseConnector<MixpanelSettings, Mixpanel
|
|
|
535
535
|
private authHeaders;
|
|
536
536
|
private buildQuery;
|
|
537
537
|
private getSegmentation;
|
|
538
|
+
private getEvents;
|
|
538
539
|
private getFunnel;
|
|
539
540
|
private getRetention;
|
|
540
541
|
private resolveActiveUserEvent;
|
package/dist/index.js
CHANGED
|
@@ -189,6 +189,18 @@ function getDateRange(options, lookbackDays, now = Date.now()) {
|
|
|
189
189
|
to
|
|
190
190
|
};
|
|
191
191
|
}
|
|
192
|
+
function replaceWindowFromRange(range) {
|
|
193
|
+
const start = mixpanelDateToMs(range.from);
|
|
194
|
+
const endDay = mixpanelDateToMs(range.to);
|
|
195
|
+
if (!Number.isFinite(start) || !Number.isFinite(endDay)) {
|
|
196
|
+
return void 0;
|
|
197
|
+
}
|
|
198
|
+
const end = endDay + MS_PER_DAY - 1;
|
|
199
|
+
if (start > end) {
|
|
200
|
+
return void 0;
|
|
201
|
+
}
|
|
202
|
+
return { start, end };
|
|
203
|
+
}
|
|
192
204
|
var dateString = z.string().regex(DATE_RE);
|
|
193
205
|
var finiteNumber = z.number();
|
|
194
206
|
var segmentationSchema = z.object({
|
|
@@ -233,7 +245,7 @@ var mixpanelResources = defineResources({
|
|
|
233
245
|
mixpanel_dau: {
|
|
234
246
|
shape: "metric",
|
|
235
247
|
description: "Daily active users - unique-user counts for the active-user event, one sample per day.",
|
|
236
|
-
endpoint: "GET /api/
|
|
248
|
+
endpoint: "GET /api/query/events (type=unique, unit=day)",
|
|
237
249
|
unit: "users",
|
|
238
250
|
granularity: "day",
|
|
239
251
|
notes: METRIC_NOTES,
|
|
@@ -249,7 +261,7 @@ var mixpanelResources = defineResources({
|
|
|
249
261
|
mixpanel_wau: {
|
|
250
262
|
shape: "metric",
|
|
251
263
|
description: "Weekly active users - unique-user counts for the active-user event, one sample per week.",
|
|
252
|
-
endpoint: "GET /api/
|
|
264
|
+
endpoint: "GET /api/query/events (type=unique, unit=week)",
|
|
253
265
|
unit: "users",
|
|
254
266
|
granularity: "week",
|
|
255
267
|
notes: METRIC_NOTES,
|
|
@@ -265,7 +277,7 @@ var mixpanelResources = defineResources({
|
|
|
265
277
|
mixpanel_mau: {
|
|
266
278
|
shape: "metric",
|
|
267
279
|
description: "Monthly active users - unique-user counts for the active-user event, one sample per month.",
|
|
268
|
-
endpoint: "GET /api/
|
|
280
|
+
endpoint: "GET /api/query/events (type=unique, unit=month)",
|
|
269
281
|
unit: "users",
|
|
270
282
|
granularity: "month",
|
|
271
283
|
notes: METRIC_NOTES,
|
|
@@ -281,7 +293,7 @@ var mixpanelResources = defineResources({
|
|
|
281
293
|
mixpanel_events_per_day: {
|
|
282
294
|
shape: "metric",
|
|
283
295
|
description: "Per-day volume for each configured event. The sample value is the total event count; unique-user count is carried as an attribute.",
|
|
284
|
-
endpoint: "GET /api/
|
|
296
|
+
endpoint: "GET /api/query/segmentation (type=general and type=unique)",
|
|
285
297
|
unit: "events",
|
|
286
298
|
granularity: "day",
|
|
287
299
|
notes: METRIC_NOTES,
|
|
@@ -301,7 +313,7 @@ var mixpanelResources = defineResources({
|
|
|
301
313
|
mixpanel_funnel_results: {
|
|
302
314
|
shape: "metric",
|
|
303
315
|
description: "Per-day funnel conversion. One sample per (date, step); the value is the user count reaching that step.",
|
|
304
|
-
endpoint: "GET /api/
|
|
316
|
+
endpoint: "GET /api/query/funnels (unit=day)",
|
|
305
317
|
unit: "users",
|
|
306
318
|
granularity: "day",
|
|
307
319
|
notes: METRIC_NOTES,
|
|
@@ -331,7 +343,7 @@ var mixpanelResources = defineResources({
|
|
|
331
343
|
mixpanel_retention: {
|
|
332
344
|
shape: "metric",
|
|
333
345
|
description: "Cohort retention for the retention event. One sample per (cohort date, period); the value is the retained user count.",
|
|
334
|
-
endpoint: "GET /api/
|
|
346
|
+
endpoint: "GET /api/query/retention (retention_type=birth, unit=day)",
|
|
335
347
|
unit: "users",
|
|
336
348
|
granularity: "day",
|
|
337
349
|
notes: METRIC_NOTES,
|
|
@@ -501,7 +513,7 @@ var MixpanelConnector = class _MixpanelConnector extends BaseConnector {
|
|
|
501
513
|
id = id;
|
|
502
514
|
credentials = mixpanelCredentials;
|
|
503
515
|
get apiBase() {
|
|
504
|
-
return `https://${regionHost(this.settings.region)}/api/
|
|
516
|
+
return `https://${regionHost(this.settings.region)}/api/query`;
|
|
505
517
|
}
|
|
506
518
|
authHeaders() {
|
|
507
519
|
return {
|
|
@@ -526,6 +538,21 @@ var MixpanelConnector = class _MixpanelConnector extends BaseConnector {
|
|
|
526
538
|
});
|
|
527
539
|
return segmentationSchema.parse(res.body);
|
|
528
540
|
}
|
|
541
|
+
async getEvents(resource, event, unit, range, signal) {
|
|
542
|
+
const url = `${this.apiBase}/events?${this.buildQuery({
|
|
543
|
+
event: JSON.stringify([event]),
|
|
544
|
+
from_date: range.from,
|
|
545
|
+
to_date: range.to,
|
|
546
|
+
unit,
|
|
547
|
+
type: "unique"
|
|
548
|
+
})}`;
|
|
549
|
+
const res = await this.get(url, {
|
|
550
|
+
resource,
|
|
551
|
+
headers: this.authHeaders(),
|
|
552
|
+
signal
|
|
553
|
+
});
|
|
554
|
+
return segmentationSchema.parse(res.body);
|
|
555
|
+
}
|
|
529
556
|
async getFunnel(funnelId, range, signal) {
|
|
530
557
|
const url = `${this.apiBase}/funnels?${this.buildQuery({
|
|
531
558
|
funnel_id: String(funnelId),
|
|
@@ -570,15 +597,11 @@ var MixpanelConnector = class _MixpanelConnector extends BaseConnector {
|
|
|
570
597
|
await storage.metrics([], { names: [metricName] });
|
|
571
598
|
return;
|
|
572
599
|
}
|
|
573
|
-
const response = await this.
|
|
600
|
+
const response = await this.getEvents(
|
|
574
601
|
phase,
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
to_date: range.to,
|
|
579
|
-
unit: PHASE_UNIT[phase],
|
|
580
|
-
type: "unique"
|
|
581
|
-
},
|
|
602
|
+
event,
|
|
603
|
+
PHASE_UNIT[phase],
|
|
604
|
+
range,
|
|
582
605
|
signal
|
|
583
606
|
);
|
|
584
607
|
const samples = buildActiveUserSamples(
|
|
@@ -587,7 +610,8 @@ var MixpanelConnector = class _MixpanelConnector extends BaseConnector {
|
|
|
587
610
|
PHASE_UNIT[phase],
|
|
588
611
|
event
|
|
589
612
|
);
|
|
590
|
-
|
|
613
|
+
const replaceWindow = replaceWindowFromRange(range);
|
|
614
|
+
await storage.metrics(samples, { names: [metricName], replaceWindow });
|
|
591
615
|
}
|
|
592
616
|
async runEventsPerDayPhase(range, storage, signal) {
|
|
593
617
|
const metricName = METRIC_NAMES.events_per_day;
|
|
@@ -629,7 +653,8 @@ var MixpanelConnector = class _MixpanelConnector extends BaseConnector {
|
|
|
629
653
|
...buildEventsPerDaySamples(generalResponse, uniqueResponse, event)
|
|
630
654
|
);
|
|
631
655
|
}
|
|
632
|
-
|
|
656
|
+
const replaceWindow = replaceWindowFromRange(range);
|
|
657
|
+
await storage.metrics(samples, { names: [metricName], replaceWindow });
|
|
633
658
|
}
|
|
634
659
|
async runFunnelPhase(range, storage, signal) {
|
|
635
660
|
const metricName = METRIC_NAMES.funnel_results;
|
|
@@ -646,7 +671,8 @@ var MixpanelConnector = class _MixpanelConnector extends BaseConnector {
|
|
|
646
671
|
const response = await this.getFunnel(funnel.id, range, signal);
|
|
647
672
|
samples.push(...buildFunnelSamples(response, funnel));
|
|
648
673
|
}
|
|
649
|
-
|
|
674
|
+
const replaceWindow = replaceWindowFromRange(range);
|
|
675
|
+
await storage.metrics(samples, { names: [metricName], replaceWindow });
|
|
650
676
|
}
|
|
651
677
|
async runRetentionPhase(range, storage, signal) {
|
|
652
678
|
const metricName = METRIC_NAMES.retention;
|
|
@@ -657,7 +683,8 @@ var MixpanelConnector = class _MixpanelConnector extends BaseConnector {
|
|
|
657
683
|
}
|
|
658
684
|
const response = await this.getRetention(event, range, signal);
|
|
659
685
|
const samples = buildRetentionSamples(response, event);
|
|
660
|
-
|
|
686
|
+
const replaceWindow = replaceWindowFromRange(range);
|
|
687
|
+
await storage.metrics(samples, { names: [metricName], replaceWindow });
|
|
661
688
|
}
|
|
662
689
|
async sync(options, storage, signal) {
|
|
663
690
|
const lookbackDays = this.settings.lookbackDays ?? DEFAULT_LOOKBACK_DAYS;
|
|
@@ -671,7 +698,7 @@ var MixpanelConnector = class _MixpanelConnector extends BaseConnector {
|
|
|
671
698
|
if (signal?.aborted) {
|
|
672
699
|
return { done: false, cursor: { phase, dateRange } };
|
|
673
700
|
}
|
|
674
|
-
if (requested && requested.size > 0 && !requested.has(phase)) {
|
|
701
|
+
if (requested && requested.size > 0 && !requested.has(METRIC_NAMES[phase])) {
|
|
675
702
|
continue;
|
|
676
703
|
}
|
|
677
704
|
const phaseStart = Date.now();
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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/map-concurrent.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 async function mapWithConcurrency<T, R>(\n items: readonly T[],\n concurrency: number,\n fn: (item: T, index: number) => Promise<R>,\n): Promise<R[]> {\n const results = new Array<R>(items.length);\n if (items.length === 0) {\n return results;\n }\n const normalized = Number.isFinite(concurrency) ? Math.floor(concurrency) : 1;\n const limit = Math.max(1, Math.min(normalized, items.length));\n let next = 0;\n let failed = false;\n\n async function worker(): Promise<void> {\n while (!failed) {\n const i = next++;\n if (i >= items.length) {\n return;\n }\n try {\n results[i] = await fn(items[i]!, i);\n } catch (err) {\n failed = true;\n throw err;\n }\n }\n }\n\n const workers: Promise<void>[] = [];\n for (let w = 0; w < limit; w++) {\n workers.push(worker());\n }\n await Promise.all(workers);\n return results;\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 ConnectorCost,\n type ConnectorDoc,\n type CredentialsSchema,\n type JSONValue,\n type MetricSample,\n type StorageHandle,\n type SyncOptions,\n type SyncResult,\n defineConfigFields,\n defineConnectorDoc,\n defineResources,\n schemasFromResources,\n} from '@rawdash/core';\nimport { z } from 'zod';\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\nexport const doc: ConnectorDoc = defineConnectorDoc({\n displayName: 'Mixpanel',\n category: 'analytics',\n brandColor: '#7856FF',\n tagline:\n 'Sync Mixpanel active-user counts, per-event volume, funnel conversion, and cohort retention as metric time series.',\n vendor: {\n name: 'Mixpanel',\n domain: 'mixpanel.com',\n apiDocs: 'https://developer.mixpanel.com/reference/query-api',\n website: 'https://mixpanel.com',\n },\n auth: {\n summary:\n 'Authenticate with a Mixpanel service account (username + secret) over HTTP Basic auth, scoped to a numeric project ID.',\n setup: [\n 'In Mixpanel, open Project settings → Service Accounts and create a service account with at least read access to the project.',\n 'Copy the generated username (e.g. `rawdash-reader.abcdef.mp-service-account`) and the secret shown once at creation.',\n 'Find the numeric project ID under Project settings → Overview and set it as `projectId`.',\n 'Store the secret and reference it from config as `secret: secret(\"MIXPANEL_SECRET\")`, alongside the `username`.',\n 'For EU-resident projects, set `region: \"eu\"`.',\n ],\n },\n rateLimit:\n \"Mixpanel's Query API quota is 60 queries/hour per project (default); requests are retried with backoff.\",\n limitations: [\n 'Incremental syncs refetch a 3-day overlap because Mixpanel can re-attribute late-arriving events.',\n ],\n});\n\nexport const cost: ConnectorCost = {\n warning:\n 'Each configured event and funnel costs one or more queries per sync against Mixpanel quotas; adding many events/funnels or syncing frequently can exhaust the quota.',\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\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\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\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\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\nconst METRIC_NOTES =\n 'Each metric is rewritten in full per sync (idempotent replace).';\n\nexport const mixpanelResources = defineResources({\n mixpanel_dau: {\n shape: 'metric',\n description:\n 'Daily active users - unique-user counts for the active-user event, one sample per day.',\n endpoint: 'GET /api/2.0/segmentation (type=unique, unit=day)',\n unit: 'users',\n granularity: 'day',\n notes: METRIC_NOTES,\n dimensions: [\n { name: 'unit', description: 'Active-user window: always `day`.' },\n {\n name: 'event',\n description: 'The event the active-user count is based on.',\n },\n ],\n responses: { dau: segmentationSchema },\n },\n mixpanel_wau: {\n shape: 'metric',\n description:\n 'Weekly active users - unique-user counts for the active-user event, one sample per week.',\n endpoint: 'GET /api/2.0/segmentation (type=unique, unit=week)',\n unit: 'users',\n granularity: 'week',\n notes: METRIC_NOTES,\n dimensions: [\n { name: 'unit', description: 'Active-user window: always `week`.' },\n {\n name: 'event',\n description: 'The event the active-user count is based on.',\n },\n ],\n responses: { wau: segmentationSchema },\n },\n mixpanel_mau: {\n shape: 'metric',\n description:\n 'Monthly active users - unique-user counts for the active-user event, one sample per month.',\n endpoint: 'GET /api/2.0/segmentation (type=unique, unit=month)',\n unit: 'users',\n granularity: 'month',\n notes: METRIC_NOTES,\n dimensions: [\n { name: 'unit', description: 'Active-user window: always `month`.' },\n {\n name: 'event',\n description: 'The event the active-user count is based on.',\n },\n ],\n responses: { mau: segmentationSchema },\n },\n mixpanel_events_per_day: {\n shape: 'metric',\n description:\n 'Per-day volume for each configured event. The sample value is the total event count; unique-user count is carried as an attribute.',\n endpoint: 'GET /api/2.0/segmentation (type=general and type=unique)',\n unit: 'events',\n granularity: 'day',\n notes: METRIC_NOTES,\n dimensions: [\n { name: 'event', description: 'The configured event name.' },\n {\n name: 'count',\n description: 'Total event count for the day (equals the value).',\n },\n {\n name: 'uniqueUsers',\n description: 'Distinct users who triggered the event that day.',\n },\n ],\n responses: { events_per_day: segmentationSchema },\n },\n mixpanel_funnel_results: {\n shape: 'metric',\n description:\n 'Per-day funnel conversion. One sample per (date, step); the value is the user count reaching that step.',\n endpoint: 'GET /api/2.0/funnels (unit=day)',\n unit: 'users',\n granularity: 'day',\n notes: METRIC_NOTES,\n dimensions: [\n { name: 'funnelId', description: 'The configured Mixpanel funnel ID.' },\n {\n name: 'funnelName',\n description: 'Optional display name from config (present when set).',\n },\n { name: 'step', description: 'Zero-based step index in the funnel.' },\n {\n name: 'stepLabel',\n description: 'Human-readable step label or event name.',\n },\n { name: 'users', description: 'Users reaching this step.' },\n {\n name: 'conversionRate',\n description: 'Overall conversion ratio from the first step.',\n },\n {\n name: 'stepConversionRate',\n description: 'Conversion ratio from the previous step.',\n },\n ],\n responses: { funnel_results: funnelSchema },\n },\n mixpanel_retention: {\n shape: 'metric',\n description:\n 'Cohort retention for the retention event. One sample per (cohort date, period); the value is the retained user count.',\n endpoint: 'GET /api/2.0/retention (retention_type=birth, unit=day)',\n unit: 'users',\n granularity: 'day',\n notes: METRIC_NOTES,\n dimensions: [\n { name: 'event', description: 'The retention (born) event.' },\n {\n name: 'period',\n description: 'Days since the cohort birth date (period index).',\n },\n {\n name: 'cohortSize',\n description: 'Number of users in the cohort at birth.',\n },\n {\n name: 'retentionRate',\n description: 'Retained users divided by cohort size.',\n },\n ],\n responses: { retention: retentionSchema },\n },\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 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\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\nexport const id = 'mixpanel';\n\nexport class MixpanelConnector extends BaseConnector<\n MixpanelSettings,\n MixpanelCredentials\n> {\n static readonly id = id;\n\n static readonly resources = mixpanelResources;\n\n static readonly schemas = schemasFromResources(mixpanelResources);\n\n static readonly cost: ConnectorCost = cost;\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 = id;\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 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 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 cost,\n doc,\n getDateRange,\n id,\n MixpanelConnector,\n mixpanelResources as resources,\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":[]}
|
|
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/map-concurrent.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(\n res: Response,\n parseJson: boolean,\n binary: boolean,\n): Promise<unknown> {\n if (res.status === 204 || res.status === 205) {\n return null;\n }\n if (binary) {\n return new Uint8Array(await res.arrayBuffer());\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 const binary = req.binary ?? false;\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, binary);\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 async function mapWithConcurrency<T, R>(\n items: readonly T[],\n concurrency: number,\n fn: (item: T, index: number) => Promise<R>,\n): Promise<R[]> {\n const results = new Array<R>(items.length);\n if (items.length === 0) {\n return results;\n }\n const normalized = Number.isFinite(concurrency) ? Math.floor(concurrency) : 1;\n const limit = Math.max(1, Math.min(normalized, items.length));\n let next = 0;\n let failed = false;\n\n async function worker(): Promise<void> {\n while (!failed) {\n const i = next++;\n if (i >= items.length) {\n return;\n }\n try {\n results[i] = await fn(items[i]!, i);\n } catch (err) {\n failed = true;\n throw err;\n }\n }\n }\n\n const workers: Promise<void>[] = [];\n for (let w = 0; w < limit; w++) {\n workers.push(worker());\n }\n await Promise.all(workers);\n return results;\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 ConnectorCost,\n type ConnectorDoc,\n type CredentialsSchema,\n type JSONValue,\n type MetricSample,\n type StorageHandle,\n type SyncOptions,\n type SyncResult,\n defineConfigFields,\n defineConnectorDoc,\n defineResources,\n schemasFromResources,\n} from '@rawdash/core';\nimport { z } from 'zod';\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\nexport const doc: ConnectorDoc = defineConnectorDoc({\n displayName: 'Mixpanel',\n category: 'analytics',\n brandColor: '#7856FF',\n tagline:\n 'Sync Mixpanel active-user counts, per-event volume, funnel conversion, and cohort retention as metric time series.',\n vendor: {\n name: 'Mixpanel',\n domain: 'mixpanel.com',\n apiDocs: 'https://developer.mixpanel.com/reference/query-api',\n website: 'https://mixpanel.com',\n },\n auth: {\n summary:\n 'Authenticate with a Mixpanel service account (username + secret) over HTTP Basic auth, scoped to a numeric project ID.',\n setup: [\n 'In Mixpanel, open Project settings → Service Accounts and create a service account with at least read access to the project.',\n 'Copy the generated username (e.g. `rawdash-reader.abcdef.mp-service-account`) and the secret shown once at creation.',\n 'Find the numeric project ID under Project settings → Overview and set it as `projectId`.',\n 'Store the secret and reference it from config as `secret: secret(\"MIXPANEL_SECRET\")`, alongside the `username`.',\n 'For EU-resident projects, set `region: \"eu\"`.',\n ],\n },\n rateLimit:\n \"Mixpanel's Query API quota is 60 queries/hour per project (default); requests are retried with backoff.\",\n limitations: [\n 'Incremental syncs refetch a 3-day overlap because Mixpanel can re-attribute late-arriving events.',\n ],\n});\n\nexport const cost: ConnectorCost = {\n warning:\n 'Each configured event and funnel costs one or more queries per sync against Mixpanel quotas; adding many events/funnels or syncing frequently can exhaust the quota.',\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\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\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\nexport function replaceWindowFromRange(\n range: MixpanelDateRange,\n): { start: number; end: number } | undefined {\n const start = mixpanelDateToMs(range.from);\n const endDay = mixpanelDateToMs(range.to);\n if (!Number.isFinite(start) || !Number.isFinite(endDay)) {\n return undefined;\n }\n const end = endDay + MS_PER_DAY - 1;\n if (start > end) {\n return undefined;\n }\n return { start, end };\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\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\nconst METRIC_NOTES =\n 'Each metric is rewritten in full per sync (idempotent replace).';\n\nexport const mixpanelResources = defineResources({\n mixpanel_dau: {\n shape: 'metric',\n description:\n 'Daily active users - unique-user counts for the active-user event, one sample per day.',\n endpoint: 'GET /api/query/events (type=unique, unit=day)',\n unit: 'users',\n granularity: 'day',\n notes: METRIC_NOTES,\n dimensions: [\n { name: 'unit', description: 'Active-user window: always `day`.' },\n {\n name: 'event',\n description: 'The event the active-user count is based on.',\n },\n ],\n responses: { dau: segmentationSchema },\n },\n mixpanel_wau: {\n shape: 'metric',\n description:\n 'Weekly active users - unique-user counts for the active-user event, one sample per week.',\n endpoint: 'GET /api/query/events (type=unique, unit=week)',\n unit: 'users',\n granularity: 'week',\n notes: METRIC_NOTES,\n dimensions: [\n { name: 'unit', description: 'Active-user window: always `week`.' },\n {\n name: 'event',\n description: 'The event the active-user count is based on.',\n },\n ],\n responses: { wau: segmentationSchema },\n },\n mixpanel_mau: {\n shape: 'metric',\n description:\n 'Monthly active users - unique-user counts for the active-user event, one sample per month.',\n endpoint: 'GET /api/query/events (type=unique, unit=month)',\n unit: 'users',\n granularity: 'month',\n notes: METRIC_NOTES,\n dimensions: [\n { name: 'unit', description: 'Active-user window: always `month`.' },\n {\n name: 'event',\n description: 'The event the active-user count is based on.',\n },\n ],\n responses: { mau: segmentationSchema },\n },\n mixpanel_events_per_day: {\n shape: 'metric',\n description:\n 'Per-day volume for each configured event. The sample value is the total event count; unique-user count is carried as an attribute.',\n endpoint: 'GET /api/query/segmentation (type=general and type=unique)',\n unit: 'events',\n granularity: 'day',\n notes: METRIC_NOTES,\n dimensions: [\n { name: 'event', description: 'The configured event name.' },\n {\n name: 'count',\n description: 'Total event count for the day (equals the value).',\n },\n {\n name: 'uniqueUsers',\n description: 'Distinct users who triggered the event that day.',\n },\n ],\n responses: { events_per_day: segmentationSchema },\n },\n mixpanel_funnel_results: {\n shape: 'metric',\n description:\n 'Per-day funnel conversion. One sample per (date, step); the value is the user count reaching that step.',\n endpoint: 'GET /api/query/funnels (unit=day)',\n unit: 'users',\n granularity: 'day',\n notes: METRIC_NOTES,\n dimensions: [\n { name: 'funnelId', description: 'The configured Mixpanel funnel ID.' },\n {\n name: 'funnelName',\n description: 'Optional display name from config (present when set).',\n },\n { name: 'step', description: 'Zero-based step index in the funnel.' },\n {\n name: 'stepLabel',\n description: 'Human-readable step label or event name.',\n },\n { name: 'users', description: 'Users reaching this step.' },\n {\n name: 'conversionRate',\n description: 'Overall conversion ratio from the first step.',\n },\n {\n name: 'stepConversionRate',\n description: 'Conversion ratio from the previous step.',\n },\n ],\n responses: { funnel_results: funnelSchema },\n },\n mixpanel_retention: {\n shape: 'metric',\n description:\n 'Cohort retention for the retention event. One sample per (cohort date, period); the value is the retained user count.',\n endpoint: 'GET /api/query/retention (retention_type=birth, unit=day)',\n unit: 'users',\n granularity: 'day',\n notes: METRIC_NOTES,\n dimensions: [\n { name: 'event', description: 'The retention (born) event.' },\n {\n name: 'period',\n description: 'Days since the cohort birth date (period index).',\n },\n {\n name: 'cohortSize',\n description: 'Number of users in the cohort at birth.',\n },\n {\n name: 'retentionRate',\n description: 'Retained users divided by cohort size.',\n },\n ],\n responses: { retention: retentionSchema },\n },\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 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\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\nexport const id = 'mixpanel';\n\nexport class MixpanelConnector extends BaseConnector<\n MixpanelSettings,\n MixpanelCredentials\n> {\n static readonly id = id;\n\n static readonly resources = mixpanelResources;\n\n static readonly schemas = schemasFromResources(mixpanelResources);\n\n static readonly cost: ConnectorCost = cost;\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 = id;\n override readonly credentials = mixpanelCredentials;\n\n private get apiBase(): string {\n return `https://${regionHost(this.settings.region)}/api/query`;\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 getEvents(\n resource: MixpanelPhase,\n event: string,\n unit: 'day' | 'week' | 'month',\n range: MixpanelDateRange,\n signal: AbortSignal | undefined,\n ): Promise<SegmentationResponse> {\n const url = `${this.apiBase}/events?${this.buildQuery({\n event: JSON.stringify([event]),\n from_date: range.from,\n to_date: range.to,\n unit,\n type: 'unique',\n })}`;\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 await storage.metrics([], { names: [metricName] });\n return;\n }\n const response = await this.getEvents(\n phase,\n event,\n PHASE_UNIT[phase],\n range,\n signal,\n );\n const samples = buildActiveUserSamples(\n response,\n metricName,\n PHASE_UNIT[phase],\n event,\n );\n const replaceWindow = replaceWindowFromRange(range);\n await storage.metrics(samples, { names: [metricName], replaceWindow });\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 const replaceWindow = replaceWindowFromRange(range);\n await storage.metrics(samples, { names: [metricName], replaceWindow });\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 const replaceWindow = replaceWindowFromRange(range);\n await storage.metrics(samples, { names: [metricName], replaceWindow });\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 const replaceWindow = replaceWindowFromRange(range);\n await storage.metrics(samples, { names: [metricName], replaceWindow });\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 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 (\n requested &&\n requested.size > 0 &&\n !requested.has(METRIC_NAMES[phase])\n ) {\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 cost,\n doc,\n getDateRange,\n id,\n MixpanelConnector,\n mixpanelResources as resources,\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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rawdash/connector-mixpanel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
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
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -24,15 +24,15 @@
|
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"zod": "^4.4.3",
|
|
27
|
-
"@rawdash/core": "0.
|
|
27
|
+
"@rawdash/core": "0.29.0"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"fast-check": "^4.8.0",
|
|
31
31
|
"tsup": "^8.0.0",
|
|
32
32
|
"typescript": "^5.7.2",
|
|
33
33
|
"vitest": "^4.1.4",
|
|
34
|
-
"@rawdash/connector-
|
|
35
|
-
"@rawdash/connector-
|
|
34
|
+
"@rawdash/connector-shared": "0.3.1",
|
|
35
|
+
"@rawdash/connector-test-utils": "0.0.10"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"build": "tsup",
|