ad2app-lib 1.23.0 → 1.24.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/dist/analytics/index.d.ts +6 -0
- package/dist/analytics/index.js +7 -0
- package/dist/types/scheduling/I_SchedulingAnalytics.d.ts +28 -0
- package/dist/types/scheduling/I_SchedulingAnalytics.js +23 -1
- package/package.json +1 -1
- package/src/analytics/index.test.ts +23 -0
- package/src/analytics/index.ts +15 -0
- package/src/types/scheduling/I_SchedulingAnalytics.test.ts +57 -0
- package/src/types/scheduling/I_SchedulingAnalytics.ts +45 -0
|
@@ -36,6 +36,7 @@ export declare const EVENTS: {
|
|
|
36
36
|
readonly SUBSCRIPTION_CANCELED: "subscription_canceled";
|
|
37
37
|
readonly PAYWALL_SHOWN: "paywall_shown";
|
|
38
38
|
readonly PAYWALL_DISMISSED: "paywall_dismissed";
|
|
39
|
+
readonly ACCESS_DENIED: "access_denied";
|
|
39
40
|
readonly ONBOARDING_STEP_COMPLETED: "onboarding_step_completed";
|
|
40
41
|
readonly EXIT_INTENT_SHOWN: "exit_intent_shown";
|
|
41
42
|
readonly EXIT_INTENT_DISMISSED: "exit_intent_dismissed";
|
|
@@ -194,6 +195,11 @@ export interface EventProperties {
|
|
|
194
195
|
page_views?: number;
|
|
195
196
|
app_locale: AppLocale;
|
|
196
197
|
};
|
|
198
|
+
[EVENTS.ACCESS_DENIED]: {
|
|
199
|
+
reason: 'beta-required' | 'subscription-required';
|
|
200
|
+
path?: string;
|
|
201
|
+
app_locale: AppLocale;
|
|
202
|
+
};
|
|
197
203
|
[EVENTS.PAYWALL_DISMISSED]: {
|
|
198
204
|
surface: 'upgrade_modal' | 'pricing_page';
|
|
199
205
|
trigger: 'user_click' | 'limit_reached' | 'connect_wall';
|
package/dist/analytics/index.js
CHANGED
|
@@ -45,6 +45,13 @@ exports.EVENTS = {
|
|
|
45
45
|
// Measurement baseline (080) — paywall / onboarding / exit-intent (frontend-owned)
|
|
46
46
|
PAYWALL_SHOWN: 'paywall_shown', // any monetization surface is shown (upgrade modal / pricing page)
|
|
47
47
|
PAYWALL_DISMISSED: 'paywall_dismissed', // closed without converting
|
|
48
|
+
// AD2-1284: an entitlement wall rejected an API READ, which is control flow,
|
|
49
|
+
// not an error. Distinct from PAYWALL_SHOWN because nothing was shown: the
|
|
50
|
+
// driver classified a 403 at the choke point and the redirect follows. Before
|
|
51
|
+
// this event existed the only record was a captured $exception, which put
|
|
52
|
+
// expected paywall hits into Error Tracking and made that dashboard harder to
|
|
53
|
+
// trust — it is part of why a REAL retry defect sat unnoticed in the same list.
|
|
54
|
+
ACCESS_DENIED: 'access_denied', // entitlement 403 at the API choke point (beta wall / subscription)
|
|
48
55
|
ONBOARDING_STEP_COMPLETED: 'onboarding_step_completed', // one step of a multi-step flow (complete-profile)
|
|
49
56
|
EXIT_INTENT_SHOWN: 'exit_intent_shown', // exit-intent capture modal armed (065, verbatim wire values)
|
|
50
57
|
EXIT_INTENT_DISMISSED: 'exit_intent_dismissed',
|
|
@@ -10,6 +10,24 @@
|
|
|
10
10
|
*/
|
|
11
11
|
export declare const ANALYTICS_SOURCES: readonly ["daily-metrics", "follower-stats", "content-decay", "top-posts"];
|
|
12
12
|
export type AnalyticsSource = (typeof ANALYTICS_SOURCES)[number];
|
|
13
|
+
/**
|
|
14
|
+
* The metrics whose visibility is decided by the connected platform mix rather
|
|
15
|
+
* than by the data itself.
|
|
16
|
+
*
|
|
17
|
+
* Not every metric is here. `engagements` is our own roll-up (likes + comments
|
|
18
|
+
* + shares + saves) and `followerGrowth` comes from the follower-stats surface,
|
|
19
|
+
* so neither is platform-reported and neither is gated.
|
|
20
|
+
*/
|
|
21
|
+
export declare const GATED_METRICS: readonly ["impressions", "reach", "likes", "comments", "shares", "saves", "clicks", "views"];
|
|
22
|
+
export type GatedMetric = (typeof GATED_METRICS)[number];
|
|
23
|
+
/**
|
|
24
|
+
* Which gated metrics the current platform mix can actually report.
|
|
25
|
+
*
|
|
26
|
+
* An ABSENT entry means UNKNOWN, never unavailable — the producer omits the map
|
|
27
|
+
* when it could not determine the platform mix, and a consumer must fall
|
|
28
|
+
* through to showing the figure rather than withhold on a guess.
|
|
29
|
+
*/
|
|
30
|
+
export type MetricAvailability = Partial<Record<GatedMetric, boolean>>;
|
|
13
31
|
/**
|
|
14
32
|
* Aggregated KPI figures for the selected date range.
|
|
15
33
|
* Returned by GET /social/analytics.
|
|
@@ -50,6 +68,16 @@ export declare class SchedulingAnalyticsKpiDTO {
|
|
|
50
68
|
* Absent/true = available.
|
|
51
69
|
*/
|
|
52
70
|
reachAvailable?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Structural availability for every gated metric, from the backend's
|
|
73
|
+
* per-platform capability map (spec 111). Supersedes the two flags above,
|
|
74
|
+
* which stay for consumers that only ever read those two.
|
|
75
|
+
*
|
|
76
|
+
* ABSENT means UNKNOWN, not unavailable: the backend omits this whole map
|
|
77
|
+
* when it could not determine the connected platform mix, and a consumer
|
|
78
|
+
* must show the figure rather than withhold on a guess.
|
|
79
|
+
*/
|
|
80
|
+
metricAvailability?: MetricAvailability;
|
|
53
81
|
constructor(data: SchedulingAnalyticsKpiDTO);
|
|
54
82
|
}
|
|
55
83
|
/**
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* per-post timeline snapshots, content decay windows, and follower stats.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.SchedulingFollowerStatDTO = exports.SchedulingContentDecayDTO = exports.SchedulingPostTimelineEntryDTO = exports.SchedulingBestTimeSlotDTO = exports.SchedulingAnalyticsParamsDTO = exports.SchedulingAnalyticsEntryDTO = exports.SchedulingAnalyticsKpiDTO = exports.ANALYTICS_SOURCES = void 0;
|
|
9
|
+
exports.SchedulingFollowerStatDTO = exports.SchedulingContentDecayDTO = exports.SchedulingPostTimelineEntryDTO = exports.SchedulingBestTimeSlotDTO = exports.SchedulingAnalyticsParamsDTO = exports.SchedulingAnalyticsEntryDTO = exports.SchedulingAnalyticsKpiDTO = exports.GATED_METRICS = exports.ANALYTICS_SOURCES = void 0;
|
|
10
10
|
// ── Analytics sources (AD2-1045) ──────────────────────────────────────────────
|
|
11
11
|
/**
|
|
12
12
|
* The upstream sources an analytics response aggregates. Used by
|
|
@@ -18,6 +18,25 @@ exports.ANALYTICS_SOURCES = [
|
|
|
18
18
|
'content-decay',
|
|
19
19
|
'top-posts',
|
|
20
20
|
];
|
|
21
|
+
// ── Per-metric structural availability (spec 111) ─────────────────────────────
|
|
22
|
+
/**
|
|
23
|
+
* The metrics whose visibility is decided by the connected platform mix rather
|
|
24
|
+
* than by the data itself.
|
|
25
|
+
*
|
|
26
|
+
* Not every metric is here. `engagements` is our own roll-up (likes + comments
|
|
27
|
+
* + shares + saves) and `followerGrowth` comes from the follower-stats surface,
|
|
28
|
+
* so neither is platform-reported and neither is gated.
|
|
29
|
+
*/
|
|
30
|
+
exports.GATED_METRICS = [
|
|
31
|
+
'impressions',
|
|
32
|
+
'reach',
|
|
33
|
+
'likes',
|
|
34
|
+
'comments',
|
|
35
|
+
'shares',
|
|
36
|
+
'saves',
|
|
37
|
+
'clicks',
|
|
38
|
+
'views',
|
|
39
|
+
];
|
|
21
40
|
// ── SchedulingAnalyticsKpiDTO ─────────────────────────────────────────────────
|
|
22
41
|
/**
|
|
23
42
|
* Aggregated KPI figures for the selected date range.
|
|
@@ -43,6 +62,9 @@ class SchedulingAnalyticsKpiDTO {
|
|
|
43
62
|
if (data.reachAvailable !== undefined) {
|
|
44
63
|
this.reachAvailable = data.reachAvailable;
|
|
45
64
|
}
|
|
65
|
+
if (data.metricAvailability !== undefined) {
|
|
66
|
+
this.metricAvailability = data.metricAvailability;
|
|
67
|
+
}
|
|
46
68
|
}
|
|
47
69
|
}
|
|
48
70
|
exports.SchedulingAnalyticsKpiDTO = SchedulingAnalyticsKpiDTO;
|
package/package.json
CHANGED
|
@@ -88,6 +88,7 @@ const EVENT_PROPERTY_WITNESS: { [E in keyof EventProperties]: EventProperties[E]
|
|
|
88
88
|
// 080 measurement baseline
|
|
89
89
|
[EVENTS.PAYWALL_SHOWN]: { surface: "upgrade_modal", trigger: "user_click", hit_number: 1, app_locale: "en" },
|
|
90
90
|
[EVENTS.PAYWALL_DISMISSED]: { surface: "upgrade_modal", trigger: "user_click", hit_number: 1, app_locale: "en" },
|
|
91
|
+
[EVENTS.ACCESS_DENIED]: { reason: "subscription-required", app_locale: "en" },
|
|
91
92
|
[EVENTS.ONBOARDING_STEP_COMPLETED]: { flow: "complete_profile", step: "role", step_index: 1, app_locale: "en" },
|
|
92
93
|
[EVENTS.EXIT_INTENT_SHOWN]: { surface: "connect_wall" },
|
|
93
94
|
[EVENTS.EXIT_INTENT_DISMISSED]: { surface: "connect_wall" },
|
|
@@ -202,6 +203,28 @@ test("080 exit_intent_* wire values match the 065 component verbatim (fidelity g
|
|
|
202
203
|
assert.equal(shown.surface, "connect_wall");
|
|
203
204
|
});
|
|
204
205
|
|
|
206
|
+
test("AD2-1284 access_denied is locked, and its reasons mirror the backend's two 403s", () => {
|
|
207
|
+
// The wire name is final: Error Tracking noise was traded for this event, and
|
|
208
|
+
// renaming it would orphan whatever reads it.
|
|
209
|
+
assert.equal(EVENTS.ACCESS_DENIED, "access_denied");
|
|
210
|
+
|
|
211
|
+
// Both reasons must type-check — they are the two DIFFERENT 403s the backend
|
|
212
|
+
// sends, and accessDenied.ts's AccessDenial union is the other copy of this
|
|
213
|
+
// fact. A drift between them is the defect this pins.
|
|
214
|
+
const beta: EventProperties["access_denied"] = {
|
|
215
|
+
reason: "beta-required",
|
|
216
|
+
path: "/social/inbox/messages",
|
|
217
|
+
app_locale: "pl",
|
|
218
|
+
};
|
|
219
|
+
const sub: EventProperties["access_denied"] = {
|
|
220
|
+
reason: "subscription-required",
|
|
221
|
+
app_locale: "en",
|
|
222
|
+
};
|
|
223
|
+
assert.equal(beta.reason, "beta-required");
|
|
224
|
+
assert.equal(beta.path, "/social/inbox/messages");
|
|
225
|
+
assert.equal(sub.path, undefined);
|
|
226
|
+
});
|
|
227
|
+
|
|
205
228
|
test("080 PAYWALL_HITS person prop is locked; hit_number stays the event-level truth", () => {
|
|
206
229
|
assert.equal(PERSON_PROPS.PAYWALL_HITS, "paywall_hits");
|
|
207
230
|
// The analytical counter lives on the event (person props are query-time-latest).
|
package/src/analytics/index.ts
CHANGED
|
@@ -48,6 +48,13 @@ export const EVENTS = {
|
|
|
48
48
|
// Measurement baseline (080) — paywall / onboarding / exit-intent (frontend-owned)
|
|
49
49
|
PAYWALL_SHOWN: 'paywall_shown', // any monetization surface is shown (upgrade modal / pricing page)
|
|
50
50
|
PAYWALL_DISMISSED: 'paywall_dismissed', // closed without converting
|
|
51
|
+
// AD2-1284: an entitlement wall rejected an API READ, which is control flow,
|
|
52
|
+
// not an error. Distinct from PAYWALL_SHOWN because nothing was shown: the
|
|
53
|
+
// driver classified a 403 at the choke point and the redirect follows. Before
|
|
54
|
+
// this event existed the only record was a captured $exception, which put
|
|
55
|
+
// expected paywall hits into Error Tracking and made that dashboard harder to
|
|
56
|
+
// trust — it is part of why a REAL retry defect sat unnoticed in the same list.
|
|
57
|
+
ACCESS_DENIED: 'access_denied', // entitlement 403 at the API choke point (beta wall / subscription)
|
|
51
58
|
ONBOARDING_STEP_COMPLETED: 'onboarding_step_completed', // one step of a multi-step flow (complete-profile)
|
|
52
59
|
EXIT_INTENT_SHOWN: 'exit_intent_shown', // exit-intent capture modal armed (065, verbatim wire values)
|
|
53
60
|
EXIT_INTENT_DISMISSED: 'exit_intent_dismissed',
|
|
@@ -241,6 +248,14 @@ export interface EventProperties {
|
|
|
241
248
|
page_views?: number;
|
|
242
249
|
app_locale: AppLocale;
|
|
243
250
|
};
|
|
251
|
+
// `reason` mirrors accessDenied.ts's AccessDenial union exactly — the two
|
|
252
|
+
// different 403s the backend sends. `path` is the request path, never a URL
|
|
253
|
+
// with query params (no PII in analytics).
|
|
254
|
+
[EVENTS.ACCESS_DENIED]: {
|
|
255
|
+
reason: 'beta-required' | 'subscription-required';
|
|
256
|
+
path?: string;
|
|
257
|
+
app_locale: AppLocale;
|
|
258
|
+
};
|
|
244
259
|
[EVENTS.PAYWALL_DISMISSED]: {
|
|
245
260
|
surface: 'upgrade_modal' | 'pricing_page';
|
|
246
261
|
trigger: 'user_click' | 'limit_reached' | 'connect_wall';
|
|
@@ -2,7 +2,9 @@ import { test } from 'node:test';
|
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import {
|
|
4
4
|
ANALYTICS_SOURCES,
|
|
5
|
+
GATED_METRICS,
|
|
5
6
|
SchedulingAnalyticsKpiDTO,
|
|
7
|
+
type MetricAvailability,
|
|
6
8
|
} from './I_SchedulingAnalytics';
|
|
7
9
|
|
|
8
10
|
// AD2-1045 — the analytics partial-failure signal: a KPI response can name
|
|
@@ -89,3 +91,58 @@ test('impressionsAvailable/reachAvailable are optional and absent by default (ba
|
|
|
89
91
|
assert.equal('impressionsAvailable' in serialized, false);
|
|
90
92
|
assert.equal('reachAvailable' in serialized, false);
|
|
91
93
|
});
|
|
94
|
+
|
|
95
|
+
// Spec 111 — per-metric structural availability. The two AD2-1078 flags above
|
|
96
|
+
// only ever described impressions and reach; the capability map the backend
|
|
97
|
+
// now derives covers eight metrics, and it travelled as an undeclared field
|
|
98
|
+
// cast onto the DTO on one side and re-widened on the other until this landed.
|
|
99
|
+
|
|
100
|
+
test('SchedulingAnalyticsKpiDTO carries metricAvailability when provided', () => {
|
|
101
|
+
const kpi = new SchedulingAnalyticsKpiDTO({
|
|
102
|
+
impressions: 10,
|
|
103
|
+
reach: 5,
|
|
104
|
+
engagements: 4,
|
|
105
|
+
views: 20,
|
|
106
|
+
engagementRate: 1.2,
|
|
107
|
+
followerGrowth: 3,
|
|
108
|
+
metricAvailability: { reach: false, saves: true, clicks: false },
|
|
109
|
+
});
|
|
110
|
+
assert.equal(kpi.metricAvailability?.reach, false);
|
|
111
|
+
assert.equal(kpi.metricAvailability?.saves, true);
|
|
112
|
+
assert.equal(kpi.metricAvailability?.clicks, false);
|
|
113
|
+
assert.deepEqual(
|
|
114
|
+
JSON.parse(JSON.stringify(kpi)).metricAvailability,
|
|
115
|
+
{ reach: false, saves: true, clicks: false },
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('metricAvailability is optional and absent by default — absent means UNKNOWN', () => {
|
|
120
|
+
const kpi = new SchedulingAnalyticsKpiDTO({
|
|
121
|
+
impressions: 10,
|
|
122
|
+
reach: 5,
|
|
123
|
+
engagements: 4,
|
|
124
|
+
views: 20,
|
|
125
|
+
engagementRate: 1.2,
|
|
126
|
+
followerGrowth: 3,
|
|
127
|
+
});
|
|
128
|
+
assert.equal(kpi.metricAvailability, undefined);
|
|
129
|
+
assert.equal('metricAvailability' in JSON.parse(JSON.stringify(kpi)), false);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('GATED_METRICS names every metric the capability map gates, and nothing else', () => {
|
|
133
|
+
// engagements is our own roll-up and followerGrowth is a different surface —
|
|
134
|
+
// neither is platform-reported, so neither may appear here.
|
|
135
|
+
assert.deepEqual([...GATED_METRICS], [
|
|
136
|
+
'impressions',
|
|
137
|
+
'reach',
|
|
138
|
+
'likes',
|
|
139
|
+
'comments',
|
|
140
|
+
'shares',
|
|
141
|
+
'saves',
|
|
142
|
+
'clicks',
|
|
143
|
+
'views',
|
|
144
|
+
]);
|
|
145
|
+
const availability: MetricAvailability = {};
|
|
146
|
+
for (const metric of GATED_METRICS) availability[metric] = true;
|
|
147
|
+
assert.equal(Object.keys(availability).length, 8);
|
|
148
|
+
});
|
|
@@ -20,6 +20,38 @@ export const ANALYTICS_SOURCES = [
|
|
|
20
20
|
|
|
21
21
|
export type AnalyticsSource = (typeof ANALYTICS_SOURCES)[number];
|
|
22
22
|
|
|
23
|
+
// ── Per-metric structural availability (spec 111) ─────────────────────────────
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The metrics whose visibility is decided by the connected platform mix rather
|
|
27
|
+
* than by the data itself.
|
|
28
|
+
*
|
|
29
|
+
* Not every metric is here. `engagements` is our own roll-up (likes + comments
|
|
30
|
+
* + shares + saves) and `followerGrowth` comes from the follower-stats surface,
|
|
31
|
+
* so neither is platform-reported and neither is gated.
|
|
32
|
+
*/
|
|
33
|
+
export const GATED_METRICS = [
|
|
34
|
+
'impressions',
|
|
35
|
+
'reach',
|
|
36
|
+
'likes',
|
|
37
|
+
'comments',
|
|
38
|
+
'shares',
|
|
39
|
+
'saves',
|
|
40
|
+
'clicks',
|
|
41
|
+
'views',
|
|
42
|
+
] as const;
|
|
43
|
+
|
|
44
|
+
export type GatedMetric = (typeof GATED_METRICS)[number];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Which gated metrics the current platform mix can actually report.
|
|
48
|
+
*
|
|
49
|
+
* An ABSENT entry means UNKNOWN, never unavailable — the producer omits the map
|
|
50
|
+
* when it could not determine the platform mix, and a consumer must fall
|
|
51
|
+
* through to showing the figure rather than withhold on a guess.
|
|
52
|
+
*/
|
|
53
|
+
export type MetricAvailability = Partial<Record<GatedMetric, boolean>>;
|
|
54
|
+
|
|
23
55
|
// ── SchedulingAnalyticsKpiDTO ─────────────────────────────────────────────────
|
|
24
56
|
|
|
25
57
|
/**
|
|
@@ -62,6 +94,16 @@ export class SchedulingAnalyticsKpiDTO {
|
|
|
62
94
|
* Absent/true = available.
|
|
63
95
|
*/
|
|
64
96
|
reachAvailable?: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Structural availability for every gated metric, from the backend's
|
|
99
|
+
* per-platform capability map (spec 111). Supersedes the two flags above,
|
|
100
|
+
* which stay for consumers that only ever read those two.
|
|
101
|
+
*
|
|
102
|
+
* ABSENT means UNKNOWN, not unavailable: the backend omits this whole map
|
|
103
|
+
* when it could not determine the connected platform mix, and a consumer
|
|
104
|
+
* must show the figure rather than withhold on a guess.
|
|
105
|
+
*/
|
|
106
|
+
metricAvailability?: MetricAvailability;
|
|
65
107
|
|
|
66
108
|
constructor(data: SchedulingAnalyticsKpiDTO) {
|
|
67
109
|
this.impressions = data.impressions;
|
|
@@ -82,6 +124,9 @@ export class SchedulingAnalyticsKpiDTO {
|
|
|
82
124
|
if (data.reachAvailable !== undefined) {
|
|
83
125
|
this.reachAvailable = data.reachAvailable;
|
|
84
126
|
}
|
|
127
|
+
if (data.metricAvailability !== undefined) {
|
|
128
|
+
this.metricAvailability = data.metricAvailability;
|
|
129
|
+
}
|
|
85
130
|
}
|
|
86
131
|
}
|
|
87
132
|
|