@funnelsgrove/cli 0.1.262 → 0.1.264

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
@@ -123,6 +123,7 @@ Analytics workflow:
123
123
  fgrove analytics conversions --project claimbee --funnel claimbee-ios --date 2026-06-11
124
124
  fgrove analytics funnel-path --project claimbee --funnel claimbee-ios --date 2026-06-11
125
125
  fgrove analytics transitions --project claimbee --funnel claimbee-ios --date 2026-06-11
126
+ fgrove analytics versions --project claimbee --funnel claimbee-ios
126
127
  fgrove analytics cohort --project claimbee --date 2026-06-11
127
128
  ```
128
129
 
@@ -132,7 +133,16 @@ Each analytics command accepts `--format table|json`, `--out <path>`, `--workspa
132
133
  fgrove analytics conversions --project claimbee --funnel claimbee-ios --date 2026-06-11 --format json --out conversions.json
133
134
  ```
134
135
 
135
- `conversions` downloads the one-day conversion totals, primary conversion metrics, full funnel path rows, and step transitions. `funnel-path` focuses on the ordered path report. `transitions` focuses on step-by-step advanced/drop-off counts. `cohort` downloads synced marketing cohort economics for the day. If the requested day has no synced data, or cohort source data is incomplete, the CLI exits non-zero with a human-readable explanation.
136
+ `conversions` downloads conversion totals, primary conversion metrics, full funnel path rows, and step transitions. `funnel-path` focuses on the ordered path report. `transitions` focuses on step-by-step advanced/drop-off counts. `cohort` downloads synced marketing cohort economics for the day. If the requested period has no synced data, or cohort source data is incomplete, the CLI exits non-zero with a human-readable explanation.
137
+
138
+ `conversions`, `funnel-path`, and `transitions` report one day with `--date`, or a range with `--from` and `--to` (inclusive, totals for the whole range). They report the current Journey Version by default, like Funnel Analytics in the console. Traffic from before the last structural change belongs to an older Journey Version: list the versions with `versions`, then pass `--journey-version <id>`:
139
+
140
+ ```bash
141
+ fgrove analytics versions --project claimbee --funnel claimbee-ios
142
+ fgrove analytics transitions --project claimbee --funnel claimbee-ios --from 2026-06-01 --to 2026-06-14 --journey-version <journey-version-id>
143
+ ```
144
+
145
+ `versions` lists each Journey Version newest first with its id, start time, active days, and visitors on those days. Active days are dates in `--timezone` (the API default, America/Los_Angeles, when omitted); the day a version replaced its predecessor belongs to both, and each version counts only its own visitors. An explicit version reports its own metrics and its own step structure. `--journey-version all` counts every version and orders `funnel-path` by the current published flow, so steps that exist only in older versions appear in `transitions` but not in `funnel-path`. JSON reports include `from`, `to`, and a `journeyVersion` object (`id`, `seq`, `label`, `startedAt`, `isCurrent`); `date` is set only for a one-day report. Table titles end with the reported version, for example `Journey 2 (current)`. The `overview` block of `conversions` always covers all Journey Versions.
136
146
 
137
147
  The package also keeps the longer `funnelsgrove` command as a compatibility alias.
138
148
 
@@ -53,20 +53,8 @@ export type AnalyticsStepDropoffResponse = {
53
53
  slug: string;
54
54
  };
55
55
  version: {
56
- selected: {
57
- id: string;
58
- seq: number;
59
- message: string | null;
60
- publishedAt: string | null;
61
- isCurrent: boolean;
62
- } | null;
63
- options: Array<{
64
- id: string;
65
- seq: number;
66
- message: string | null;
67
- publishedAt: string | null;
68
- isCurrent: boolean;
69
- }>;
56
+ selected: AnalyticsJourneyVersionOption | null;
57
+ options: AnalyticsJourneyVersionOption[];
70
58
  };
71
59
  totals: Record<string, number>;
72
60
  metrics: Array<{
@@ -83,6 +71,30 @@ export type AnalyticsStepDropoffResponse = {
83
71
  steps: AnalyticsStepTransition[];
84
72
  flow: AnalyticsFunnelFlow | null;
85
73
  };
74
+ export type AnalyticsJourneyVersionOption = {
75
+ id: string;
76
+ seq: number;
77
+ message: string | null;
78
+ publishedAt: string | null;
79
+ isCurrent: boolean;
80
+ };
81
+ export type AnalyticsReportJourneyVersion = {
82
+ id: string;
83
+ seq: number | null;
84
+ label: string;
85
+ startedAt: string | null;
86
+ isCurrent: boolean;
87
+ };
88
+ export type AnalyticsJourneyVersionRow = {
89
+ id: string;
90
+ seq: number;
91
+ label: string;
92
+ isCurrent: boolean;
93
+ startedAt: string | null;
94
+ activeFrom: string | null;
95
+ activeTo: string | null;
96
+ visitors: number | null;
97
+ };
86
98
  export type AnalyticsStepTransition = {
87
99
  stepId: string;
88
100
  stepName: string | null;
@@ -153,11 +165,32 @@ type ReportContext = {
153
165
  workspaceId: string;
154
166
  project: AnalyticsProjectSummary;
155
167
  };
156
- export declare const parseAnalyticsDate: (value: string) => ParsedAnalyticsDate;
168
+ type FunnelReportContext = {
169
+ period: AnalyticsPeriod;
170
+ journeyVersion: string;
171
+ workspaceId: string;
172
+ project: AnalyticsProjectSummary;
173
+ stepDropoff: AnalyticsStepDropoffResponse;
174
+ };
175
+ export declare const parseAnalyticsDate: (value: string, flag?: string) => ParsedAnalyticsDate;
176
+ export type AnalyticsPeriod = {
177
+ date: string | null;
178
+ from: string;
179
+ to: string;
180
+ label: string;
181
+ };
182
+ export declare const parseAnalyticsPeriod: (input: {
183
+ date?: string;
184
+ from?: string;
185
+ to?: string;
186
+ }) => AnalyticsPeriod & {
187
+ fromIso: string;
188
+ toIso: string;
189
+ };
157
190
  export declare const normalizeAnalyticsOutputFormat: (value: string | undefined) => AnalyticsOutputFormat;
158
191
  export declare const assertHasConversionData: (date: string, overview: AnalyticsOverviewResponse, stepDropoff: AnalyticsStepDropoffResponse) => void;
159
192
  export declare const assertHasTransitionData: (date: string, stepDropoff: AnalyticsStepDropoffResponse) => void;
160
- export declare const assertHasFunnelPathData: (date: string, stepDropoff: AnalyticsStepDropoffResponse, publishedFunnelFlow: PublishedFunnelFlowResponse) => void;
193
+ export declare const assertHasFunnelPathData: (date: string, stepDropoff: AnalyticsStepDropoffResponse, pathFlow: AnalyticsFunnelFlow | null) => void;
161
194
  export declare const assertHasCohortData: (date: string, report: MarketingCohortPerformanceReport) => void;
162
195
  export declare const buildTransitionRows: (stepDropoff: AnalyticsStepDropoffResponse) => {
163
196
  stepId: string;
@@ -168,7 +201,7 @@ export declare const buildTransitionRows: (stepDropoff: AnalyticsStepDropoffResp
168
201
  dropoffs: number;
169
202
  dropoffRate: number;
170
203
  }[];
171
- export declare const buildFunnelPathRows: (stepDropoff: AnalyticsStepDropoffResponse, publishedFunnelFlow: PublishedFunnelFlowResponse) => {
204
+ export declare const buildFunnelPathRows: (stepDropoff: AnalyticsStepDropoffResponse, pathFlow: AnalyticsFunnelFlow | null) => {
172
205
  stepId: string;
173
206
  path: string;
174
207
  label: string;
@@ -178,20 +211,11 @@ export declare const buildFunnelPathRows: (stepDropoff: AnalyticsStepDropoffResp
178
211
  dropoffs: number;
179
212
  dropoffRate: number;
180
213
  }[];
181
- export declare const buildConversionReportPayload: (input: ReportContext & {
214
+ export declare const journeyVersionLabel: (version: Pick<AnalyticsJourneyVersionOption, "seq">) => string;
215
+ export declare const buildConversionReportPayload: (input: FunnelReportContext & {
182
216
  overview: AnalyticsOverviewResponse;
183
- stepDropoff: AnalyticsStepDropoffResponse;
184
- publishedFunnelFlow: PublishedFunnelFlowResponse;
217
+ pathFlow: AnalyticsFunnelFlow | null;
185
218
  }) => {
186
- generatedAt: string;
187
- date: string;
188
- workspaceId: string;
189
- project: AnalyticsProjectSummary;
190
- funnel: {
191
- id: string;
192
- name: string;
193
- slug: string;
194
- };
195
219
  overview: AnalyticsOverviewResponse;
196
220
  totals: Record<string, number>;
197
221
  metrics: {
@@ -224,13 +248,10 @@ export declare const buildConversionReportPayload: (input: ReportContext & {
224
248
  dropoffs: number;
225
249
  dropoffRate: number;
226
250
  }[];
227
- };
228
- export declare const buildFunnelPathReportPayload: (input: ReportContext & {
229
- stepDropoff: AnalyticsStepDropoffResponse;
230
- publishedFunnelFlow: PublishedFunnelFlowResponse;
231
- }) => {
232
251
  generatedAt: string;
233
- date: string;
252
+ date: string | null;
253
+ from: string;
254
+ to: string;
234
255
  workspaceId: string;
235
256
  project: AnalyticsProjectSummary;
236
257
  funnel: {
@@ -238,6 +259,11 @@ export declare const buildFunnelPathReportPayload: (input: ReportContext & {
238
259
  name: string;
239
260
  slug: string;
240
261
  };
262
+ journeyVersion: AnalyticsReportJourneyVersion | null;
263
+ };
264
+ export declare const buildFunnelPathReportPayload: (input: FunnelReportContext & {
265
+ pathFlow: AnalyticsFunnelFlow | null;
266
+ }) => {
241
267
  funnelPath: {
242
268
  stepId: string;
243
269
  path: string;
@@ -248,12 +274,10 @@ export declare const buildFunnelPathReportPayload: (input: ReportContext & {
248
274
  dropoffs: number;
249
275
  dropoffRate: number;
250
276
  }[];
251
- };
252
- export declare const buildTransitionReportPayload: (input: ReportContext & {
253
- stepDropoff: AnalyticsStepDropoffResponse;
254
- }) => {
255
277
  generatedAt: string;
256
- date: string;
278
+ date: string | null;
279
+ from: string;
280
+ to: string;
257
281
  workspaceId: string;
258
282
  project: AnalyticsProjectSummary;
259
283
  funnel: {
@@ -261,6 +285,9 @@ export declare const buildTransitionReportPayload: (input: ReportContext & {
261
285
  name: string;
262
286
  slug: string;
263
287
  };
288
+ journeyVersion: AnalyticsReportJourneyVersion | null;
289
+ };
290
+ export declare const buildTransitionReportPayload: (input: FunnelReportContext) => {
264
291
  transitions: {
265
292
  stepId: string;
266
293
  step: string;
@@ -270,6 +297,18 @@ export declare const buildTransitionReportPayload: (input: ReportContext & {
270
297
  dropoffs: number;
271
298
  dropoffRate: number;
272
299
  }[];
300
+ generatedAt: string;
301
+ date: string | null;
302
+ from: string;
303
+ to: string;
304
+ workspaceId: string;
305
+ project: AnalyticsProjectSummary;
306
+ funnel: {
307
+ id: string;
308
+ name: string;
309
+ slug: string;
310
+ };
311
+ journeyVersion: AnalyticsReportJourneyVersion | null;
273
312
  };
274
313
  export declare const buildCohortReportPayload: (input: ReportContext & {
275
314
  cohort: MarketingCohortPerformanceReport;
@@ -285,21 +324,34 @@ export declare const buildCohortReportPayload: (input: ReportContext & {
285
324
  } | null;
286
325
  };
287
326
  export declare const formatAnalyticsJson: (payload: unknown) => string;
288
- export declare const formatTransitionsTable: (input: {
289
- date: string;
290
- stepDropoff: AnalyticsStepDropoffResponse;
327
+ type FunnelReportTableInput = Pick<FunnelReportContext, 'period' | 'journeyVersion' | 'stepDropoff'>;
328
+ export declare const formatTransitionsTable: (input: FunnelReportTableInput) => string;
329
+ export declare const formatFunnelPathTable: (input: FunnelReportTableInput & {
330
+ pathFlow: AnalyticsFunnelFlow | null;
291
331
  }) => string;
292
- export declare const formatFunnelPathTable: (input: {
293
- date: string;
294
- stepDropoff: AnalyticsStepDropoffResponse;
295
- publishedFunnelFlow: PublishedFunnelFlowResponse;
296
- }) => string;
297
- export declare const formatConversionsTable: (input: {
298
- date: string;
332
+ export declare const formatConversionsTable: (input: FunnelReportTableInput & {
299
333
  overview: AnalyticsOverviewResponse;
300
- stepDropoff: AnalyticsStepDropoffResponse;
301
- publishedFunnelFlow: PublishedFunnelFlowResponse;
334
+ pathFlow: AnalyticsFunnelFlow | null;
302
335
  }) => string;
336
+ type JourneyVersionsReport = {
337
+ funnel: AnalyticsStepDropoffResponse['funnel'];
338
+ versions: AnalyticsJourneyVersionRow[];
339
+ };
340
+ export declare const buildJourneyVersionsReportPayload: (input: JourneyVersionsReport & {
341
+ workspaceId: string;
342
+ project: AnalyticsProjectSummary;
343
+ }) => {
344
+ generatedAt: string;
345
+ workspaceId: string;
346
+ project: AnalyticsProjectSummary;
347
+ funnel: {
348
+ id: string;
349
+ name: string;
350
+ slug: string;
351
+ };
352
+ versions: AnalyticsJourneyVersionRow[];
353
+ };
354
+ export declare const formatJourneyVersionsTable: (input: JourneyVersionsReport) => string;
303
355
  export declare const formatCohortTable: (input: {
304
356
  date: string;
305
357
  cohort: MarketingCohortPerformanceReport;
@@ -1,8 +1,8 @@
1
- export const parseAnalyticsDate = (value) => {
1
+ export const parseAnalyticsDate = (value, flag = '--date') => {
2
2
  const trimmed = value.trim();
3
3
  const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(trimmed);
4
4
  if (!match) {
5
- throw new Error('Expected --date in YYYY-MM-DD format.');
5
+ throw new Error(`Expected ${flag} in YYYY-MM-DD format.`);
6
6
  }
7
7
  const year = Number(match[1]);
8
8
  const month = Number(match[2]);
@@ -13,7 +13,7 @@ export const parseAnalyticsDate = (value) => {
13
13
  parsed.getUTCMonth() + 1 === month &&
14
14
  parsed.getUTCDate() === day;
15
15
  if (!isRealDate) {
16
- throw new Error('Expected --date to be a real calendar date.');
16
+ throw new Error(`Expected ${flag} to be a real calendar date.`);
17
17
  }
18
18
  return {
19
19
  date: trimmed,
@@ -21,6 +21,32 @@ export const parseAnalyticsDate = (value) => {
21
21
  toIso: `${trimmed}T23:59:59.999Z`,
22
22
  };
23
23
  };
24
+ export const parseAnalyticsPeriod = (input) => {
25
+ if (input.date !== undefined) {
26
+ if (input.from !== undefined || input.to !== undefined) {
27
+ throw new Error('Use either --date or --from with --to, not both.');
28
+ }
29
+ const day = parseAnalyticsDate(input.date);
30
+ return { date: day.date, from: day.date, to: day.date, label: day.date, fromIso: day.fromIso, toIso: day.toIso };
31
+ }
32
+ if (input.from === undefined || input.to === undefined) {
33
+ throw new Error('Pass --date, or --from together with --to.');
34
+ }
35
+ const from = parseAnalyticsDate(input.from, '--from');
36
+ const to = parseAnalyticsDate(input.to, '--to');
37
+ if (from.date > to.date) {
38
+ throw new Error('--from must be on or before --to.');
39
+ }
40
+ const date = from.date === to.date ? from.date : null;
41
+ return {
42
+ date,
43
+ from: from.date,
44
+ to: to.date,
45
+ label: date ?? `${from.date}..${to.date}`,
46
+ fromIso: from.fromIso,
47
+ toIso: to.toIso,
48
+ };
49
+ };
24
50
  export const normalizeAnalyticsOutputFormat = (value) => {
25
51
  const normalized = (value || 'table').trim().toLowerCase();
26
52
  if (normalized === 'table' || normalized === 'json') {
@@ -40,6 +66,9 @@ const hasPositiveNumber = (value) => {
40
66
  }
41
67
  return false;
42
68
  };
69
+ const olderJourneyVersionHint = (stepDropoff) => (stepDropoff.version.selected?.isCurrent && stepDropoff.version.options.length > 1
70
+ ? ' Traffic on these dates may belong to an older journey version: list them with `fgrove analytics versions` and pass --journey-version.'
71
+ : '');
43
72
  export const assertHasConversionData = (date, overview, stepDropoff) => {
44
73
  if (hasPositiveNumber(overview.totals) ||
45
74
  hasPositiveNumber(overview.byDay) ||
@@ -47,19 +76,19 @@ export const assertHasConversionData = (date, overview, stepDropoff) => {
47
76
  hasPositiveNumber(stepDropoff.steps)) {
48
77
  return;
49
78
  }
50
- throw new Error(`No synced conversion data found for ${date}. Analytics may not have synced yet, or the funnel had no tracked traffic that day.`);
79
+ throw new Error(`No synced conversion data found for ${date}. Analytics may not have synced yet, or the funnel had no tracked traffic in that period.${olderJourneyVersionHint(stepDropoff)}`);
51
80
  };
52
81
  export const assertHasTransitionData = (date, stepDropoff) => {
53
82
  if (stepDropoff.steps.length > 0 && hasPositiveNumber(stepDropoff.steps)) {
54
83
  return;
55
84
  }
56
- throw new Error(`No synced step transition data found for ${date}. Analytics may not have synced yet, or the funnel had no tracked step traffic that day.`);
85
+ throw new Error(`No synced step transition data found for ${date}. Analytics may not have synced yet, or the funnel had no tracked step traffic in that period.${olderJourneyVersionHint(stepDropoff)}`);
57
86
  };
58
- export const assertHasFunnelPathData = (date, stepDropoff, publishedFunnelFlow) => {
59
- if (buildFunnelPathRows(stepDropoff, publishedFunnelFlow).length > 0) {
87
+ export const assertHasFunnelPathData = (date, stepDropoff, pathFlow) => {
88
+ if (buildFunnelPathRows(stepDropoff, pathFlow).length > 0) {
60
89
  return;
61
90
  }
62
- throw new Error(`No synced full funnel path data found for ${date}. Analytics may not have synced yet, or the funnel had no tracked path traffic that day.`);
91
+ throw new Error(`No synced full funnel path data found for ${date}. Analytics may not have synced yet, or the funnel had no tracked path traffic in that period.${olderJourneyVersionHint(stepDropoff)}`);
63
92
  };
64
93
  export const assertHasCohortData = (date, report) => {
65
94
  if (report.sourceHealth && !report.sourceHealth.complete) {
@@ -96,12 +125,12 @@ export const buildTransitionRows = (stepDropoff) => [...stepDropoff.steps]
96
125
  dropoffs: step.dropoffs,
97
126
  dropoffRate: step.dropoffRate,
98
127
  }));
99
- export const buildFunnelPathRows = (stepDropoff, publishedFunnelFlow) => {
100
- if (publishedFunnelFlow.status !== 'ready' || !publishedFunnelFlow.flow) {
128
+ export const buildFunnelPathRows = (stepDropoff, pathFlow) => {
129
+ if (!pathFlow) {
101
130
  return [];
102
131
  }
103
132
  const transitionsByStepId = new Map(stepDropoff.steps.map((step) => [step.stepId, step]));
104
- return [...publishedFunnelFlow.flow.steps]
133
+ return [...pathFlow.steps]
105
134
  .sort((left, right) => left.stepOrder - right.stepOrder || left.stepId.localeCompare(right.stepId))
106
135
  .map((flowStep) => {
107
136
  const step = transitionsByStepId.get(flowStep.stepId);
@@ -118,32 +147,55 @@ export const buildFunnelPathRows = (stepDropoff, publishedFunnelFlow) => {
118
147
  };
119
148
  });
120
149
  };
121
- export const buildConversionReportPayload = (input) => ({
150
+ export const journeyVersionLabel = (version) => `Journey ${version.seq}`;
151
+ const describeReportJourneyVersion = (input) => {
152
+ if (input.journeyVersion === 'all') {
153
+ return { id: 'all', seq: null, label: 'All journey versions', startedAt: null, isCurrent: false };
154
+ }
155
+ const selected = input.stepDropoff.version.selected;
156
+ return selected
157
+ ? {
158
+ id: selected.id,
159
+ seq: selected.seq,
160
+ label: journeyVersionLabel(selected),
161
+ startedAt: selected.publishedAt,
162
+ isCurrent: selected.isCurrent,
163
+ }
164
+ : null;
165
+ };
166
+ const buildFunnelReportHeader = (input) => ({
122
167
  generatedAt: new Date().toISOString(),
123
- date: input.date,
168
+ date: input.period.date,
169
+ from: input.period.from,
170
+ to: input.period.to,
124
171
  workspaceId: input.workspaceId,
125
172
  project: input.project,
126
173
  funnel: input.stepDropoff.funnel,
174
+ journeyVersion: describeReportJourneyVersion(input),
175
+ });
176
+ const formatFunnelReportTitle = (title, input) => {
177
+ const version = describeReportJourneyVersion(input);
178
+ return [
179
+ title,
180
+ input.period.label,
181
+ input.stepDropoff.funnel.name,
182
+ ...(version ? [version.isCurrent ? `${version.label} (current)` : version.label] : []),
183
+ ].join('\t');
184
+ };
185
+ export const buildConversionReportPayload = (input) => ({
186
+ ...buildFunnelReportHeader(input),
127
187
  overview: input.overview,
128
188
  totals: input.stepDropoff.totals,
129
189
  metrics: input.stepDropoff.metrics,
130
- funnelPath: buildFunnelPathRows(input.stepDropoff, input.publishedFunnelFlow),
190
+ funnelPath: buildFunnelPathRows(input.stepDropoff, input.pathFlow),
131
191
  transitions: buildTransitionRows(input.stepDropoff),
132
192
  });
133
193
  export const buildFunnelPathReportPayload = (input) => ({
134
- generatedAt: new Date().toISOString(),
135
- date: input.date,
136
- workspaceId: input.workspaceId,
137
- project: input.project,
138
- funnel: input.stepDropoff.funnel,
139
- funnelPath: buildFunnelPathRows(input.stepDropoff, input.publishedFunnelFlow),
194
+ ...buildFunnelReportHeader(input),
195
+ funnelPath: buildFunnelPathRows(input.stepDropoff, input.pathFlow),
140
196
  });
141
197
  export const buildTransitionReportPayload = (input) => ({
142
- generatedAt: new Date().toISOString(),
143
- date: input.date,
144
- workspaceId: input.workspaceId,
145
- project: input.project,
146
- funnel: input.stepDropoff.funnel,
198
+ ...buildFunnelReportHeader(input),
147
199
  transitions: buildTransitionRows(input.stepDropoff),
148
200
  });
149
201
  export const buildCohortReportPayload = (input) => ({
@@ -157,7 +209,7 @@ export const buildCohortReportPayload = (input) => ({
157
209
  export const formatAnalyticsJson = (payload) => `${JSON.stringify(payload, null, 2)}\n`;
158
210
  export const formatTransitionsTable = (input) => {
159
211
  const lines = [
160
- `Step transitions\t${input.date}\t${input.stepDropoff.funnel.name}`,
212
+ formatFunnelReportTitle('Step transitions', input),
161
213
  'step\tnextStep\tentrants\tadvanced\tdropoffs\tdropoffRate',
162
214
  ];
163
215
  for (const row of buildTransitionRows(input.stepDropoff)) {
@@ -174,10 +226,10 @@ export const formatTransitionsTable = (input) => {
174
226
  };
175
227
  export const formatFunnelPathTable = (input) => {
176
228
  const lines = [
177
- `Full funnel path\t${input.date}\t${input.stepDropoff.funnel.name}`,
229
+ formatFunnelReportTitle('Full funnel path', input),
178
230
  'step\tpath\tusers\tadvanced\tconversionToNext\tdropoffs\tdropoffRate',
179
231
  ];
180
- for (const row of buildFunnelPathRows(input.stepDropoff, input.publishedFunnelFlow)) {
232
+ for (const row of buildFunnelPathRows(input.stepDropoff, input.pathFlow)) {
181
233
  lines.push([
182
234
  row.label,
183
235
  row.path,
@@ -194,7 +246,7 @@ export const formatConversionsTable = (input) => {
194
246
  const totals = input.stepDropoff.totals;
195
247
  const overviewTotals = input.overview.totals;
196
248
  const lines = [
197
- `Conversions\t${input.date}\t${input.stepDropoff.funnel.name}`,
249
+ formatFunnelReportTitle('Conversions', input),
198
250
  'metric\tvalue',
199
251
  ['visitors', formatNumber(totals.visitors ?? overviewTotals.visitors ?? 0)].join('\t'),
200
252
  ['quizEntries', formatNumber(totals.quizEntries ?? 0)].join('\t'),
@@ -217,11 +269,31 @@ export const formatConversionsTable = (input) => {
217
269
  formatPercent(metric.rate),
218
270
  ].join('\t'));
219
271
  }
220
- lines.push('', formatFunnelPathTable({
221
- date: input.date,
222
- stepDropoff: input.stepDropoff,
223
- publishedFunnelFlow: input.publishedFunnelFlow,
224
- }).trimEnd());
272
+ lines.push('', formatFunnelPathTable(input).trimEnd());
273
+ return `${lines.join('\n')}\n`;
274
+ };
275
+ export const buildJourneyVersionsReportPayload = (input) => ({
276
+ generatedAt: new Date().toISOString(),
277
+ workspaceId: input.workspaceId,
278
+ project: input.project,
279
+ funnel: input.funnel,
280
+ versions: input.versions,
281
+ });
282
+ export const formatJourneyVersionsTable = (input) => {
283
+ const lines = [
284
+ `Journey versions\t${input.funnel.name}`,
285
+ 'version\tid\tstartedAt\tactiveFrom\tactiveTo\tvisitors',
286
+ ];
287
+ for (const version of input.versions) {
288
+ lines.push([
289
+ version.isCurrent ? `${version.label} (current)` : version.label,
290
+ version.id,
291
+ version.startedAt || '',
292
+ version.activeFrom || '',
293
+ version.activeTo || '',
294
+ version.visitors === null ? '' : formatNumber(version.visitors),
295
+ ].join('\t'));
296
+ }
225
297
  return `${lines.join('\n')}\n`;
226
298
  };
227
299
  const flattenCohortRows = (rows) => rows.flatMap((row) => [row, ...(row.segments || []), ...(row.children || [])]);
package/dist/cli.d.ts CHANGED
@@ -8,7 +8,7 @@ import { type DocsExitCode } from './docsOutput.js';
8
8
  import { type DocsDiagnostic, type DocsStatus } from './docsTransaction.js';
9
9
  import { type FunnelValidationExitCode, type FunnelValidationResult } from './diagnosticOutput.js';
10
10
  import { type StepContractMigrationApplyResult, type StepContractMigrationCheckResult, type StepContractMigrationTargetVersion, type StepContractMigrationValidationResult } from './stepContractMigration.js';
11
- import { type AnalyticsOutputFormat, type AnalyticsStepDropoffResponse, type PublishedFunnelFlowResponse } from './analyticsOutput.js';
11
+ import { type AnalyticsFunnelFlow, type AnalyticsJourneyVersionRow, type AnalyticsPeriod, type AnalyticsOutputFormat, type AnalyticsStepDropoffResponse } from './analyticsOutput.js';
12
12
  export declare const CANONICAL_CREATE_TEMPLATE_SLUG = "template";
13
13
  export declare const SCAFFOLD_TRANSIENT_PATHS: readonly ["node_modules", ".next", "out", "tsconfig.tsbuildinfo", ".funnelsgrove-scaffold.json"];
14
14
  export declare class CliContractError extends Error {
@@ -142,6 +142,15 @@ type AnalyticsCommandInput = {
142
142
  format: AnalyticsOutputFormat;
143
143
  timezone?: string;
144
144
  };
145
+ type FunnelAnalyticsCommandInput = {
146
+ period: AnalyticsPeriod & {
147
+ fromIso: string;
148
+ toIso: string;
149
+ };
150
+ format: AnalyticsOutputFormat;
151
+ timezone?: string;
152
+ journeyVersion: string;
153
+ };
145
154
  type SyncTargetIdInput = {
146
155
  explicitWorkspaceId?: string;
147
156
  explicitFunnelId?: string;
@@ -161,6 +170,14 @@ export declare function buildAnalyticsCommandInput(options: {
161
170
  format?: string;
162
171
  timezone?: string;
163
172
  }): AnalyticsCommandInput;
173
+ export declare function buildFunnelAnalyticsCommandInput(options: {
174
+ date?: string;
175
+ from?: string;
176
+ to?: string;
177
+ journeyVersion?: string;
178
+ format?: string;
179
+ timezone?: string;
180
+ }): FunnelAnalyticsCommandInput;
164
181
  export declare function buildPatchSourceInput(input: {
165
182
  workspaceId: string;
166
183
  funnelId: string;
@@ -289,10 +306,18 @@ type FunnelPathAnalyticsInput = {
289
306
  workspaceId: string;
290
307
  projectId: string;
291
308
  funnelId: string;
292
- commandInput: AnalyticsCommandInput;
309
+ commandInput: FunnelAnalyticsCommandInput;
293
310
  };
294
311
  export declare const fetchFunnelPathAnalytics: (input: FunnelPathAnalyticsInput, apiCall?: AnalyticsApiCall) => Promise<{
295
312
  stepDropoff: AnalyticsStepDropoffResponse;
296
- publishedFunnelFlow: PublishedFunnelFlowResponse;
313
+ pathFlow: AnalyticsFunnelFlow | null;
314
+ }>;
315
+ type JourneyVersionsInput = Pick<FunnelPathAnalyticsInput, 'token' | 'workspaceId' | 'projectId' | 'funnelId'> & {
316
+ timezone?: string;
317
+ today: string;
318
+ };
319
+ export declare const fetchJourneyVersions: (input: JourneyVersionsInput, apiCall?: AnalyticsApiCall) => Promise<{
320
+ funnel: AnalyticsStepDropoffResponse["funnel"];
321
+ versions: AnalyticsJourneyVersionRow[];
297
322
  }>;
298
323
  export {};