@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 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/2.0/segmentation (type=unique, unit=day)`
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/2.0/segmentation (type=unique, unit=week)`
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/2.0/segmentation (type=unique, unit=month)`
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/2.0/segmentation (type=general and type=unique)`
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/2.0/funnels (unit=day)`
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/2.0/retention (retention_type=birth, unit=day)`
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/2.0/segmentation (type=unique, unit=day)";
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/2.0/segmentation (type=unique, unit=week)";
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/2.0/segmentation (type=unique, unit=month)";
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/2.0/segmentation (type=general and type=unique)";
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/2.0/funnels (unit=day)";
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/2.0/retention (retention_type=birth, unit=day)";
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/2.0/segmentation (type=unique, unit=day)";
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/2.0/segmentation (type=unique, unit=week)";
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/2.0/segmentation (type=unique, unit=month)";
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/2.0/segmentation (type=general and type=unique)";
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/2.0/funnels (unit=day)";
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/2.0/retention (retention_type=birth, unit=day)";
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/2.0/segmentation (type=unique, unit=day)",
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/2.0/segmentation (type=unique, unit=week)",
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/2.0/segmentation (type=unique, unit=month)",
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/2.0/segmentation (type=general and type=unique)",
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/2.0/funnels (unit=day)",
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/2.0/retention (retention_type=birth, unit=day)",
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/2.0`;
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.getSegmentation(
600
+ const response = await this.getEvents(
574
601
  phase,
575
- {
576
- event,
577
- from_date: range.from,
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
- await storage.metrics(samples, { names: [metricName] });
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
- await storage.metrics(samples, { names: [metricName] });
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
- await storage.metrics(samples, { names: [metricName] });
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
- await storage.metrics(samples, { names: [metricName] });
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":";AEAO,IAAM,sBAAsB;AAE5B,IAAM,qBAAqB,qBAAqB,mBAAmB;AAEnE,SAAS,mBAAmB,aAA6B;AAC9D,SAAO,qBAAqB,WAAW,IAAI,mBAAmB;AAChE;;;AQLA;AAAA,EACE;AAAA,EAUA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,SAAS;AAElB,IAAM,aAAa,EAAE,OAAO;AAAA,EAC1B,IAAI,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC,EAAE,KAAK;AAAA,IACjE,OAAO;AAAA,IACP,aAAa;AAAA,EACf,CAAC;AAAA,EACD,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,KAAK;AAAA,IACtC,OAAO;AAAA,IACP,aAAa;AAAA,EACf,CAAC;AACH,CAAC;AAEM,IAAM,eAAe;AAAA,EAC1B,EAAE,OAAO;AAAA,IACP,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,KAAK;AAAA,MAC/B,OAAO;AAAA,MACP,aACE;AAAA,IACJ,CAAC;AAAA,IACD,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK;AAAA,MAC7C,OAAO;AAAA,MACP,aACE;AAAA,MACF,QAAQ;AAAA,IACV,CAAC;AAAA,IACD,WAAW,EACR,OAAO,EACP,KAAK,EACL,MAAM,SAAS,iDAAiD,EAChE,KAAK;AAAA,MACJ,OAAO;AAAA,MACP,aACE;AAAA,MACF,aAAa;AAAA,IACf,CAAC;AAAA,IACH,QAAQ,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,EAAE,SAAS,EAAE,KAAK;AAAA,MAC3C,OAAO;AAAA,MACP,aAAa;AAAA,IACf,CAAC;AAAA,IACD,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK;AAAA,MACjD,OAAO;AAAA,MACP,aACE;AAAA,IACJ,CAAC;AAAA,IACD,SAAS,EAAE,MAAM,UAAU,EAAE,SAAS,EAAE,KAAK;AAAA,MAC3C,OAAO;AAAA,MACP,aACE;AAAA,IACJ,CAAC;AAAA,IACD,gBAAgB,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,KAAK;AAAA,MAChD,OAAO;AAAA,MACP,aACE;AAAA,IACJ,CAAC;AAAA,IACD,iBAAiB,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,KAAK;AAAA,MACjD,OAAO;AAAA,MACP,aACE;AAAA,IACJ,CAAC;AAAA,IACD,cAAc,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK;AAAA,MACxD,OAAO;AAAA,MACP,aAAa;AAAA,MACb,aAAa;AAAA,IACf,CAAC;AAAA,EACH,CAAC;AACH;AAEO,IAAM,MAAoB,mBAAmB;AAAA,EAClD,aAAa;AAAA,EACb,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,SACE;AAAA,EACF,QAAQ;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,SAAS;AAAA,EACX;AAAA,EACA,MAAM;AAAA,IACJ,SACE;AAAA,IACF,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA,EACA,WACE;AAAA,EACF,aAAa;AAAA,IACX;AAAA,EACF;AACF,CAAC;AAEM,IAAM,OAAsB;AAAA,EACjC,SACE;AACJ;AAiBA,IAAM,sBAAsB;AAAA,EAC1B,UAAU;AAAA,IACR,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA,QAAQ;AAAA,IACN,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AACF;AAIA,IAAM,wBAAwB;AAC9B,IAAM,4BAA4B;AAClC,IAAM,aAAa;AAEnB,IAAM,cAAc;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAKA,IAAM,eAA8C;AAAA,EAClD,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,gBAAgB;AAAA,EAChB,gBAAgB;AAAA,EAChB,WAAW;AACb;AAEA,IAAM,aAAsE;AAAA,EAC1E,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AACP;AAYA,IAAM,UAAU;AAEhB,SAAS,aAAa,OAAiC;AACrD,SAAO,OAAO,UAAU,YAAY,QAAQ,KAAK,KAAK;AACxD;AAEA,SAAS,YAAY,OAA4C;AAC/D,MAAI,OAAO,UAAU,YAAY,UAAU,MAAM;AAC/C,WAAO;AAAA,EACT;AACA,QAAM,IAAI;AACV,SAAO,aAAa,EAAE,IAAI,KAAK,aAAa,EAAE,EAAE;AAClD;AAEA,SAAS,qBAAqB,OAA6C;AACzE,MAAI,OAAO,UAAU,YAAY,UAAU,MAAM;AAC/C,WAAO;AAAA,EACT;AACA,QAAM,IAAI;AACV,MAAI,OAAO,EAAE,UAAU,UAAU;AAC/B,WAAO;AAAA,EACT;AACA,MAAI,CAAE,YAAkC,SAAS,EAAE,KAAK,GAAG;AACzD,WAAO;AAAA,EACT;AACA,SAAO,YAAY,EAAE,SAAS;AAChC;AAEA,SAAS,KAAK,GAAmB;AAC/B,SAAO,OAAO,CAAC,EAAE,SAAS,GAAG,GAAG;AAClC;AAEA,SAAS,eAAe,IAAoB;AAC1C,QAAM,IAAI,IAAI,KAAK,EAAE;AACrB,SAAO,GAAG,EAAE,eAAe,CAAC,IAAI,KAAK,EAAE,YAAY,IAAI,CAAC,CAAC,IAAI,KAAK,EAAE,WAAW,CAAC,CAAC;AACnF;AAEA,SAAS,iBAAiB,MAAsB;AAC9C,QAAM,IAAI,4BAA4B,KAAK,IAAI;AAC/C,MAAI,CAAC,GAAG;AACN,WAAO;AAAA,EACT;AACA,SAAO,KAAK,IAAI,OAAO,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,CAAC,IAAI,GAAG,OAAO,EAAE,CAAC,CAAC,CAAC;AAC9D;AAEO,SAAS,aACd,SACA,cACA,MAAc,KAAK,IAAI,GACJ;AACnB,QAAM,KAAK,eAAe,GAAG;AAC7B,MAAI,QAAQ,SAAS,UAAU;AAC7B,WAAO;AAAA,MACL,MAAM,eAAe,OAAO,4BAA4B,KAAK,UAAU;AAAA,MACvE;AAAA,IACF;AAAA,EACF;AACA,MAAI,QAAQ,UAAU,QAAW;AAC/B,UAAM,UAAU,KAAK,MAAM,QAAQ,KAAK;AACxC,QAAI,OAAO,SAAS,OAAO,GAAG;AAC5B,YAAM,UAAU,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,WAAW,UAAU,CAAC;AACnE,YAAM,OAAO,KAAK,IAAI,SAAS,YAAY;AAC3C,aAAO,EAAE,MAAM,eAAe,OAAO,OAAO,KAAK,UAAU,GAAG,GAAG;AAAA,IACnE;AAAA,EACF;AACA,SAAO;AAAA,IACL,MAAM,eAAe,OAAO,eAAe,KAAK,UAAU;AAAA,IAC1D;AAAA,EACF;AACF;AAEO,SAAS,uBACd,OAC4C;AAC5C,QAAM,QAAQ,iBAAiB,MAAM,IAAI;AACzC,QAAM,SAAS,iBAAiB,MAAM,EAAE;AACxC,MAAI,CAAC,OAAO,SAAS,KAAK,KAAK,CAAC,OAAO,SAAS,MAAM,GAAG;AACvD,WAAO;AAAA,EACT;AACA,QAAM,MAAM,SAAS,aAAa;AAClC,MAAI,QAAQ,KAAK;AACf,WAAO;AAAA,EACT;AACA,SAAO,EAAE,OAAO,IAAI;AACtB;AAyCA,IAAM,aAAa,EAAE,OAAO,EAAE,MAAM,OAAO;AAC3C,IAAM,eAAe,EAAE,OAAO;AAE9B,IAAM,qBAAqB,EAAE,OAAO;AAAA,EAClC,aAAa,EAAE,OAAO,EAAE,SAAS;AAAA,EACjC,MAAM,EAAE,OAAO;AAAA,IACb,QAAQ,EAAE,MAAM,UAAU;AAAA,IAC1B,QAAQ,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,OAAO,YAAY,YAAY,CAAC;AAAA,EACjE,CAAC;AACH,CAAC;AAED,IAAM,mBAAmB,EAAE,OAAO;AAAA,EAChC,YAAY,EAAE,OAAO,EAAE,SAAS;AAAA,EAChC,MAAM,EAAE,OAAO,EAAE,SAAS;AAAA,EAC1B,OAAO,EAAE,OAAO,EAAE,SAAS;AAAA,EAC3B,OAAO;AAAA,EACP,oBAAoB,aAAa,SAAS;AAAA,EAC1C,iBAAiB,aAAa,SAAS;AACzC,CAAC;AAED,IAAM,eAAe,EAAE,OAAO;AAAA,EAC5B,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,EAAE,SAAS,EAAE,CAAC,EAAE,SAAS;AAAA,EACnE,MAAM,EAAE;AAAA,IACN;AAAA,IACA,EAAE,OAAO;AAAA,MACP,OAAO,EAAE,MAAM,gBAAgB;AAAA,MAC/B,UAAU,EACP,OAAO;AAAA,QACN,YAAY,aAAa,SAAS;AAAA,QAClC,iBAAiB,aAAa,SAAS;AAAA,QACvC,OAAO,aAAa,SAAS;AAAA,QAC7B,OAAO,aAAa,SAAS;AAAA,MAC/B,CAAC,EACA,SAAS;AAAA,IACd,CAAC;AAAA,EACH;AACF,CAAC;AAED,IAAM,kBAAkB,EAAE;AAAA,EACxB;AAAA,EACA,EAAE,OAAO;AAAA,IACP,OAAO;AAAA,IACP,QAAQ,EAAE,MAAM,YAAY;AAAA,EAC9B,CAAC;AACH;AAEA,IAAM,eACJ;AAEK,IAAM,oBAAoB,gBAAgB;AAAA,EAC/C,cAAc;AAAA,IACZ,OAAO;AAAA,IACP,aACE;AAAA,IACF,UAAU;AAAA,IACV,MAAM;AAAA,IACN,aAAa;AAAA,IACb,OAAO;AAAA,IACP,YAAY;AAAA,MACV,EAAE,MAAM,QAAQ,aAAa,oCAAoC;AAAA,MACjE;AAAA,QACE,MAAM;AAAA,QACN,aAAa;AAAA,MACf;AAAA,IACF;AAAA,IACA,WAAW,EAAE,KAAK,mBAAmB;AAAA,EACvC;AAAA,EACA,cAAc;AAAA,IACZ,OAAO;AAAA,IACP,aACE;AAAA,IACF,UAAU;AAAA,IACV,MAAM;AAAA,IACN,aAAa;AAAA,IACb,OAAO;AAAA,IACP,YAAY;AAAA,MACV,EAAE,MAAM,QAAQ,aAAa,qCAAqC;AAAA,MAClE;AAAA,QACE,MAAM;AAAA,QACN,aAAa;AAAA,MACf;AAAA,IACF;AAAA,IACA,WAAW,EAAE,KAAK,mBAAmB;AAAA,EACvC;AAAA,EACA,cAAc;AAAA,IACZ,OAAO;AAAA,IACP,aACE;AAAA,IACF,UAAU;AAAA,IACV,MAAM;AAAA,IACN,aAAa;AAAA,IACb,OAAO;AAAA,IACP,YAAY;AAAA,MACV,EAAE,MAAM,QAAQ,aAAa,sCAAsC;AAAA,MACnE;AAAA,QACE,MAAM;AAAA,QACN,aAAa;AAAA,MACf;AAAA,IACF;AAAA,IACA,WAAW,EAAE,KAAK,mBAAmB;AAAA,EACvC;AAAA,EACA,yBAAyB;AAAA,IACvB,OAAO;AAAA,IACP,aACE;AAAA,IACF,UAAU;AAAA,IACV,MAAM;AAAA,IACN,aAAa;AAAA,IACb,OAAO;AAAA,IACP,YAAY;AAAA,MACV,EAAE,MAAM,SAAS,aAAa,6BAA6B;AAAA,MAC3D;AAAA,QACE,MAAM;AAAA,QACN,aAAa;AAAA,MACf;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,aAAa;AAAA,MACf;AAAA,IACF;AAAA,IACA,WAAW,EAAE,gBAAgB,mBAAmB;AAAA,EAClD;AAAA,EACA,yBAAyB;AAAA,IACvB,OAAO;AAAA,IACP,aACE;AAAA,IACF,UAAU;AAAA,IACV,MAAM;AAAA,IACN,aAAa;AAAA,IACb,OAAO;AAAA,IACP,YAAY;AAAA,MACV,EAAE,MAAM,YAAY,aAAa,qCAAqC;AAAA,MACtE;AAAA,QACE,MAAM;AAAA,QACN,aAAa;AAAA,MACf;AAAA,MACA,EAAE,MAAM,QAAQ,aAAa,uCAAuC;AAAA,MACpE;AAAA,QACE,MAAM;AAAA,QACN,aAAa;AAAA,MACf;AAAA,MACA,EAAE,MAAM,SAAS,aAAa,4BAA4B;AAAA,MAC1D;AAAA,QACE,MAAM;AAAA,QACN,aAAa;AAAA,MACf;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,aAAa;AAAA,MACf;AAAA,IACF;AAAA,IACA,WAAW,EAAE,gBAAgB,aAAa;AAAA,EAC5C;AAAA,EACA,oBAAoB;AAAA,IAClB,OAAO;AAAA,IACP,aACE;AAAA,IACF,UAAU;AAAA,IACV,MAAM;AAAA,IACN,aAAa;AAAA,IACb,OAAO;AAAA,IACP,YAAY;AAAA,MACV,EAAE,MAAM,SAAS,aAAa,8BAA8B;AAAA,MAC5D;AAAA,QACE,MAAM;AAAA,QACN,aAAa;AAAA,MACf;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,aAAa;AAAA,MACf;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,aAAa;AAAA,MACf;AAAA,IACF;AAAA,IACA,WAAW,EAAE,WAAW,gBAAgB;AAAA,EAC1C;AACF,CAAC;AAEM,SAAS,uBACd,UACA,YACA,MACA,OACgB;AAChB,QAAM,UAA0B,CAAC;AACjC,QAAM,gBAAgB,SAAS,KAAK;AACpC,QAAM,aAAa,oBAAI,IAAoB;AAC3C,aAAW,eAAe,OAAO,OAAO,aAAa,GAAG;AACtD,eAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,WAAW,GAAG;AACvD,YAAM,KAAK,iBAAiB,IAAI;AAChC,UAAI,CAAC,OAAO,SAAS,EAAE,GAAG;AACxB;AAAA,MACF;AACA,YAAM,QAAQ,WAAW,IAAI,IAAI,KAAK;AACtC,iBAAW,IAAI,MAAM,QAAQ,KAAK;AAAA,IACpC;AAAA,EACF;AACA,aAAW,CAAC,MAAM,KAAK,KAAK,YAAY;AACtC,UAAM,KAAK,iBAAiB,IAAI;AAChC,YAAQ,KAAK;AAAA,MACX,MAAM;AAAA,MACN;AAAA,MACA;AAAA,MACA,YAAY,EAAE,MAAM,MAAM;AAAA,IAC5B,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAEO,SAAS,yBACd,iBACA,gBACA,OACgB;AAChB,QAAM,UAA0B,CAAC;AACjC,QAAM,gBAAgB,gBAAgB,KAAK,OAAO,KAAK,KAAK,CAAC;AAC7D,QAAM,eAAe,eAAe,KAAK,OAAO,KAAK,KAAK,CAAC;AAC3D,QAAM,WAAW,oBAAI,IAAY;AAAA,IAC/B,GAAG,OAAO,KAAK,aAAa;AAAA,IAC5B,GAAG,OAAO,KAAK,YAAY;AAAA,EAC7B,CAAC;AACD,aAAW,QAAQ,UAAU;AAC3B,UAAM,KAAK,iBAAiB,IAAI;AAChC,QAAI,CAAC,OAAO,SAAS,EAAE,GAAG;AACxB;AAAA,IACF;AACA,UAAM,QAAQ,cAAc,IAAI,KAAK;AACrC,UAAM,cAAc,aAAa,IAAI,KAAK;AAC1C,YAAQ,KAAK;AAAA,MACX,MAAM,aAAa;AAAA,MACnB;AAAA,MACA,OAAO;AAAA,MACP,YAAY;AAAA,QACV;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAEO,SAAS,mBACd,UACA,QACgB;AAChB,QAAM,UAA0B,CAAC;AACjC,QAAM,eACJ,OAAO,OAAO,OAAO,WAAW,OAAO,KAAK,OAAO,OAAO,EAAE;AAC9D,aAAW,CAAC,MAAM,MAAM,KAAK,OAAO,QAAQ,SAAS,IAAI,GAAG;AAC1D,UAAM,KAAK,iBAAiB,IAAI;AAChC,QAAI,CAAC,OAAO,SAAS,EAAE,GAAG;AACxB;AAAA,IACF;AACA,WAAO,MAAM,QAAQ,CAAC,MAAM,YAAY;AACtC,YAAM,aAAwC;AAAA,QAC5C,UAAU;AAAA,QACV,MAAM;AAAA,QACN,WAAW,KAAK,cAAc,KAAK,SAAS,QAAQ,OAAO;AAAA,QAC3D,OAAO,KAAK;AAAA,QACZ,gBAAgB,KAAK,sBAAsB;AAAA,QAC3C,oBAAoB,KAAK,mBAAmB;AAAA,MAC9C;AACA,UAAI,OAAO,SAAS,QAAW;AAC7B,mBAAW,YAAY,IAAI,OAAO;AAAA,MACpC;AACA,cAAQ,KAAK;AAAA,QACX,MAAM,aAAa;AAAA,QACnB;AAAA,QACA,OAAO,KAAK;AAAA,QACZ;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAEO,SAAS,sBACd,UACA,OACgB;AAChB,QAAM,UAA0B,CAAC;AACjC,aAAW,CAAC,YAAY,MAAM,KAAK,OAAO,QAAQ,QAAQ,GAAG;AAC3D,UAAM,KAAK,iBAAiB,UAAU;AACtC,QAAI,CAAC,OAAO,SAAS,EAAE,GAAG;AACxB;AAAA,IACF;AACA,WAAO,OAAO,QAAQ,CAAC,UAAU,WAAW;AAC1C,cAAQ,KAAK;AAAA,QACX,MAAM,aAAa;AAAA,QACnB;AAAA,QACA,OAAO;AAAA,QACP,YAAY;AAAA,UACV;AAAA,UACA;AAAA,UACA,YAAY,OAAO;AAAA,UACnB,eAAe,OAAO,QAAQ,IAAI,WAAW,OAAO,QAAQ;AAAA,QAC9D;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAEA,SAAS,gBAAgB,UAAkB,QAAwB;AACjE,QAAM,MAAM,GAAG,QAAQ,IAAI,MAAM;AACjC,MAAI,OAAO,SAAS,YAAY;AAC9B,WAAO,SAAS,KAAK,GAAG,CAAC;AAAA,EAC3B;AACA,QAAM,aACJ,WAGA;AACF,MAAI,YAAY;AACd,WAAO,SAAS,WAAW,KAAK,GAAG,EAAE,SAAS,QAAQ,CAAC;AAAA,EACzD;AACA,QAAM,IAAI,MAAM,6CAA6C;AAC/D;AAEA,SAAS,WAAW,QAAyC;AAC3D,SAAO,WAAW,OAAO,oBAAoB;AAC/C;AAEO,IAAM,KAAK;AAEX,IAAM,oBAAN,MAAM,2BAA0B,cAGrC;AAAA,EACA,OAAgB,KAAK;AAAA,EAErB,OAAgB,YAAY;AAAA,EAE5B,OAAgB,UAAU,qBAAqB,iBAAiB;AAAA,EAEhE,OAAgB,OAAsB;AAAA,EAEtC,OAAO,OAAO,OAAgB,KAA2C;AACvE,UAAM,SAAS,aAAa,MAAM,KAAK;AACvC,WAAO,IAAI;AAAA,MACT;AAAA,QACE,WAAW,OAAO;AAAA,QAClB,QAAQ,OAAO;AAAA,QACf,QAAQ,OAAO;AAAA,QACf,SAAS,OAAO;AAAA,QAChB,gBAAgB,OAAO;AAAA,QACvB,iBAAiB,OAAO;AAAA,QACxB,cAAc,OAAO;AAAA,MACvB;AAAA,MACA;AAAA,QACE,UAAU,OAAO;AAAA,QACjB,QAAQ,OAAO;AAAA,MACjB;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA,EAES,KAAK;AAAA,EACI,cAAc;AAAA,EAEhC,IAAY,UAAkB;AAC5B,WAAO,WAAW,WAAW,KAAK,SAAS,MAAM,CAAC;AAAA,EACpD;AAAA,EAEQ,cAAsC;AAC5C,WAAO;AAAA,MACL,eAAe,gBAAgB,KAAK,MAAM,UAAU,KAAK,MAAM,MAAM;AAAA,MACrE,QAAQ;AAAA,MACR,cAAc,mBAAmB,UAAU;AAAA,IAC7C;AAAA,EACF;AAAA,EAEQ,WAAW,OAAuC;AACxD,UAAM,SAAS,IAAI,gBAAgB;AAAA,MACjC,YAAY,KAAK,SAAS;AAAA,MAC1B,GAAG;AAAA,IACL,CAAC;AACD,WAAO,OAAO,SAAS;AAAA,EACzB;AAAA,EAEA,MAAc,gBACZ,UACA,QACA,QAC+B;AAC/B,UAAM,MAAM,GAAG,KAAK,OAAO,iBAAiB,KAAK,WAAW,MAAM,CAAC;AACnE,UAAM,MAAM,MAAM,KAAK,IAAa,KAAK;AAAA,MACvC;AAAA,MACA,SAAS,KAAK,YAAY;AAAA,MAC1B;AAAA,IACF,CAAC;AACD,WAAO,mBAAmB,MAAM,IAAI,IAAI;AAAA,EAC1C;AAAA,EAEA,MAAc,UACZ,UACA,OACA,MACA,OACA,QAC+B;AAC/B,UAAM,MAAM,GAAG,KAAK,OAAO,WAAW,KAAK,WAAW;AAAA,MACpD,OAAO,KAAK,UAAU,CAAC,KAAK,CAAC;AAAA,MAC7B,WAAW,MAAM;AAAA,MACjB,SAAS,MAAM;AAAA,MACf;AAAA,MACA,MAAM;AAAA,IACR,CAAC,CAAC;AACF,UAAM,MAAM,MAAM,KAAK,IAAa,KAAK;AAAA,MACvC;AAAA,MACA,SAAS,KAAK,YAAY;AAAA,MAC1B;AAAA,IACF,CAAC;AACD,WAAO,mBAAmB,MAAM,IAAI,IAAI;AAAA,EAC1C;AAAA,EAEA,MAAc,UACZ,UACA,OACA,QACyB;AACzB,UAAM,MAAM,GAAG,KAAK,OAAO,YAAY,KAAK,WAAW;AAAA,MACrD,WAAW,OAAO,QAAQ;AAAA,MAC1B,WAAW,MAAM;AAAA,MACjB,SAAS,MAAM;AAAA,MACf,MAAM;AAAA,IACR,CAAC,CAAC;AACF,UAAM,MAAM,MAAM,KAAK,IAAa,KAAK;AAAA,MACvC,UAAU;AAAA,MACV,SAAS,KAAK,YAAY;AAAA,MAC1B;AAAA,IACF,CAAC;AACD,WAAO,aAAa,MAAM,IAAI,IAAI;AAAA,EACpC;AAAA,EAEA,MAAc,aACZ,OACA,OACA,QAC4B;AAC5B,UAAM,MAAM,GAAG,KAAK,OAAO,cAAc,KAAK,WAAW;AAAA,MACvD,WAAW,MAAM;AAAA,MACjB,SAAS,MAAM;AAAA,MACf,gBAAgB;AAAA,MAChB,MAAM;AAAA,MACN,YAAY;AAAA,MACZ;AAAA,IACF,CAAC,CAAC;AACF,UAAM,MAAM,MAAM,KAAK,IAAa,KAAK;AAAA,MACvC,UAAU;AAAA,MACV,SAAS,KAAK,YAAY;AAAA,MAC1B;AAAA,IACF,CAAC;AACD,WAAO,gBAAgB,MAAM,IAAI,IAAI;AAAA,EACvC;AAAA,EAEQ,yBAA6C;AACnD,QAAI,KAAK,SAAS,oBAAoB,QAAW;AAC/C,aAAO,KAAK,SAAS;AAAA,IACvB;AACA,UAAM,QAAQ,KAAK,SAAS,SAAS,CAAC;AACtC,WAAO;AAAA,EACT;AAAA,EAEA,MAAc,mBACZ,OACA,OACA,SACA,QACe;AACf,UAAM,aAAa,aAAa,KAAK;AACrC,UAAM,QAAQ,KAAK,uBAAuB;AAC1C,QAAI,UAAU,QAAW;AACvB,YAAM,QAAQ,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC;AACjD;AAAA,IACF;AACA,UAAM,WAAW,MAAM,KAAK;AAAA,MAC1B;AAAA,MACA;AAAA,MACA,WAAW,KAAK;AAAA,MAChB;AAAA,MACA;AAAA,IACF;AACA,UAAM,UAAU;AAAA,MACd;AAAA,MACA;AAAA,MACA,WAAW,KAAK;AAAA,MAChB;AAAA,IACF;AACA,UAAM,gBAAgB,uBAAuB,KAAK;AAClD,UAAM,QAAQ,QAAQ,SAAS,EAAE,OAAO,CAAC,UAAU,GAAG,cAAc,CAAC;AAAA,EACvE;AAAA,EAEA,MAAc,qBACZ,OACA,SACA,QACe;AACf,UAAM,aAAa,aAAa;AAChC,UAAM,SAAS,KAAK,SAAS,UAAU,CAAC;AACxC,QAAI,OAAO,WAAW,GAAG;AACvB,YAAM,QAAQ,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC;AACjD;AAAA,IACF;AACA,UAAM,UAA0B,CAAC;AACjC,eAAW,SAAS,QAAQ;AAC1B,UAAI,QAAQ,SAAS;AACnB,cAAM,IAAI,MAAM,SAAS;AAAA,MAC3B;AACA,YAAM,CAAC,iBAAiB,cAAc,IAAI,MAAM,QAAQ,IAAI;AAAA,QAC1D,KAAK;AAAA,UACH;AAAA,UACA;AAAA,YACE;AAAA,YACA,WAAW,MAAM;AAAA,YACjB,SAAS,MAAM;AAAA,YACf,MAAM;AAAA,YACN,MAAM;AAAA,UACR;AAAA,UACA;AAAA,QACF;AAAA,QACA,KAAK;AAAA,UACH;AAAA,UACA;AAAA,YACE;AAAA,YACA,WAAW,MAAM;AAAA,YACjB,SAAS,MAAM;AAAA,YACf,MAAM;AAAA,YACN,MAAM;AAAA,UACR;AAAA,UACA;AAAA,QACF;AAAA,MACF,CAAC;AACD,cAAQ;AAAA,QACN,GAAG,yBAAyB,iBAAiB,gBAAgB,KAAK;AAAA,MACpE;AAAA,IACF;AACA,UAAM,gBAAgB,uBAAuB,KAAK;AAClD,UAAM,QAAQ,QAAQ,SAAS,EAAE,OAAO,CAAC,UAAU,GAAG,cAAc,CAAC;AAAA,EACvE;AAAA,EAEA,MAAc,eACZ,OACA,SACA,QACe;AACf,UAAM,aAAa,aAAa;AAChC,UAAM,UAAU,KAAK,SAAS,WAAW,CAAC;AAC1C,QAAI,QAAQ,WAAW,GAAG;AACxB,YAAM,QAAQ,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC;AACjD;AAAA,IACF;AACA,UAAM,UAA0B,CAAC;AACjC,eAAW,UAAU,SAAS;AAC5B,UAAI,QAAQ,SAAS;AACnB,cAAM,IAAI,MAAM,SAAS;AAAA,MAC3B;AACA,YAAM,WAAW,MAAM,KAAK,UAAU,OAAO,IAAI,OAAO,MAAM;AAC9D,cAAQ,KAAK,GAAG,mBAAmB,UAAU,MAAM,CAAC;AAAA,IACtD;AACA,UAAM,gBAAgB,uBAAuB,KAAK;AAClD,UAAM,QAAQ,QAAQ,SAAS,EAAE,OAAO,CAAC,UAAU,GAAG,cAAc,CAAC;AAAA,EACvE;AAAA,EAEA,MAAc,kBACZ,OACA,SACA,QACe;AACf,UAAM,aAAa,aAAa;AAChC,UAAM,QAAQ,KAAK,SAAS;AAC5B,QAAI,UAAU,QAAW;AACvB,YAAM,QAAQ,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC;AACjD;AAAA,IACF;AACA,UAAM,WAAW,MAAM,KAAK,aAAa,OAAO,OAAO,MAAM;AAC7D,UAAM,UAAU,sBAAsB,UAAU,KAAK;AACrD,UAAM,gBAAgB,uBAAuB,KAAK;AAClD,UAAM,QAAQ,QAAQ,SAAS,EAAE,OAAO,CAAC,UAAU,GAAG,cAAc,CAAC;AAAA,EACvE;AAAA,EAEA,MAAM,KACJ,SACA,SACA,QACqB;AACrB,UAAM,eAAe,KAAK,SAAS,gBAAgB;AACnD,UAAM,SAAS,qBAAqB,QAAQ,MAAM,IAC9C,QAAQ,SACR;AACJ,UAAM,YAAY,QAAQ,aAAa,aAAa,SAAS,YAAY;AAEzE,UAAM,YAAY,SAAS,YAAY,QAAQ,OAAO,KAAK,IAAI;AAC/D,UAAM,WAAW,aAAa,IAAI,YAAY;AAC9C,UAAM,YAAY,QAAQ;AAE1B,aAAS,IAAI,UAAU,IAAI,YAAY,QAAQ,KAAK;AAClD,YAAM,QAAQ,YAAY,CAAC;AAC3B,UAAI,QAAQ,SAAS;AACnB,eAAO,EAAE,MAAM,OAAO,QAAQ,EAAE,OAAO,UAAU,EAAE;AAAA,MACrD;AACA,UACE,aACA,UAAU,OAAO,KACjB,CAAC,UAAU,IAAI,aAAa,KAAK,CAAC,GAClC;AACA;AAAA,MACF;AACA,YAAM,aAAa,KAAK,IAAI;AAC5B,UAAI;AACF,YAAI,UAAU,SAAS,UAAU,SAAS,UAAU,OAAO;AACzD,gBAAM,KAAK,mBAAmB,OAAO,WAAW,SAAS,MAAM;AAAA,QACjE,WAAW,UAAU,kBAAkB;AACrC,gBAAM,KAAK,qBAAqB,WAAW,SAAS,MAAM;AAAA,QAC5D,WAAW,UAAU,kBAAkB;AACrC,gBAAM,KAAK,eAAe,WAAW,SAAS,MAAM;AAAA,QACtD,OAAO;AACL,gBAAM,KAAK,kBAAkB,WAAW,SAAS,MAAM;AAAA,QACzD;AAAA,MACF,SAAS,KAAK;AACZ,YACE,QAAQ,WACP,eAAe,SAAS,IAAI,SAAS,cACtC;AACA,iBAAO,EAAE,MAAM,OAAO,QAAQ,EAAE,OAAO,UAAU,EAAE;AAAA,QACrD;AACA,aAAK,OAAO,KAAK,qBAAqB;AAAA,UACpC,UAAU;AAAA,UACV,MAAM;AAAA,UACN,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,QACxD,CAAC;AACD,eAAO;AAAA,UACL,MAAM;AAAA,UACN,QAAQ,EAAE,OAAO,UAAU;AAAA,UAC3B,gBAAgB;AAAA,QAClB;AAAA,MACF;AACA,WAAK,OAAO,KAAK,iBAAiB;AAAA,QAChC,UAAU;AAAA,QACV,OAAO;AAAA,QACP,OAAO;AAAA,QACP,aAAa,KAAK,IAAI,IAAI;AAAA,MAC5B,CAAC;AAAA,IACH;AAEA,WAAO,EAAE,MAAM,KAAK;AAAA,EACtB;AACF;;;AC/6BA,IAAO,gBAAQ;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rawdash/connector-mixpanel",
3
- "version": "0.28.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.28.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-test-utils": "0.0.10",
35
- "@rawdash/connector-shared": "0.3.1"
34
+ "@rawdash/connector-shared": "0.3.1",
35
+ "@rawdash/connector-test-utils": "0.0.10"
36
36
  },
37
37
  "scripts": {
38
38
  "build": "tsup",