adapty 0.11.0 → 0.13.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.
Files changed (122) hide show
  1. package/README.md +45 -0
  2. package/dist/cli/base/adapty/flags.d.ts +65 -0
  3. package/dist/cli/base/adapty/flags.js +95 -0
  4. package/dist/cli/base/adapty/index.d.ts +1 -0
  5. package/dist/cli/base/adapty/index.js +1 -0
  6. package/dist/cli/base/attribution/flags.d.ts +3 -3
  7. package/dist/cli/base/attribution/flags.js +5 -20
  8. package/dist/cli/commands/analytics/catalog.d.ts +10 -0
  9. package/dist/cli/commands/analytics/catalog.js +52 -0
  10. package/dist/cli/commands/analytics/chart.d.ts +21 -0
  11. package/dist/cli/commands/analytics/chart.js +47 -0
  12. package/dist/cli/commands/analytics/cohort.d.ts +19 -0
  13. package/dist/cli/commands/analytics/cohort.js +40 -0
  14. package/dist/cli/commands/analytics/conversion.d.ts +20 -0
  15. package/dist/cli/commands/analytics/conversion.js +44 -0
  16. package/dist/cli/commands/analytics/funnel.d.ts +16 -0
  17. package/dist/cli/commands/analytics/funnel.js +32 -0
  18. package/dist/cli/commands/analytics/ltv.d.ts +19 -0
  19. package/dist/cli/commands/analytics/ltv.js +40 -0
  20. package/dist/cli/commands/analytics/retention.d.ts +17 -0
  21. package/dist/cli/commands/analytics/retention.js +41 -0
  22. package/dist/cli/commands/analytics/values.d.ts +11 -0
  23. package/dist/cli/commands/analytics/values.js +33 -0
  24. package/dist/cli/commands/attribution/report/command.js +6 -11
  25. package/dist/cli/commands/product-offers/create.d.ts +15 -0
  26. package/dist/cli/commands/product-offers/create.js +41 -0
  27. package/dist/cli/commands/product-offers/get.d.ts +13 -0
  28. package/dist/cli/commands/product-offers/get.js +16 -0
  29. package/dist/cli/commands/product-offers/list.d.ts +11 -0
  30. package/dist/cli/commands/product-offers/list.js +15 -0
  31. package/dist/cli/commands/product-offers/update.d.ts +20 -0
  32. package/dist/cli/commands/product-offers/update.js +41 -0
  33. package/dist/cli/errors.js +8 -1
  34. package/dist/cli/input/app.js +1 -11
  35. package/dist/cli/input/filter.d.ts +11 -0
  36. package/dist/cli/input/filter.js +16 -0
  37. package/dist/cli/input/offer.d.ts +3 -0
  38. package/dist/cli/input/offer.js +9 -0
  39. package/dist/cli/input/period.d.ts +10 -0
  40. package/dist/cli/input/period.js +23 -0
  41. package/dist/cli/input/product.d.ts +3 -0
  42. package/dist/cli/input/product.js +9 -0
  43. package/dist/cli/input/uuid.d.ts +6 -0
  44. package/dist/cli/input/uuid.js +11 -0
  45. package/dist/cli/views/analytics/csv.d.ts +3 -0
  46. package/dist/cli/views/analytics/csv.js +16 -0
  47. package/dist/cli/views/analytics/human.d.ts +41 -0
  48. package/dist/cli/views/analytics/human.js +134 -0
  49. package/dist/cli/views/analytics/segments.d.ts +68 -0
  50. package/dist/cli/views/analytics/segments.js +189 -0
  51. package/dist/commands/analytics/catalog.d.ts +1 -0
  52. package/dist/commands/analytics/catalog.js +2 -0
  53. package/dist/commands/analytics/chart.d.ts +1 -0
  54. package/dist/commands/analytics/chart.js +2 -0
  55. package/dist/commands/analytics/cohort.d.ts +1 -0
  56. package/dist/commands/analytics/cohort.js +2 -0
  57. package/dist/commands/analytics/conversion.d.ts +1 -0
  58. package/dist/commands/analytics/conversion.js +2 -0
  59. package/dist/commands/analytics/funnel.d.ts +1 -0
  60. package/dist/commands/analytics/funnel.js +2 -0
  61. package/dist/commands/analytics/ltv.d.ts +1 -0
  62. package/dist/commands/analytics/ltv.js +2 -0
  63. package/dist/commands/analytics/retention.d.ts +1 -0
  64. package/dist/commands/analytics/retention.js +2 -0
  65. package/dist/commands/analytics/values.d.ts +1 -0
  66. package/dist/commands/analytics/values.js +2 -0
  67. package/dist/commands/product-offers/create.d.ts +1 -0
  68. package/dist/commands/product-offers/create.js +2 -0
  69. package/dist/commands/product-offers/get.d.ts +1 -0
  70. package/dist/commands/product-offers/get.js +2 -0
  71. package/dist/commands/product-offers/list.d.ts +1 -0
  72. package/dist/commands/product-offers/list.js +2 -0
  73. package/dist/commands/product-offers/update.d.ts +1 -0
  74. package/dist/commands/product-offers/update.js +2 -0
  75. package/dist/lib/api-schemas.d.ts +1 -0
  76. package/dist/sdk/adapty/analytics/catalog.d.ts +51 -0
  77. package/dist/sdk/adapty/analytics/catalog.js +5 -0
  78. package/dist/sdk/adapty/analytics/chart.d.ts +41 -0
  79. package/dist/sdk/adapty/analytics/chart.js +6 -0
  80. package/dist/sdk/adapty/analytics/cohort.d.ts +79 -0
  81. package/dist/sdk/adapty/analytics/cohort.js +4 -0
  82. package/dist/sdk/adapty/analytics/conversion.d.ts +55 -0
  83. package/dist/sdk/adapty/analytics/conversion.js +7 -0
  84. package/dist/sdk/adapty/analytics/funnel.d.ts +41 -0
  85. package/dist/sdk/adapty/analytics/funnel.js +3 -0
  86. package/dist/sdk/adapty/analytics/index.d.ts +24 -0
  87. package/dist/sdk/adapty/analytics/index.js +13 -0
  88. package/dist/sdk/adapty/analytics/ltv.d.ts +27 -0
  89. package/dist/sdk/adapty/analytics/ltv.js +4 -0
  90. package/dist/sdk/adapty/analytics/model.d.ts +53 -0
  91. package/dist/sdk/adapty/analytics/model.js +12 -0
  92. package/dist/sdk/adapty/analytics/query.d.ts +48 -0
  93. package/dist/sdk/adapty/analytics/query.js +52 -0
  94. package/dist/sdk/adapty/analytics/renewal-periods.d.ts +23 -0
  95. package/dist/sdk/adapty/analytics/renewal-periods.js +27 -0
  96. package/dist/sdk/adapty/analytics/resource.d.ts +29 -0
  97. package/dist/sdk/adapty/analytics/resource.js +72 -0
  98. package/dist/sdk/adapty/analytics/retention.d.ts +32 -0
  99. package/dist/sdk/adapty/analytics/retention.js +6 -0
  100. package/dist/sdk/adapty/analytics/values.d.ts +20 -0
  101. package/dist/sdk/adapty/analytics/values.js +1 -0
  102. package/dist/sdk/adapty/index.d.ts +8 -0
  103. package/dist/sdk/adapty/index.js +6 -0
  104. package/dist/sdk/adapty/product-offers/create.d.ts +5 -0
  105. package/dist/sdk/adapty/product-offers/create.js +12 -0
  106. package/dist/sdk/adapty/product-offers/index.d.ts +9 -0
  107. package/dist/sdk/adapty/product-offers/index.js +5 -0
  108. package/dist/sdk/adapty/product-offers/model.d.ts +48 -0
  109. package/dist/sdk/adapty/product-offers/model.js +30 -0
  110. package/dist/sdk/adapty/product-offers/resource.d.ts +21 -0
  111. package/dist/sdk/adapty/product-offers/resource.js +28 -0
  112. package/dist/sdk/adapty/product-offers/update.d.ts +19 -0
  113. package/dist/sdk/adapty/product-offers/update.js +42 -0
  114. package/dist/sdk/attribution/index.d.ts +1 -1
  115. package/dist/sdk/attribution/index.js +1 -1
  116. package/dist/sdk/attribution/report.d.ts +0 -2
  117. package/dist/sdk/attribution/report.js +2 -26
  118. package/dist/sdk/core/dates.d.ts +15 -0
  119. package/dist/sdk/core/dates.js +34 -0
  120. package/dist/sdk/core/errors.d.ts +3 -0
  121. package/oclif.manifest.json +1163 -128
  122. package/package.json +7 -1
@@ -0,0 +1,79 @@
1
+ import type { AnalyticsData, AnalyticsMeta, AnalyticsResponse, AnalyticsSegment, Money, PeriodUnit } from './model.js';
2
+ import type { GroupingInput, GroupingRequest } from './query.js';
3
+ import type { RenewalPeriodsInput, RenewalPeriodsQuery, RenewalPeriodsRequest } from './renewal-periods.js';
4
+ import type { Issue } from '../../core/errors.js';
5
+ /** No grouping: each segment is one cohort already. `granularity` is the length of one cohort. */
6
+ export type CohortInput = Pick<GroupingInput, 'granularity'> & RenewalPeriodsInput;
7
+ type CohortRequest = Pick<GroupingRequest, 'period_unit'> & RenewalPeriodsRequest;
8
+ /**
9
+ * One period of a cohort. `value` and `value_relative` are the revenue on the asked basis; the money fields
10
+ * give each basis.
11
+ */
12
+ export type CohortPoint = {
13
+ arpas: Money;
14
+ arppu: Money;
15
+ arpu: Money;
16
+ currently_active_period: boolean;
17
+ installs: number;
18
+ revenue: Money;
19
+ revenue_relative: Money;
20
+ subscribers: number;
21
+ subscribers_relative: number;
22
+ subscriptions: number;
23
+ subscriptions_relative: number;
24
+ value: number;
25
+ value_relative: number;
26
+ x: number;
27
+ };
28
+ /** Predicted revenue (`rev*`) and LTV (`ltv*`) after 3..24 months; null where the backend cannot predict. */
29
+ export type PredictValues = {
30
+ ltv3: null | number;
31
+ ltv6: null | number;
32
+ ltv9: null | number;
33
+ ltv12: null | number;
34
+ ltv18: null | number;
35
+ ltv24: null | number;
36
+ rev3: null | number;
37
+ rev6: null | number;
38
+ rev9: null | number;
39
+ rev12: null | number;
40
+ rev18: null | number;
41
+ rev24: null | number;
42
+ };
43
+ /** The backend predictions per basis (`gross_v2`: its second gross model); a section is null where it gives none. */
44
+ export type CohortPredict = {
45
+ gross: null | PredictValues;
46
+ gross_v2: null | PredictValues;
47
+ net: null | PredictValues;
48
+ proceeds: null | PredictValues;
49
+ status: null | {
50
+ status_3: null | number;
51
+ status_6: null | number;
52
+ status_9: null | number;
53
+ status_12: null | number;
54
+ status_18: null | number;
55
+ status_24: null | number;
56
+ };
57
+ };
58
+ /** One cohort, its start date as `key` (null for the total); `value` is its revenue on the asked basis. */
59
+ export type CohortSegment = AnalyticsSegment & {
60
+ arpas: Money;
61
+ arppu: Money;
62
+ arpu: Money;
63
+ installs: number;
64
+ predict: CohortPredict | null;
65
+ revenue: Money;
66
+ start_date: null | string;
67
+ subscribers: number;
68
+ subscriptions: number;
69
+ value: number;
70
+ values: CohortPoint[];
71
+ };
72
+ export type CohortData = AnalyticsData<CohortSegment>;
73
+ export type CohortMeta = AnalyticsMeta<RenewalPeriodsQuery & {
74
+ period_unit: PeriodUnit;
75
+ }>;
76
+ export type CohortResponse = AnalyticsResponse<CohortData, CohortMeta>;
77
+ export declare const validateCohort: (input: CohortInput) => Issue[];
78
+ export declare const toCohortRequest: (input: CohortInput) => CohortRequest;
79
+ export {};
@@ -0,0 +1,4 @@
1
+ import { toGroupingRequest } from './query.js';
2
+ import { toRenewalPeriodsRequest, validateRenewalPeriods } from './renewal-periods.js';
3
+ export const validateCohort = (input) => validateRenewalPeriods(input);
4
+ export const toCohortRequest = (input) => ({ ...toRenewalPeriodsRequest(input), ...toGroupingRequest(input) });
@@ -0,0 +1,55 @@
1
+ import type { AnalyticsData, AnalyticsMeta, AnalyticsResponse, AnalyticsSegment, PeriodUnit } from './model.js';
2
+ import type { ComparableQueryInput, GroupingInput, GroupingRequest, QueryRequest } from './query.js';
3
+ import type { Issue } from '../../core/errors.js';
4
+ /** A conversion id as the catalog lists it; the backend knows which ids exist and never maps one id to another pair. */
5
+ export type ConversionId = string;
6
+ export type ConversionInput = ComparableQueryInput & GroupingInput & {
7
+ conversionId: ConversionId;
8
+ };
9
+ type ConversionRequest = GroupingRequest & QueryRequest & {
10
+ conversion_id: ConversionId;
11
+ };
12
+ export type ConversionPoint = {
13
+ has_unfinished: boolean;
14
+ value: number;
15
+ value_from: number;
16
+ value_from_unfinished: number;
17
+ value_to: number;
18
+ x: string;
19
+ };
20
+ /** `value` is the conversion in percent: `value_to` of `value_from`. */
21
+ /** `value_from` and `value_to` are null only on a compared total that the backend does not give. */
22
+ export type ConversionSegment = AnalyticsSegment & {
23
+ value: number;
24
+ value_from: null | number;
25
+ value_to: null | number;
26
+ values: ConversionPoint[];
27
+ };
28
+ export type ConversionData = AnalyticsData<ConversionSegment> & {
29
+ /** The counts of the compared period itself; null when the backend has no total row for it. */
30
+ compare: null | {
31
+ segments: ConversionSegment[];
32
+ value: number;
33
+ value_from: null | number;
34
+ value_to: null | number;
35
+ };
36
+ value: number;
37
+ value_from: number;
38
+ value_to: number;
39
+ };
40
+ export type ConversionMeta = AnalyticsMeta<{
41
+ conversion_id: ConversionId;
42
+ period_unit: PeriodUnit;
43
+ segmentation: null | string;
44
+ }> & {
45
+ /** A window with data, given when the asked window has none. */
46
+ recommended_period: null | {
47
+ date_from: string;
48
+ date_to: string;
49
+ period_unit: string;
50
+ };
51
+ };
52
+ export type ConversionResponse = AnalyticsResponse<ConversionData, ConversionMeta>;
53
+ export declare const validateConversion: (input: ConversionInput) => Issue[];
54
+ export declare const toConversionRequest: (input: ConversionInput) => ConversionRequest;
55
+ export {};
@@ -0,0 +1,7 @@
1
+ import { toGroupingRequest, toQueryRequest, validateComparableQuery } from './query.js';
2
+ export const validateConversion = (input) => validateComparableQuery(input);
3
+ export const toConversionRequest = (input) => ({
4
+ conversion_id: input.conversionId,
5
+ ...toQueryRequest(input),
6
+ ...toGroupingRequest(input),
7
+ });
@@ -0,0 +1,41 @@
1
+ import type { AnalyticsData, AnalyticsMeta, AnalyticsResponse, AnalyticsSegment } from './model.js';
2
+ import type { AnalyticsQueryInput, GroupingInput, GroupingRequest, QueryRequest } from './query.js';
3
+ import type { Issue } from '../../core/errors.js';
4
+ /** No compared period: the funnel query mixes two date ranges into one, so the backend refuses it. */
5
+ export type FunnelInput = AnalyticsQueryInput & Pick<GroupingInput, 'groupBy'>;
6
+ type FunnelRequest = Pick<GroupingRequest, 'segmentation'> & QueryRequest;
7
+ /** A count with its share in percent: a drop-off or pick-up, a churn reason, a part of a step. */
8
+ export type Share = {
9
+ title: string;
10
+ value: number;
11
+ value_relative: number;
12
+ };
13
+ /**
14
+ * One funnel step: `x` is the backend step number. `relative_changes` is the share that reaches the next step,
15
+ * `following_changes` the drop-off (or pick-up) to it, `churn_reasons` why users left before this step, and
16
+ * `parts` splits the first paid step into Install → Paid and Trial → Paid.
17
+ */
18
+ export type FunnelStep = {
19
+ churn_reason_title: string;
20
+ churn_reasons: Share[];
21
+ following_changes: Share;
22
+ parts: Share[];
23
+ relative_changes: number;
24
+ title: string;
25
+ value: number;
26
+ value_relative: number;
27
+ x: number;
28
+ };
29
+ /** `value` is the first step, the installs; null for a segment without steps. */
30
+ export type FunnelSegment = AnalyticsSegment & {
31
+ value: null | number;
32
+ values: FunnelStep[];
33
+ };
34
+ export type FunnelData = AnalyticsData<FunnelSegment>;
35
+ export type FunnelMeta = AnalyticsMeta<{
36
+ segmentation: null | string;
37
+ }>;
38
+ export type FunnelResponse = AnalyticsResponse<FunnelData, FunnelMeta>;
39
+ export declare const validateFunnel: (input: FunnelInput) => Issue[];
40
+ export declare const toFunnelRequest: (input: FunnelInput) => FunnelRequest;
41
+ export {};
@@ -0,0 +1,3 @@
1
+ import { toGroupingRequest, toQueryRequest, validateQuery } from './query.js';
2
+ export const validateFunnel = (input) => validateQuery(input);
3
+ export const toFunnelRequest = (input) => ({ ...toQueryRequest(input), ...toGroupingRequest(input) });
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The door of the analytics resource: re-exports only, and only what a consumer uses. The request builders
3
+ * and the shapes inside an answer stay private to their operation.
4
+ */
5
+ export { validateChart } from './chart.js';
6
+ export { validateCohort } from './cohort.js';
7
+ export { validateConversion } from './conversion.js';
8
+ export { validateFunnel } from './funnel.js';
9
+ export { validateLtv } from './ltv.js';
10
+ export { periodTypes, periodUnits, revenueBases } from './model.js';
11
+ export { analytics } from './resource.js';
12
+ export { validateRetention } from './retention.js';
13
+ export { validateFilterValues } from './values.js';
14
+ export type { CatalogChart, CatalogOperation, CatalogOption, CatalogResponse, CatalogSegmentation, } from './catalog.js';
15
+ export type { ChartInput, ChartResponse } from './chart.js';
16
+ export type { CohortInput, CohortResponse } from './cohort.js';
17
+ export type { ConversionInput, ConversionResponse } from './conversion.js';
18
+ export type { FunnelInput, FunnelResponse } from './funnel.js';
19
+ export type { LtvInput, LtvResponse } from './ltv.js';
20
+ export type { AnalyticsSegment, PeriodUnit } from './model.js';
21
+ export type { AnalyticsFilter } from './query.js';
22
+ export type { AnalyticsApi } from './resource.js';
23
+ export type { RetentionInput, RetentionResponse } from './retention.js';
24
+ export type { FilterValue, FilterValuesInput, FilterValuesResponse } from './values.js';
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The door of the analytics resource: re-exports only, and only what a consumer uses. The request builders
3
+ * and the shapes inside an answer stay private to their operation.
4
+ */
5
+ export { validateChart } from './chart.js';
6
+ export { validateCohort } from './cohort.js';
7
+ export { validateConversion } from './conversion.js';
8
+ export { validateFunnel } from './funnel.js';
9
+ export { validateLtv } from './ltv.js';
10
+ export { periodTypes, periodUnits, revenueBases } from './model.js';
11
+ export { analytics } from './resource.js';
12
+ export { validateRetention } from './retention.js';
13
+ export { validateFilterValues } from './values.js';
@@ -0,0 +1,27 @@
1
+ import type { AnalyticsData, AnalyticsMeta, AnalyticsResponse, AnalyticsSegment } from './model.js';
2
+ import type { GroupingInput, GroupingRequest } from './query.js';
3
+ import type { RenewalPeriodsInput, RenewalPeriodsQuery, RenewalPeriodsRequest } from './renewal-periods.js';
4
+ import type { Issue } from '../../core/errors.js';
5
+ export type LtvInput = Pick<GroupingInput, 'groupBy'> & RenewalPeriodsInput;
6
+ type LtvRequest = Pick<GroupingRequest, 'segmentation'> & RenewalPeriodsRequest;
7
+ /** `value` is null for a renewal period (or day) that the cohort has not reached yet. */
8
+ export type LtvPoint = {
9
+ value: null | number;
10
+ x: number;
11
+ };
12
+ /** `cohort_size` is null on a total that the backend does not give. */
13
+ export type LtvSegment = AnalyticsSegment & {
14
+ cohort_size: null | number;
15
+ value: number;
16
+ values: LtvPoint[];
17
+ };
18
+ export type LtvData = AnalyticsData<LtvSegment> & {
19
+ value: number;
20
+ };
21
+ export type LtvMeta = AnalyticsMeta<RenewalPeriodsQuery & {
22
+ segmentation: null | string;
23
+ }>;
24
+ export type LtvResponse = AnalyticsResponse<LtvData, LtvMeta>;
25
+ export declare const validateLtv: (input: LtvInput) => Issue[];
26
+ export declare const toLtvRequest: (input: LtvInput) => LtvRequest;
27
+ export {};
@@ -0,0 +1,4 @@
1
+ import { toGroupingRequest } from './query.js';
2
+ import { toRenewalPeriodsRequest, validateRenewalPeriods } from './renewal-periods.js';
3
+ export const validateLtv = (input) => validateRenewalPeriods(input);
4
+ export const toLtvRequest = (input) => ({ ...toRenewalPeriodsRequest(input), ...toGroupingRequest(input) });
@@ -0,0 +1,53 @@
1
+ /**
2
+ * What the six analytics operations share: one answer shape, so one parser reads all of them. Answers pass
3
+ * through as the server sends them, in snake_case: that object is what --json prints.
4
+ *
5
+ * `data.segments` starts with the total (`is_total` true); every other segment is one group, and its `key` is the
6
+ * raw value that the backend gives for that group. No rows for the window: `segments` is empty.
7
+ * A number is the backend's number: 0 stays 0, and null means the backend has no value.
8
+ */
9
+ /** The vocabularies of the query fields, in the order the catalog lists them. */
10
+ export declare const periodUnits: readonly ["day", "week", "month", "quarter", "year"];
11
+ export type PeriodUnit = (typeof periodUnits)[number];
12
+ export declare const revenueBases: readonly ["gross", "proceeds", "net"];
13
+ export type RevenueBasis = (typeof revenueBases)[number];
14
+ export declare const periodTypes: readonly ["renewals", "days"];
15
+ export type PeriodType = (typeof periodTypes)[number];
16
+ export type AnalyticsSegment = {
17
+ /** The one way to tell the total from a group: a group's `key` can be null too. */
18
+ is_total: boolean;
19
+ /**
20
+ * The raw value that the backend gives for the group, as the dashboard has it; null for the total. Any value is
21
+ * possible, null, `""` and `"None"` (a "No Data" group) included. It is not promised to be a `--filter` value.
22
+ */
23
+ key: null | string;
24
+ /** The backend title, "No Data" included. */
25
+ title: string;
26
+ };
27
+ /**
28
+ * The query that ran: every field of the operation with its default, the filters as sent, the time zone and
29
+ * where it comes from (`request` for `--timezone`, else `app`).
30
+ */
31
+ export type ResolvedQuery<Fields> = Fields & {
32
+ filters: Record<string, string[]>;
33
+ timezone: string;
34
+ timezone_source: 'app' | 'request';
35
+ };
36
+ export type AnalyticsMeta<Fields = Record<string, unknown>> = {
37
+ query: ResolvedQuery<Fields>;
38
+ };
39
+ /** One amount in USD on each revenue basis. */
40
+ export type Money = {
41
+ gross: number;
42
+ net: number;
43
+ proceeds: number;
44
+ };
45
+ export type AnalyticsResponse<Data, Meta> = {
46
+ data: Data;
47
+ meta: Meta;
48
+ };
49
+ /** Every data answer has these: the main number and the segments, total first. */
50
+ export type AnalyticsData<Segment extends AnalyticsSegment> = {
51
+ segments: Segment[];
52
+ value: null | number;
53
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * What the six analytics operations share: one answer shape, so one parser reads all of them. Answers pass
3
+ * through as the server sends them, in snake_case: that object is what --json prints.
4
+ *
5
+ * `data.segments` starts with the total (`is_total` true); every other segment is one group, and its `key` is the
6
+ * raw value that the backend gives for that group. No rows for the window: `segments` is empty.
7
+ * A number is the backend's number: 0 stays 0, and null means the backend has no value.
8
+ */
9
+ /** The vocabularies of the query fields, in the order the catalog lists them. */
10
+ export const periodUnits = ['day', 'week', 'month', 'quarter', 'year'];
11
+ export const revenueBases = ['gross', 'proceeds', 'net'];
12
+ export const periodTypes = ['renewals', 'days'];
@@ -0,0 +1,48 @@
1
+ import type { PeriodUnit } from './model.js';
2
+ import type { Issue } from '../../core/errors.js';
3
+ /** One filter: a dimension key from the catalog and the raw values it keeps (any of them). */
4
+ export type AnalyticsFilter = {
5
+ dimension: string;
6
+ values: readonly string[];
7
+ };
8
+ /** What every operation takes. Dates are inclusive days of the query time zone, `YYYY-MM-DD`. */
9
+ export type AnalyticsQueryInput = {
10
+ appId: string;
11
+ dateFrom: string;
12
+ dateTo: string;
13
+ filters?: readonly AnalyticsFilter[] | undefined;
14
+ /** An IANA name; the app time zone when left out. Which names exist is the backend's knowledge. */
15
+ timezone?: string | undefined;
16
+ };
17
+ /** The two operations whose query keeps two periods apart: a chart and a conversion. */
18
+ export type ComparableQueryInput = AnalyticsQueryInput & {
19
+ compareFrom?: string | undefined;
20
+ compareTo?: string | undefined;
21
+ };
22
+ /** `date` and `compare_date` are `[start, end]`; every other key is a dimension with its values. */
23
+ export type FiltersRequest = Record<string, string[]>;
24
+ export type QueryRequest = {
25
+ filters: FiltersRequest;
26
+ timezone?: string;
27
+ };
28
+ export declare const validateQuery: (input: AnalyticsQueryInput) => Issue[];
29
+ /** A compared period is both ends or neither, and a real range: the backend has no default for half of one. */
30
+ export declare const validateComparableQuery: (input: ComparableQueryInput) => Issue[];
31
+ /**
32
+ * The fields every operation sends. Filters are a map of dimension to values, as the dashboard sends them: two
33
+ * filters of one dimension become one list (a value kept by either is kept by the union), each value once.
34
+ * An absent timezone is left out, so the backend applies the app zone and says so in `meta.query`.
35
+ */
36
+ export declare const toQueryRequest: (input: ComparableQueryInput) => QueryRequest;
37
+ /** The grouping and the period of one point, as the operations that take them name them. */
38
+ export type GroupingInput = {
39
+ granularity?: PeriodUnit | undefined;
40
+ /** A grouping key of the operation (of the chart, for a chart), as the catalog lists it. */
41
+ groupBy?: string | undefined;
42
+ };
43
+ export type GroupingRequest = {
44
+ period_unit?: PeriodUnit;
45
+ segmentation?: string;
46
+ };
47
+ /** The input names meet the API names here, once for every operation; a field left out stays out. */
48
+ export declare const toGroupingRequest: (input: GroupingInput) => GroupingRequest;
@@ -0,0 +1,52 @@
1
+ import { validateDayRange } from '../../core/dates.js';
2
+ /** The period has flags of its own; as a filter it would replace them without a word. */
3
+ const PERIOD_KEYS = new Set(['compare_date', 'date']);
4
+ export const validateQuery = (input) => {
5
+ const issues = validateDayRange(input.dateFrom, input.dateTo, { from: 'dateFrom', to: 'dateTo' });
6
+ for (const filter of input.filters ?? []) {
7
+ if (PERIOD_KEYS.has(filter.dimension)) {
8
+ issues.push({ message: `${filter.dimension} is not a filter: the period has flags of its own`, path: 'filter' });
9
+ }
10
+ }
11
+ return issues;
12
+ };
13
+ /** A compared period is both ends or neither, and a real range: the backend has no default for half of one. */
14
+ export const validateComparableQuery = (input) => {
15
+ const issues = validateQuery(input);
16
+ const { compareFrom, compareTo } = input;
17
+ if (compareFrom === undefined && compareTo === undefined) {
18
+ return issues;
19
+ }
20
+ if (compareFrom === undefined) {
21
+ return [...issues, { fields: ['compareTo'], message: 'required with {compareTo}', path: 'compareFrom' }];
22
+ }
23
+ if (compareTo === undefined) {
24
+ return [...issues, { fields: ['compareFrom'], message: 'required with {compareFrom}', path: 'compareTo' }];
25
+ }
26
+ return [...issues, ...validateDayRange(compareFrom, compareTo, { from: 'compareFrom', to: 'compareTo' })];
27
+ };
28
+ /**
29
+ * The fields every operation sends. Filters are a map of dimension to values, as the dashboard sends them: two
30
+ * filters of one dimension become one list (a value kept by either is kept by the union), each value once.
31
+ * An absent timezone is left out, so the backend applies the app zone and says so in `meta.query`.
32
+ */
33
+ export const toQueryRequest = (input) => {
34
+ const filters = { date: [input.dateFrom, input.dateTo] };
35
+ if (input.compareFrom !== undefined && input.compareTo !== undefined) {
36
+ filters.compare_date = [input.compareFrom, input.compareTo];
37
+ }
38
+ for (const filter of input.filters ?? []) {
39
+ const known = filters[filter.dimension] ?? [];
40
+ filters[filter.dimension] = [...new Set([...known, ...filter.values])];
41
+ }
42
+ const body = { filters };
43
+ if (input.timezone !== undefined) {
44
+ body.timezone = input.timezone;
45
+ }
46
+ return body;
47
+ };
48
+ /** The input names meet the API names here, once for every operation; a field left out stays out. */
49
+ export const toGroupingRequest = (input) => ({
50
+ ...(input.granularity === undefined ? {} : { period_unit: input.granularity }),
51
+ ...(input.groupBy === undefined ? {} : { segmentation: input.groupBy }),
52
+ });
@@ -0,0 +1,23 @@
1
+ import type { PeriodType, RevenueBasis } from './model.js';
2
+ import type { AnalyticsQueryInput, QueryRequest } from './query.js';
3
+ import type { Issue } from '../../core/errors.js';
4
+ /** What LTV and cohorts share: revenue on a basis, counted per renewal or per day. */
5
+ export type RenewalPeriodsInput = AnalyticsQueryInput & {
6
+ periodType?: PeriodType | undefined;
7
+ /** Whole days, 0 or more; only with `periodType: 'days'`. The backend list applies when left out. */
8
+ renewalDays?: readonly number[] | undefined;
9
+ revenueBasis?: RevenueBasis | undefined;
10
+ };
11
+ /** The fields of the resolved query that LTV and cohorts share. */
12
+ export type RenewalPeriodsQuery = {
13
+ period_type: PeriodType;
14
+ renewal_days: null | number[];
15
+ revenue_basis: RevenueBasis;
16
+ };
17
+ export type RenewalPeriodsRequest = QueryRequest & {
18
+ period_type?: PeriodType;
19
+ renewal_days?: number[];
20
+ revenue_basis?: RevenueBasis;
21
+ };
22
+ export declare const validateRenewalPeriods: (input: RenewalPeriodsInput) => Issue[];
23
+ export declare const toRenewalPeriodsRequest: (input: RenewalPeriodsInput) => RenewalPeriodsRequest;
@@ -0,0 +1,27 @@
1
+ import { toQueryRequest, validateQuery } from './query.js';
2
+ export const validateRenewalPeriods = (input) => {
3
+ const issues = validateQuery(input);
4
+ if (input.renewalDays !== undefined && input.periodType !== 'days') {
5
+ issues.push({ fields: ['periodType'], message: 'needs {periodType} days', path: 'renewalDays' });
6
+ }
7
+ // The backend reads the list as given: whole days, ascending, each once; anything else it refuses
8
+ const days = input.renewalDays ?? [];
9
+ const ordered = days.every((day, index) => index === 0 || day > (days[index - 1] ?? day));
10
+ if (days.some(day => !Number.isInteger(day) || day < 0) || !ordered) {
11
+ issues.push({ message: 'must be whole days, 0 or more, in ascending order, each day once', path: 'renewalDays' });
12
+ }
13
+ return issues;
14
+ };
15
+ export const toRenewalPeriodsRequest = (input) => {
16
+ const body = toQueryRequest(input);
17
+ if (input.revenueBasis !== undefined) {
18
+ body.revenue_basis = input.revenueBasis;
19
+ }
20
+ if (input.periodType !== undefined) {
21
+ body.period_type = input.periodType;
22
+ }
23
+ if (input.renewalDays !== undefined) {
24
+ body.renewal_days = [...input.renewalDays];
25
+ }
26
+ return body;
27
+ };
@@ -0,0 +1,29 @@
1
+ import type { CatalogResponse } from './catalog.js';
2
+ import type { ChartInput, ChartResponse } from './chart.js';
3
+ import type { CohortInput, CohortResponse } from './cohort.js';
4
+ import type { ConversionInput, ConversionResponse } from './conversion.js';
5
+ import type { FunnelInput, FunnelResponse } from './funnel.js';
6
+ import type { LtvInput, LtvResponse } from './ltv.js';
7
+ import type { RetentionInput, RetentionResponse } from './retention.js';
8
+ import type { FilterValuesInput, FilterValuesResponse } from './values.js';
9
+ import type { Http } from '../../core/http/index.js';
10
+ /**
11
+ * Every path of the analytics of an app in one place. The catalog and the values are GETs and keep the
12
+ * transport's retry. The six operations are POSTs that run analytics queries under the rate limits of each route
13
+ * (2 requests per second per app and 360 per minute for all callers together, as the Export API): sent once,
14
+ * never retried, and a 429 reaches the caller with the server's wait.
15
+ *
16
+ * The validating methods are async on purpose — a broken rule then arrives as a rejected promise, like a
17
+ * network failure, and one await covers both.
18
+ */
19
+ export declare const analytics: (http: Http) => {
20
+ catalog: (appId: string) => Promise<CatalogResponse>;
21
+ chart: (input: ChartInput) => Promise<ChartResponse>;
22
+ cohort: (input: CohortInput) => Promise<CohortResponse>;
23
+ conversion: (input: ConversionInput) => Promise<ConversionResponse>;
24
+ funnel: (input: FunnelInput) => Promise<FunnelResponse>;
25
+ ltv: (input: LtvInput) => Promise<LtvResponse>;
26
+ retention: (input: RetentionInput) => Promise<RetentionResponse>;
27
+ values: (input: FilterValuesInput) => Promise<FilterValuesResponse>;
28
+ };
29
+ export type AnalyticsApi = ReturnType<typeof analytics>;
@@ -0,0 +1,72 @@
1
+ import { ApiError } from '../../core/errors.js';
2
+ import { assertValid } from '../../core/validation.js';
3
+ import { toChartRequest, validateChart } from './chart.js';
4
+ import { toCohortRequest, validateCohort } from './cohort.js';
5
+ import { toConversionRequest, validateConversion } from './conversion.js';
6
+ import { toFunnelRequest, validateFunnel } from './funnel.js';
7
+ import { toLtvRequest, validateLtv } from './ltv.js';
8
+ import { toRetentionRequest, validateRetention } from './retention.js';
9
+ import { validateFilterValues } from './values.js';
10
+ const base = (appId) => `/apps/${appId}/analytics`;
11
+ const isObject = (value) => typeof value === 'object' && value !== null;
12
+ const dataOf = (body) => (isObject(body) ? body.data : undefined);
13
+ /** A data answer: `data.segments` is the list every view reads. */
14
+ const hasSegments = (data) => isObject(data) && Array.isArray(data.segments);
15
+ /**
16
+ * The transport hands back whatever a success body parsed to, and a proxy's HTML page parses to a string.
17
+ * Passed on, it would reach the views and exit 0 under --json, so a success without the `{ data }` envelope is
18
+ * an error, as in the attribution resource.
19
+ */
20
+ const envelope = async (pending, isData = isObject) => {
21
+ const body = await pending;
22
+ if (!isData(dataOf(body))) {
23
+ throw new ApiError({
24
+ code: 'malformed_response',
25
+ details: body,
26
+ message: 'The Developer API answered with a success status but not with its response envelope, '
27
+ + 'so the answer was cut short or came from something in between. Nothing was read.',
28
+ status: 200,
29
+ });
30
+ }
31
+ return body;
32
+ };
33
+ /**
34
+ * Every path of the analytics of an app in one place. The catalog and the values are GETs and keep the
35
+ * transport's retry. The six operations are POSTs that run analytics queries under the rate limits of each route
36
+ * (2 requests per second per app and 360 per minute for all callers together, as the Export API): sent once,
37
+ * never retried, and a 429 reaches the caller with the server's wait.
38
+ *
39
+ * The validating methods are async on purpose — a broken rule then arrives as a rejected promise, like a
40
+ * network failure, and one await covers both.
41
+ */
42
+ export const analytics = (http) => ({
43
+ catalog: (appId) => envelope(http.get(`${base(appId)}/catalog`)),
44
+ chart: async (input) => {
45
+ assertValid(validateChart(input));
46
+ return envelope(http.post(`${base(input.appId)}/metrics`, toChartRequest(input), { idempotent: false }), hasSegments);
47
+ },
48
+ cohort: async (input) => {
49
+ assertValid(validateCohort(input));
50
+ return envelope(http.post(`${base(input.appId)}/cohort`, toCohortRequest(input), { idempotent: false }), hasSegments);
51
+ },
52
+ conversion: async (input) => {
53
+ assertValid(validateConversion(input));
54
+ return envelope(http.post(`${base(input.appId)}/conversion`, toConversionRequest(input), { idempotent: false }), hasSegments);
55
+ },
56
+ funnel: async (input) => {
57
+ assertValid(validateFunnel(input));
58
+ return envelope(http.post(`${base(input.appId)}/funnel`, toFunnelRequest(input), { idempotent: false }), hasSegments);
59
+ },
60
+ ltv: async (input) => {
61
+ assertValid(validateLtv(input));
62
+ return envelope(http.post(`${base(input.appId)}/ltv`, toLtvRequest(input), { idempotent: false }), hasSegments);
63
+ },
64
+ retention: async (input) => {
65
+ assertValid(validateRetention(input));
66
+ return envelope(http.post(`${base(input.appId)}/retention`, toRetentionRequest(input), { idempotent: false }), hasSegments);
67
+ },
68
+ values: async (input) => {
69
+ assertValid(validateFilterValues(input));
70
+ return envelope(http.get(`${base(input.appId)}/values`, { query: { dimension: input.dimension } }));
71
+ },
72
+ });
@@ -0,0 +1,32 @@
1
+ import type { AnalyticsData, AnalyticsMeta, AnalyticsResponse, AnalyticsSegment } from './model.js';
2
+ import type { AnalyticsQueryInput, GroupingInput, GroupingRequest, QueryRequest } from './query.js';
3
+ import type { Issue } from '../../core/errors.js';
4
+ export type RetentionInput = AnalyticsQueryInput & Pick<GroupingInput, 'groupBy'> & {
5
+ /** Count from the trial instead of from the first paid period. */
6
+ useTrial?: boolean | undefined;
7
+ };
8
+ type RetentionRequest = Pick<GroupingRequest, 'segmentation'> & QueryRequest & {
9
+ use_trial?: boolean;
10
+ };
11
+ /** One retention period: `x` is the backend period number. */
12
+ export type RetentionPoint = {
13
+ title: string;
14
+ value: number;
15
+ value_relative: number;
16
+ x: number;
17
+ };
18
+ /** `value` and `first_value` are the subscribers of the first period. */
19
+ export type RetentionSegment = AnalyticsSegment & {
20
+ first_value: number;
21
+ value: number;
22
+ values: RetentionPoint[];
23
+ };
24
+ export type RetentionData = AnalyticsData<RetentionSegment>;
25
+ export type RetentionMeta = AnalyticsMeta<{
26
+ segmentation: null | string;
27
+ use_trial: boolean;
28
+ }>;
29
+ export type RetentionResponse = AnalyticsResponse<RetentionData, RetentionMeta>;
30
+ export declare const validateRetention: (input: RetentionInput) => Issue[];
31
+ export declare const toRetentionRequest: (input: RetentionInput) => RetentionRequest;
32
+ export {};
@@ -0,0 +1,6 @@
1
+ import { toGroupingRequest, toQueryRequest, validateQuery } from './query.js';
2
+ export const validateRetention = (input) => validateQuery(input);
3
+ export const toRetentionRequest = (input) => {
4
+ const body = { ...toQueryRequest(input), ...toGroupingRequest(input) };
5
+ return input.useTrial === undefined ? body : { ...body, use_trial: input.useTrial };
6
+ };