adapty 0.11.0 → 0.12.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 +32 -0
- package/dist/cli/base/adapty/flags.d.ts +65 -0
- package/dist/cli/base/adapty/flags.js +95 -0
- package/dist/cli/base/adapty/index.d.ts +1 -0
- package/dist/cli/base/adapty/index.js +1 -0
- package/dist/cli/base/attribution/flags.d.ts +3 -3
- package/dist/cli/base/attribution/flags.js +5 -20
- package/dist/cli/commands/analytics/catalog.d.ts +10 -0
- package/dist/cli/commands/analytics/catalog.js +52 -0
- package/dist/cli/commands/analytics/chart.d.ts +21 -0
- package/dist/cli/commands/analytics/chart.js +47 -0
- package/dist/cli/commands/analytics/cohort.d.ts +19 -0
- package/dist/cli/commands/analytics/cohort.js +40 -0
- package/dist/cli/commands/analytics/conversion.d.ts +20 -0
- package/dist/cli/commands/analytics/conversion.js +44 -0
- package/dist/cli/commands/analytics/funnel.d.ts +16 -0
- package/dist/cli/commands/analytics/funnel.js +32 -0
- package/dist/cli/commands/analytics/ltv.d.ts +19 -0
- package/dist/cli/commands/analytics/ltv.js +40 -0
- package/dist/cli/commands/analytics/retention.d.ts +17 -0
- package/dist/cli/commands/analytics/retention.js +41 -0
- package/dist/cli/commands/analytics/values.d.ts +11 -0
- package/dist/cli/commands/analytics/values.js +33 -0
- package/dist/cli/commands/attribution/report/command.js +6 -11
- package/dist/cli/errors.js +8 -1
- package/dist/cli/input/filter.d.ts +11 -0
- package/dist/cli/input/filter.js +16 -0
- package/dist/cli/input/period.d.ts +10 -0
- package/dist/cli/input/period.js +23 -0
- package/dist/cli/views/analytics/csv.d.ts +3 -0
- package/dist/cli/views/analytics/csv.js +16 -0
- package/dist/cli/views/analytics/human.d.ts +41 -0
- package/dist/cli/views/analytics/human.js +134 -0
- package/dist/cli/views/analytics/segments.d.ts +68 -0
- package/dist/cli/views/analytics/segments.js +185 -0
- package/dist/commands/analytics/catalog.d.ts +1 -0
- package/dist/commands/analytics/catalog.js +2 -0
- package/dist/commands/analytics/chart.d.ts +1 -0
- package/dist/commands/analytics/chart.js +2 -0
- package/dist/commands/analytics/cohort.d.ts +1 -0
- package/dist/commands/analytics/cohort.js +2 -0
- package/dist/commands/analytics/conversion.d.ts +1 -0
- package/dist/commands/analytics/conversion.js +2 -0
- package/dist/commands/analytics/funnel.d.ts +1 -0
- package/dist/commands/analytics/funnel.js +2 -0
- package/dist/commands/analytics/ltv.d.ts +1 -0
- package/dist/commands/analytics/ltv.js +2 -0
- package/dist/commands/analytics/retention.d.ts +1 -0
- package/dist/commands/analytics/retention.js +2 -0
- package/dist/commands/analytics/values.d.ts +1 -0
- package/dist/commands/analytics/values.js +2 -0
- package/dist/sdk/adapty/analytics/catalog.d.ts +51 -0
- package/dist/sdk/adapty/analytics/catalog.js +5 -0
- package/dist/sdk/adapty/analytics/chart.d.ts +41 -0
- package/dist/sdk/adapty/analytics/chart.js +6 -0
- package/dist/sdk/adapty/analytics/cohort.d.ts +79 -0
- package/dist/sdk/adapty/analytics/cohort.js +4 -0
- package/dist/sdk/adapty/analytics/conversion.d.ts +55 -0
- package/dist/sdk/adapty/analytics/conversion.js +7 -0
- package/dist/sdk/adapty/analytics/funnel.d.ts +41 -0
- package/dist/sdk/adapty/analytics/funnel.js +3 -0
- package/dist/sdk/adapty/analytics/index.d.ts +24 -0
- package/dist/sdk/adapty/analytics/index.js +13 -0
- package/dist/sdk/adapty/analytics/ltv.d.ts +27 -0
- package/dist/sdk/adapty/analytics/ltv.js +4 -0
- package/dist/sdk/adapty/analytics/model.d.ts +53 -0
- package/dist/sdk/adapty/analytics/model.js +12 -0
- package/dist/sdk/adapty/analytics/query.d.ts +48 -0
- package/dist/sdk/adapty/analytics/query.js +52 -0
- package/dist/sdk/adapty/analytics/renewal-periods.d.ts +23 -0
- package/dist/sdk/adapty/analytics/renewal-periods.js +27 -0
- package/dist/sdk/adapty/analytics/resource.d.ts +29 -0
- package/dist/sdk/adapty/analytics/resource.js +72 -0
- package/dist/sdk/adapty/analytics/retention.d.ts +32 -0
- package/dist/sdk/adapty/analytics/retention.js +6 -0
- package/dist/sdk/adapty/analytics/values.d.ts +20 -0
- package/dist/sdk/adapty/analytics/values.js +1 -0
- package/dist/sdk/adapty/index.d.ts +4 -0
- package/dist/sdk/adapty/index.js +3 -0
- package/dist/sdk/attribution/index.d.ts +1 -1
- package/dist/sdk/attribution/index.js +1 -1
- package/dist/sdk/attribution/report.d.ts +0 -2
- package/dist/sdk/attribution/report.js +2 -26
- package/dist/sdk/core/dates.d.ts +15 -0
- package/dist/sdk/core/dates.js +34 -0
- package/dist/sdk/core/errors.d.ts +3 -0
- package/oclif.manifest.json +884 -120
- package/package.json +4 -1
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Issue } from '../../core/errors.js';
|
|
2
|
+
/** The values one filter takes for an app: what `filters.<dimension>` accepts, and the `key` of its groups. */
|
|
3
|
+
export type FilterValuesInput = {
|
|
4
|
+
appId: string;
|
|
5
|
+
/** A filter key, as the catalog `dimensions` lists it; the backend refuses any other. */
|
|
6
|
+
dimension: string;
|
|
7
|
+
};
|
|
8
|
+
/** `group` is the parent in the dashboard list (the region of a country, ...); null at the top level. */
|
|
9
|
+
export type FilterValue = {
|
|
10
|
+
group: null | string;
|
|
11
|
+
label: string;
|
|
12
|
+
value: string;
|
|
13
|
+
};
|
|
14
|
+
export type FilterValuesResponse = {
|
|
15
|
+
data: FilterValue[];
|
|
16
|
+
meta: {
|
|
17
|
+
dimension: string;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export declare const validateFilterValues: (input: FilterValuesInput) => Issue[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const validateFilterValues = (input) => (input.dimension.trim() === '' ? [{ message: 'must not be empty', path: 'dimension' }] : []);
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import type { AccessLevelsApi } from './access-levels.js';
|
|
2
|
+
import type { AnalyticsApi } from './analytics/index.js';
|
|
2
3
|
import type { AppsApi } from './apps/index.js';
|
|
3
4
|
import type { AuthApi } from './auth/index.js';
|
|
4
5
|
import type { FeedbackApi } from './feedback/index.js';
|
|
5
6
|
import type { MigrationApi } from './migrations/index.js';
|
|
6
7
|
import type { Clock } from '../core/clock.js';
|
|
7
8
|
import type { RetryAttempt } from '../core/http/index.js';
|
|
9
|
+
export { periodTypes, periodUnits, revenueBases, validateChart, validateCohort, validateConversion, validateFilterValues, validateFunnel, validateLtv, validateRetention, } from './analytics/index.js';
|
|
8
10
|
export { platforms } from './apps/index.js';
|
|
9
11
|
export { developerErrorParser } from './errors.js';
|
|
10
12
|
export { sentiments } from './feedback/index.js';
|
|
11
13
|
export type { AccessLevel, AccessLevelList, AccessLevelsApi } from './access-levels.js';
|
|
14
|
+
export type { AnalyticsFilter, AnalyticsSegment, CatalogChart, CatalogOperation, CatalogOption, CatalogResponse, CatalogSegmentation, ChartInput, ChartResponse, CohortInput, CohortResponse, ConversionInput, ConversionResponse, FilterValue, FilterValuesInput, FilterValuesResponse, FunnelInput, FunnelResponse, LtvInput, LtvResponse, PeriodUnit, RetentionInput, RetentionResponse, } from './analytics/index.js';
|
|
12
15
|
export type { AppDetail, AppsApi, AppSummary, CreateAppInput, UpdateAppInput } from './apps/index.js';
|
|
13
16
|
export type { AuthApi, AuthUser, IssuedToken } from './auth/index.js';
|
|
14
17
|
export type { FeedbackApi, SendFeedbackInput, Sentiment } from './feedback/index.js';
|
|
@@ -36,6 +39,7 @@ export type AdaptyOptions = {
|
|
|
36
39
|
};
|
|
37
40
|
export type Adapty = {
|
|
38
41
|
accessLevels: AccessLevelsApi;
|
|
42
|
+
analytics: AnalyticsApi;
|
|
39
43
|
apps: AppsApi;
|
|
40
44
|
auth: AuthApi;
|
|
41
45
|
feedback: FeedbackApi;
|
package/dist/sdk/adapty/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { systemClock } from '../core/clock.js';
|
|
2
2
|
import { createHttp } from '../core/http/index.js';
|
|
3
3
|
import { accessLevels } from './access-levels.js';
|
|
4
|
+
import { analytics } from './analytics/index.js';
|
|
4
5
|
import { apps } from './apps/index.js';
|
|
5
6
|
import { auth } from './auth/index.js';
|
|
6
7
|
import { developerErrorParser } from './errors.js';
|
|
7
8
|
import { feedback } from './feedback/index.js';
|
|
8
9
|
import { migrations } from './migrations/index.js';
|
|
10
|
+
export { periodTypes, periodUnits, revenueBases, validateChart, validateCohort, validateConversion, validateFilterValues, validateFunnel, validateLtv, validateRetention, } from './analytics/index.js';
|
|
9
11
|
export { platforms } from './apps/index.js';
|
|
10
12
|
export { developerErrorParser } from './errors.js';
|
|
11
13
|
export { sentiments } from './feedback/index.js';
|
|
@@ -46,6 +48,7 @@ export const createAdapty = (options = {}) => {
|
|
|
46
48
|
const wizard = createHttp({ ...transport, trailingSlash: false });
|
|
47
49
|
return {
|
|
48
50
|
accessLevels: accessLevels(http),
|
|
51
|
+
analytics: analytics(http),
|
|
49
52
|
apps: apps(http),
|
|
50
53
|
auth: auth(http),
|
|
51
54
|
feedback: feedback(http),
|
|
@@ -2,7 +2,7 @@ import type { AttributionApi } from './resource.js';
|
|
|
2
2
|
import type { Clock } from '../core/clock.js';
|
|
3
3
|
import type { RetryAttempt } from '../core/http/index.js';
|
|
4
4
|
export { attributionErrorParser } from './errors.js';
|
|
5
|
-
export { granularities,
|
|
5
|
+
export { granularities, reportGroupBy, revenueBases, sortDirections, validateReport } from './report.js';
|
|
6
6
|
export { validateValues } from './values.js';
|
|
7
7
|
export type { Dimension, DimensionCatalog, DimensionIdentity, Metric, MetricCatalog, MetricUnit, ReportLimits, } from './catalog.js';
|
|
8
8
|
export type { Granularity, ReportData, ReportFilter, ReportGroupBy, ReportInput, ReportMeta, ReportRow, ReportSort, RevenueBasis, SortDirection, } from './report.js';
|
|
@@ -3,7 +3,7 @@ import { createHttp, defaultRetryPolicy, defaultShouldRetry } from '../core/http
|
|
|
3
3
|
import { attributionErrorParser } from './errors.js';
|
|
4
4
|
import { attribution } from './resource.js';
|
|
5
5
|
export { attributionErrorParser } from './errors.js';
|
|
6
|
-
export { granularities,
|
|
6
|
+
export { granularities, reportGroupBy, revenueBases, sortDirections, validateReport } from './report.js';
|
|
7
7
|
export { validateValues } from './values.js';
|
|
8
8
|
/** Exported because the adapter compares the resolved URL with it and warns when they differ. */
|
|
9
9
|
export const DEFAULT_ATTRIBUTION_API_URL = 'https://api-ua.adapty.io/api/v1/cli';
|
|
@@ -57,8 +57,6 @@ export type ReportMeta = {
|
|
|
57
57
|
/** The query as the backend resolved it: timezone, currency and defaults filled in. */
|
|
58
58
|
query: Record<string, unknown>;
|
|
59
59
|
};
|
|
60
|
-
/** The `YYYY-MM-DD` shape alone, shared with the adapter's date flag parser. */
|
|
61
|
-
export declare const ISO_DATE: RegExp;
|
|
62
60
|
/** Shared with values: both read a day range, and both are refused the same way. */
|
|
63
61
|
export declare const validateDateRange: (input: {
|
|
64
62
|
dateFrom: string;
|
|
@@ -1,35 +1,11 @@
|
|
|
1
|
+
import { validateDayRange } from '../core/dates.js';
|
|
1
2
|
/** In the order the backend documents them, so a flag's help reads the same way. */
|
|
2
3
|
export const reportGroupBy = ['date', 'campaign', 'adset', 'ad', 'keyword', 'channel', 'country', 'store'];
|
|
3
4
|
export const granularities = ['day', 'week', 'month', 'quarter', 'year'];
|
|
4
5
|
export const revenueBases = ['gross', 'proceeds', 'net'];
|
|
5
6
|
export const sortDirections = ['asc', 'desc'];
|
|
6
|
-
/** The `YYYY-MM-DD` shape alone, shared with the adapter's date flag parser. */
|
|
7
|
-
export const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
8
|
-
/** The shape alone lets 2026-02-30 through; a round trip through Date does not. */
|
|
9
|
-
const isIsoDate = (value) => {
|
|
10
|
-
if (!ISO_DATE.test(value)) {
|
|
11
|
-
return false;
|
|
12
|
-
}
|
|
13
|
-
const date = new Date(`${value}T00:00:00Z`);
|
|
14
|
-
return !Number.isNaN(date.getTime()) && date.toISOString().startsWith(value);
|
|
15
|
-
};
|
|
16
7
|
/** Shared with values: both read a day range, and both are refused the same way. */
|
|
17
|
-
export const validateDateRange = (input) => {
|
|
18
|
-
const issues = [];
|
|
19
|
-
const fromValid = isIsoDate(input.dateFrom);
|
|
20
|
-
const toValid = isIsoDate(input.dateTo);
|
|
21
|
-
if (!fromValid) {
|
|
22
|
-
issues.push({ message: 'must be a date in YYYY-MM-DD form', path: 'dateFrom' });
|
|
23
|
-
}
|
|
24
|
-
if (!toValid) {
|
|
25
|
-
issues.push({ message: 'must be a date in YYYY-MM-DD form', path: 'dateTo' });
|
|
26
|
-
}
|
|
27
|
-
// ISO days order as strings, but only two real days are worth comparing
|
|
28
|
-
if (fromValid && toValid && input.dateTo < input.dateFrom) {
|
|
29
|
-
issues.push({ message: 'must not be before the start date', path: 'dateTo' });
|
|
30
|
-
}
|
|
31
|
-
return issues;
|
|
32
|
-
};
|
|
8
|
+
export const validateDateRange = (input) => validateDayRange(input.dateFrom, input.dateTo, { from: 'dateFrom', to: 'dateTo' });
|
|
33
9
|
/**
|
|
34
10
|
* The rules a report can break without asking the server. Which metric and dimension names exist
|
|
35
11
|
* is the catalog's knowledge, so the backend checks those. An Issue path names an input field
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Issue } from './errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Days as the backends take them: `YYYY-MM-DD`, an inclusive range of two. Shared by every product that
|
|
4
|
+
* reads a day range, so a range is checked, and refused, in the same words everywhere.
|
|
5
|
+
*/
|
|
6
|
+
/** The `YYYY-MM-DD` shape alone, shared with the adapter's date flag parser. */
|
|
7
|
+
export declare const ISO_DATE: RegExp;
|
|
8
|
+
/**
|
|
9
|
+
* Both ends real days, the end not before the start. `paths` names the two input fields, so the adapter
|
|
10
|
+
* can name the two flags the user typed.
|
|
11
|
+
*/
|
|
12
|
+
export declare const validateDayRange: (from: string, to: string, paths: {
|
|
13
|
+
from: string;
|
|
14
|
+
to: string;
|
|
15
|
+
}) => Issue[];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Days as the backends take them: `YYYY-MM-DD`, an inclusive range of two. Shared by every product that
|
|
3
|
+
* reads a day range, so a range is checked, and refused, in the same words everywhere.
|
|
4
|
+
*/
|
|
5
|
+
/** The `YYYY-MM-DD` shape alone, shared with the adapter's date flag parser. */
|
|
6
|
+
export const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
7
|
+
/** The shape alone lets 2026-02-30 through; a round trip through Date does not. */
|
|
8
|
+
const isIsoDate = (value) => {
|
|
9
|
+
if (!ISO_DATE.test(value)) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
const date = new Date(`${value}T00:00:00Z`);
|
|
13
|
+
return !Number.isNaN(date.getTime()) && date.toISOString().startsWith(value);
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Both ends real days, the end not before the start. `paths` names the two input fields, so the adapter
|
|
17
|
+
* can name the two flags the user typed.
|
|
18
|
+
*/
|
|
19
|
+
export const validateDayRange = (from, to, paths) => {
|
|
20
|
+
const issues = [];
|
|
21
|
+
const fromValid = isIsoDate(from);
|
|
22
|
+
const toValid = isIsoDate(to);
|
|
23
|
+
if (!fromValid) {
|
|
24
|
+
issues.push({ message: 'must be a date in YYYY-MM-DD form', path: paths.from });
|
|
25
|
+
}
|
|
26
|
+
if (!toValid) {
|
|
27
|
+
issues.push({ message: 'must be a date in YYYY-MM-DD form', path: paths.to });
|
|
28
|
+
}
|
|
29
|
+
// ISO days order as strings, but only two real days are worth comparing
|
|
30
|
+
if (fromValid && toValid && to < from) {
|
|
31
|
+
issues.push({ message: 'must not be before the start date', path: paths.to });
|
|
32
|
+
}
|
|
33
|
+
return issues;
|
|
34
|
+
};
|
|
@@ -44,6 +44,9 @@ export declare class CancelledError extends SdkError {
|
|
|
44
44
|
constructor();
|
|
45
45
|
}
|
|
46
46
|
export type Issue = {
|
|
47
|
+
/** The other input fields that `message` names, each written there as `{fieldName}`. */
|
|
48
|
+
fields?: readonly string[] | undefined;
|
|
49
|
+
/** Plain words; another input field appears as `{fieldName}`, listed in `fields`, for the adapter to write. */
|
|
47
50
|
message: string;
|
|
48
51
|
/** The input field the problem belongs to. No path: the input as a whole is wrong. */
|
|
49
52
|
path?: string | undefined;
|