ad2app-lib 1.15.0 → 1.20.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 +206 -0
- package/dist/analytics/index.js +41 -0
- package/dist/legal/privacy.js +1 -1
- package/dist/types/scheduling/I_SchedulingInbox.d.ts +165 -0
- package/dist/types/scheduling/I_SchedulingInbox.js +186 -1
- package/dist/types/scheduling/I_SchedulingPost.d.ts +16 -2
- package/dist/types/scheduling/I_SchedulingPost.js +9 -0
- package/package.json +4 -3
- package/src/analytics/index.test.ts +199 -2
- package/src/analytics/index.ts +187 -1
- package/src/legal/privacy.ts +1 -1
- package/src/types/scheduling/I_SchedulingInbox.test.ts +218 -1
- package/src/types/scheduling/I_SchedulingInbox.ts +289 -0
- package/src/types/scheduling/I_SchedulingPost.test.ts +24 -0
- package/src/types/scheduling/I_SchedulingPost.ts +13 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* the connected social accounts.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.SchedulingReplyToCommentDTO = exports.SchedulingSendMessageDTO = exports.SchedulingInboxCommentDTO = exports.SchedulingInboxPostWithCommentsDTO = exports.SchedulingInboxConversationDTO = void 0;
|
|
9
|
+
exports.SchedulingCrossPostCommentDTO = exports.SchedulingInboxCapabilityDTO = exports.SchedulingCreateConversationDTO = exports.SchedulingCommentModerationDTO = exports.SchedulingReplyToReviewDTO = exports.SchedulingInboxReviewDTO = exports.SchedulingReplyToMentionDTO = exports.SchedulingInboxMentionDTO = exports.SchedulingConversationReadDTO = exports.SchedulingInboxMessagePageDTO = exports.SchedulingInboxMessageDTO = exports.SchedulingReplyToCommentDTO = exports.SchedulingSendMessageDTO = exports.SchedulingInboxCommentDTO = exports.SchedulingInboxPostWithCommentsDTO = exports.SchedulingInboxConversationDTO = void 0;
|
|
10
10
|
// ── SchedulingInboxConversationDTO ────────────────────────────────────────────
|
|
11
11
|
/**
|
|
12
12
|
* A DM conversation (thread) entry.
|
|
@@ -60,6 +60,7 @@ class SchedulingInboxCommentDTO {
|
|
|
60
60
|
this.isOwner = data.isOwner;
|
|
61
61
|
this.parentId = data.parentId;
|
|
62
62
|
this.replies = data.replies;
|
|
63
|
+
this.canModerate = data.canModerate;
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
66
|
exports.SchedulingInboxCommentDTO = SchedulingInboxCommentDTO;
|
|
@@ -84,3 +85,187 @@ class SchedulingReplyToCommentDTO {
|
|
|
84
85
|
}
|
|
85
86
|
}
|
|
86
87
|
exports.SchedulingReplyToCommentDTO = SchedulingReplyToCommentDTO;
|
|
88
|
+
// ── 082 Inbox Parity — new inbox surfaces (US1) ───────────────────────────────
|
|
89
|
+
//
|
|
90
|
+
// Shapes below mirror the Zernio OpenAPI v1.0.4 inbox surface (line anchors in
|
|
91
|
+
// each comment); ad2app must NOT invent fields (AD2-1038). Availability is
|
|
92
|
+
// honest-or-absent: an optional field is absent when Zernio does not report it,
|
|
93
|
+
// never zero/false-backfilled.
|
|
94
|
+
// ── SchedulingInboxMessageDTO / SchedulingInboxMessagePageDTO ──────────────────
|
|
95
|
+
/**
|
|
96
|
+
* One message inside a DM thread.
|
|
97
|
+
* Returned (paginated) by GET /social/inbox/messages/:id/thread — backed by
|
|
98
|
+
* Zernio GET /v1/inbox/conversations/{id}/messages (OpenAPI v1.0.4:20845).
|
|
99
|
+
*/
|
|
100
|
+
class SchedulingInboxMessageDTO {
|
|
101
|
+
constructor(data) {
|
|
102
|
+
this.id = data.id;
|
|
103
|
+
this.conversationId = data.conversationId;
|
|
104
|
+
this.authorId = data.authorId;
|
|
105
|
+
this.authorName = data.authorName;
|
|
106
|
+
this.text = data.text;
|
|
107
|
+
this.createdAt = data.createdAt;
|
|
108
|
+
this.isOwn = data.isOwn;
|
|
109
|
+
this.mediaUrl = data.mediaUrl;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
exports.SchedulingInboxMessageDTO = SchedulingInboxMessageDTO;
|
|
113
|
+
/**
|
|
114
|
+
* A cursor page of thread messages — kills the prior 100-message truncation.
|
|
115
|
+
* `nextCursor` absent = last page.
|
|
116
|
+
*/
|
|
117
|
+
class SchedulingInboxMessagePageDTO {
|
|
118
|
+
constructor(data) {
|
|
119
|
+
this.items = data.items;
|
|
120
|
+
this.nextCursor = data.nextCursor;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
exports.SchedulingInboxMessagePageDTO = SchedulingInboxMessagePageDTO;
|
|
124
|
+
// ── SchedulingConversationReadDTO ─────────────────────────────────────────────
|
|
125
|
+
/**
|
|
126
|
+
* Mark-read acknowledgement — POST /social/inbox/messages/:id/read → Zernio
|
|
127
|
+
* POST /v1/inbox/conversations/{id}/read (OpenAPI v1.0.4:21692). `unreadCount`
|
|
128
|
+
* absent unless Zernio reports the post-read count.
|
|
129
|
+
*/
|
|
130
|
+
class SchedulingConversationReadDTO {
|
|
131
|
+
constructor(data) {
|
|
132
|
+
this.conversationId = data.conversationId;
|
|
133
|
+
this.unreadCount = data.unreadCount;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
exports.SchedulingConversationReadDTO = SchedulingConversationReadDTO;
|
|
137
|
+
// ── SchedulingInboxMentionDTO / SchedulingReplyToMentionDTO ────────────────────
|
|
138
|
+
/**
|
|
139
|
+
* An inbound mention.
|
|
140
|
+
* Returned by GET /social/inbox/mentions → Zernio GET /v1/inbox/mentions
|
|
141
|
+
* (OpenAPI v1.0.4:23108). Source post fields absent when the platform omits them.
|
|
142
|
+
*/
|
|
143
|
+
class SchedulingInboxMentionDTO {
|
|
144
|
+
constructor(data) {
|
|
145
|
+
this.id = data.id;
|
|
146
|
+
this.platform = data.platform;
|
|
147
|
+
this.accountId = data.accountId;
|
|
148
|
+
this.authorName = data.authorName;
|
|
149
|
+
this.authorAvatarUrl = data.authorAvatarUrl;
|
|
150
|
+
this.text = data.text;
|
|
151
|
+
this.createdAt = data.createdAt;
|
|
152
|
+
this.sourcePostId = data.sourcePostId;
|
|
153
|
+
this.sourcePostUrl = data.sourcePostUrl;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
exports.SchedulingInboxMentionDTO = SchedulingInboxMentionDTO;
|
|
157
|
+
/**
|
|
158
|
+
* Input for replying to a mention. Zernio requires [accountId, mediaId, message]
|
|
159
|
+
* (OpenAPI v1.0.4:23207) — `mediaId` (the mention's target media) is mandatory,
|
|
160
|
+
* unlike a DM reply.
|
|
161
|
+
*/
|
|
162
|
+
class SchedulingReplyToMentionDTO {
|
|
163
|
+
constructor(data) {
|
|
164
|
+
this.accountId = data.accountId;
|
|
165
|
+
this.mediaId = data.mediaId;
|
|
166
|
+
this.message = data.message;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
exports.SchedulingReplyToMentionDTO = SchedulingReplyToMentionDTO;
|
|
170
|
+
// ── SchedulingInboxReviewDTO / SchedulingReplyToReviewDTO ──────────────────────
|
|
171
|
+
/**
|
|
172
|
+
* A platform review (e.g. Google Business).
|
|
173
|
+
* Returned by GET /social/inbox/reviews → Zernio GET /v1/inbox/reviews
|
|
174
|
+
* (OpenAPI v1.0.4:23269). `replied` absent unless Zernio reports reply state.
|
|
175
|
+
*/
|
|
176
|
+
class SchedulingInboxReviewDTO {
|
|
177
|
+
constructor(data) {
|
|
178
|
+
this.id = data.id;
|
|
179
|
+
this.platform = data.platform;
|
|
180
|
+
this.accountId = data.accountId;
|
|
181
|
+
this.authorName = data.authorName;
|
|
182
|
+
this.rating = data.rating;
|
|
183
|
+
this.text = data.text;
|
|
184
|
+
this.createdAt = data.createdAt;
|
|
185
|
+
this.replied = data.replied;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
exports.SchedulingInboxReviewDTO = SchedulingInboxReviewDTO;
|
|
189
|
+
/**
|
|
190
|
+
* Input for replying to a review — POST /social/inbox/reviews/:reviewId/reply →
|
|
191
|
+
* Zernio POST /v1/inbox/reviews/{reviewId}/reply (OpenAPI v1.0.4:23377).
|
|
192
|
+
*/
|
|
193
|
+
class SchedulingReplyToReviewDTO {
|
|
194
|
+
constructor(data) {
|
|
195
|
+
this.reviewId = data.reviewId;
|
|
196
|
+
this.accountId = data.accountId;
|
|
197
|
+
this.text = data.text;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
exports.SchedulingReplyToReviewDTO = SchedulingReplyToReviewDTO;
|
|
201
|
+
class SchedulingCommentModerationDTO {
|
|
202
|
+
constructor(data) {
|
|
203
|
+
this.moderationStatus = data.moderationStatus;
|
|
204
|
+
this.banAuthor = data.banAuthor;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
exports.SchedulingCommentModerationDTO = SchedulingCommentModerationDTO;
|
|
208
|
+
// ── SchedulingCreateConversationDTO ───────────────────────────────────────────
|
|
209
|
+
/**
|
|
210
|
+
* Input for starting a new DM — POST /social/inbox/conversations → Zernio
|
|
211
|
+
* POST /v1/inbox/conversations (OpenAPI v1.0.4:20355). WhatsApp is excluded
|
|
212
|
+
* (it requires an approved template to open a thread; 082 non-goal).
|
|
213
|
+
*/
|
|
214
|
+
class SchedulingCreateConversationDTO {
|
|
215
|
+
constructor(data) {
|
|
216
|
+
this.accountId = data.accountId;
|
|
217
|
+
this.recipientId = data.recipientId;
|
|
218
|
+
this.text = data.text;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
exports.SchedulingCreateConversationDTO = SchedulingCreateConversationDTO;
|
|
222
|
+
// ── SchedulingInboxCapabilityDTO ──────────────────────────────────────────────
|
|
223
|
+
/**
|
|
224
|
+
* Per-connected-account inbox capability truth — which surfaces a platform
|
|
225
|
+
* actually supports. Drives honest showing/hiding (082 FR-4): a false/absent
|
|
226
|
+
* surface is HIDDEN, never rendered as a fabricated empty. Reconciled from
|
|
227
|
+
* platform-capabilities + Zernio /accounts/health.
|
|
228
|
+
*/
|
|
229
|
+
class SchedulingInboxCapabilityDTO {
|
|
230
|
+
constructor(data) {
|
|
231
|
+
this.platform = data.platform;
|
|
232
|
+
this.dm = data.dm;
|
|
233
|
+
this.comments = data.comments;
|
|
234
|
+
this.mentions = data.mentions;
|
|
235
|
+
this.reviews = data.reviews;
|
|
236
|
+
this.dmInit = data.dmInit;
|
|
237
|
+
this.moderation = data.moderation;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
exports.SchedulingInboxCapabilityDTO = SchedulingInboxCapabilityDTO;
|
|
241
|
+
// ── SchedulingCrossPostCommentDTO ─────────────────────────────────────────────
|
|
242
|
+
/**
|
|
243
|
+
* A comment re-sourced from Zernio's cross-post inbox (GET /v1/inbox/comments),
|
|
244
|
+
* carrying its OWN post attribution so it surfaces even when the post is absent
|
|
245
|
+
* from our local SocialPostCache. The cache is enrichment ONLY (post snippet /
|
|
246
|
+
* thumbnail), never the source of truth (082 US3 / AD2-1110): a comment on an
|
|
247
|
+
* uncached post must still reach the moderator. Comment/engagement fields mirror
|
|
248
|
+
* SchedulingInboxCommentDTO; enrichment fields are honest-or-absent — filled from
|
|
249
|
+
* the cache when present, never zero/empty-backfilled.
|
|
250
|
+
*/
|
|
251
|
+
class SchedulingCrossPostCommentDTO {
|
|
252
|
+
constructor(data) {
|
|
253
|
+
this.id = data.id;
|
|
254
|
+
this.authorName = data.authorName;
|
|
255
|
+
this.authorAvatarUrl = data.authorAvatarUrl;
|
|
256
|
+
this.text = data.text;
|
|
257
|
+
this.createdAt = data.createdAt;
|
|
258
|
+
this.likeCount = data.likeCount;
|
|
259
|
+
this.replyCount = data.replyCount;
|
|
260
|
+
this.isOwner = data.isOwner;
|
|
261
|
+
this.parentId = data.parentId;
|
|
262
|
+
this.canModerate = data.canModerate;
|
|
263
|
+
this.replies = data.replies;
|
|
264
|
+
this.postId = data.postId;
|
|
265
|
+
this.platform = data.platform;
|
|
266
|
+
this.accountId = data.accountId;
|
|
267
|
+
this.postContentSnippet = data.postContentSnippet;
|
|
268
|
+
this.postThumbnailUrl = data.postThumbnailUrl;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
exports.SchedulingCrossPostCommentDTO = SchedulingCrossPostCommentDTO;
|
|
@@ -9,9 +9,17 @@ export declare enum SchedulingPostStatus {
|
|
|
9
9
|
SCHEDULED = "scheduled",
|
|
10
10
|
PUBLISHED = "published",
|
|
11
11
|
FAILED = "failed",
|
|
12
|
-
PARTIAL = "partial"
|
|
12
|
+
PARTIAL = "partial",
|
|
13
|
+
/**
|
|
14
|
+
* The publisher accepted the post (an id exists) but its outcome could not
|
|
15
|
+
* be confirmed inline — a gateway timeout, or a 409 whose live status
|
|
16
|
+
* couldn't be fetched immediately (AD2-1153). Distinct from SCHEDULED: a
|
|
17
|
+
* PUBLISHING post may already be publishing server-side and must not be
|
|
18
|
+
* treated as still-editable.
|
|
19
|
+
*/
|
|
20
|
+
PUBLISHING = "publishing"
|
|
13
21
|
}
|
|
14
|
-
export type SchedulingPlatformStatusMap = Record<string, 'scheduled' | 'published' | 'failed'>;
|
|
22
|
+
export type SchedulingPlatformStatusMap = Record<string, 'scheduled' | 'published' | 'failed' | 'unknown'>;
|
|
15
23
|
export declare class SchedulingPlatformTargetDTO {
|
|
16
24
|
platform: string;
|
|
17
25
|
accountId: string;
|
|
@@ -86,6 +94,12 @@ export declare class SchedulingPostDTO {
|
|
|
86
94
|
postUrls?: Record<string, string>;
|
|
87
95
|
/** Per-platform error messages for platforms that failed to publish. */
|
|
88
96
|
platformErrors?: Record<string, string>;
|
|
97
|
+
/**
|
|
98
|
+
* Per-platform retry-attempt counts (AD2-1154 FR-3a). A platform that has
|
|
99
|
+
* used its 3rd attempt and is still failed is terminal — the retry
|
|
100
|
+
* affordance withdraws and the last real error is shown as permanent.
|
|
101
|
+
*/
|
|
102
|
+
platformRetryCounts?: Record<string, number>;
|
|
89
103
|
scheduledAt?: string;
|
|
90
104
|
publishedAt?: string;
|
|
91
105
|
status: SchedulingPostStatus;
|
|
@@ -15,6 +15,14 @@ var SchedulingPostStatus;
|
|
|
15
15
|
SchedulingPostStatus["PUBLISHED"] = "published";
|
|
16
16
|
SchedulingPostStatus["FAILED"] = "failed";
|
|
17
17
|
SchedulingPostStatus["PARTIAL"] = "partial";
|
|
18
|
+
/**
|
|
19
|
+
* The publisher accepted the post (an id exists) but its outcome could not
|
|
20
|
+
* be confirmed inline — a gateway timeout, or a 409 whose live status
|
|
21
|
+
* couldn't be fetched immediately (AD2-1153). Distinct from SCHEDULED: a
|
|
22
|
+
* PUBLISHING post may already be publishing server-side and must not be
|
|
23
|
+
* treated as still-editable.
|
|
24
|
+
*/
|
|
25
|
+
SchedulingPostStatus["PUBLISHING"] = "publishing";
|
|
18
26
|
})(SchedulingPostStatus || (exports.SchedulingPostStatus = SchedulingPostStatus = {}));
|
|
19
27
|
// ── SchedulingPlatformTargetDTO ───────────────────────────────────────────────
|
|
20
28
|
class SchedulingPlatformTargetDTO {
|
|
@@ -114,6 +122,7 @@ class SchedulingPostDTO {
|
|
|
114
122
|
this.platformStatuses = data.platformStatuses;
|
|
115
123
|
this.postUrls = data.postUrls;
|
|
116
124
|
this.platformErrors = data.platformErrors;
|
|
125
|
+
this.platformRetryCounts = data.platformRetryCounts;
|
|
117
126
|
this.scheduledAt = data.scheduledAt;
|
|
118
127
|
this.publishedAt = data.publishedAt;
|
|
119
128
|
this.status = data.status;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ad2app-lib",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -43,10 +43,11 @@
|
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
45
|
"build": "npx tsc",
|
|
46
|
-
"test": "node --import tsx --test $(find src -name '*.test.ts')"
|
|
46
|
+
"test": "node --import tsx --test $(find src -name '*.test.ts')",
|
|
47
|
+
"prepare": "npm run build"
|
|
47
48
|
},
|
|
48
49
|
"keywords": [],
|
|
49
|
-
"author": "Maciej
|
|
50
|
+
"author": "Maciej G\u00f3rski@ad2.app",
|
|
50
51
|
"license": "ISC",
|
|
51
52
|
"description": "Package to share types and utils across the ad2app projects",
|
|
52
53
|
"dependencies": {
|
|
@@ -9,7 +9,7 @@ import assert from "node:assert/strict";
|
|
|
9
9
|
import { test } from "node:test";
|
|
10
10
|
|
|
11
11
|
import { EVENTS, PERSON_PROPS } from "./index";
|
|
12
|
-
import type { EventProperties, EmailEventProperties } from "./index";
|
|
12
|
+
import type { EventProperties, EmailEventProperties, TriggerSource, PublishFailureReason } from "./index";
|
|
13
13
|
|
|
14
14
|
const SNAKE_CASE = /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/;
|
|
15
15
|
|
|
@@ -77,13 +77,45 @@ const EVENT_PROPERTY_WITNESS: { [E in keyof EventProperties]: EventProperties[E]
|
|
|
77
77
|
[EVENTS.SOCIAL_ACCOUNT_CONNECTED]: { platform: "instagram" },
|
|
78
78
|
[EVENTS.COMPOSE_STARTED]: {},
|
|
79
79
|
[EVENTS.POST_SCHEDULED]: { platforms: ["instagram"], platform_count: 1 },
|
|
80
|
-
[EVENTS.POST_PUBLISHED]: { platforms: ["instagram"], platform_count: 1, status: "success" },
|
|
80
|
+
[EVENTS.POST_PUBLISHED]: { platforms: ["instagram"], platform_count: 1, status: "success", platform: "instagram" },
|
|
81
|
+
[EVENTS.PUBLISH_FAILED]: { platform: "instagram", reason_class: "auth", post_id: "post-1", platform_count: 1 },
|
|
81
82
|
[EVENTS.ANALYTICS_VIEWED]: {},
|
|
82
83
|
[EVENTS.LIMIT_REACHED]: { limit: "accounts", plan: "free", cap: 1 },
|
|
83
84
|
[EVENTS.UPGRADE_CLICKED]: { surface: "compose" },
|
|
84
85
|
[EVENTS.CHECKOUT_STARTED]: { plan: "starter" },
|
|
85
86
|
[EVENTS.SUBSCRIPTION_STARTED]: { plan: "starter" },
|
|
86
87
|
[EVENTS.SUBSCRIPTION_CANCELED]: { plan: "starter" },
|
|
88
|
+
// 080 measurement baseline
|
|
89
|
+
[EVENTS.PAYWALL_SHOWN]: { surface: "upgrade_modal", trigger: "user_click", hit_number: 1, app_locale: "en" },
|
|
90
|
+
[EVENTS.PAYWALL_DISMISSED]: { surface: "upgrade_modal", trigger: "user_click", hit_number: 1, app_locale: "en" },
|
|
91
|
+
[EVENTS.ONBOARDING_STEP_COMPLETED]: { flow: "complete_profile", step: "role", step_index: 1, app_locale: "en" },
|
|
92
|
+
[EVENTS.EXIT_INTENT_SHOWN]: { surface: "connect_wall" },
|
|
93
|
+
[EVENTS.EXIT_INTENT_DISMISSED]: { surface: "connect_wall" },
|
|
94
|
+
[EVENTS.EXIT_INTENT_CAPTURED]: { surface: "connect_wall" },
|
|
95
|
+
[EVENTS.BETA_SIGNUP_CONFIRMED]: { source: "exit_intent" },
|
|
96
|
+
[EVENTS.PAYMENT_FAILED]: { plan: "starter", attempt: 1 },
|
|
97
|
+
[EVENTS.DUNNING_RECOVERED]: { plan: "starter", attempts: 2, days_late: 3 },
|
|
98
|
+
[EVENTS.REFUND_ISSUED]: { plan: "starter", days_since_start: 10 },
|
|
99
|
+
[EVENTS.DISPUTE_OPENED]: { plan: "starter", days_since_start: 10 },
|
|
100
|
+
[EVENTS.CANCEL_INITIATED]: { plan: "starter", days_since_start: 30 },
|
|
101
|
+
[EVENTS.RETENTION_OFFER_SHOWN]: { plan: "starter", offer: "discount_20" },
|
|
102
|
+
[EVENTS.CANCEL_COMPLETED]: { plan: "starter", days_since_start: 30 },
|
|
103
|
+
// Onboarding + inbox + post-lifecycle coverage (2026-07-19)
|
|
104
|
+
[EVENTS.ONBOARDING_STARTED]: {},
|
|
105
|
+
[EVENTS.ONBOARDING_COMPLETED]: {},
|
|
106
|
+
[EVENTS.ONBOARDING_SKIPPED]: {},
|
|
107
|
+
[EVENTS.COMMENT_REPLIED]: { success: true },
|
|
108
|
+
[EVENTS.COMMENT_LIKED]: { liked: true, success: true },
|
|
109
|
+
[EVENTS.COMMENT_HIDDEN]: { hidden: true, success: true },
|
|
110
|
+
[EVENTS.COMMENT_DELETED]: { success: true },
|
|
111
|
+
[EVENTS.COMMENT_PRIVATE_REPLIED]: { success: true },
|
|
112
|
+
[EVENTS.DM_REPLIED]: { success: true },
|
|
113
|
+
[EVENTS.INBOX_MARKED_READ]: { success: true },
|
|
114
|
+
[EVENTS.POST_EDITED]: { success: true },
|
|
115
|
+
[EVENTS.POST_DELETED]: { success: true },
|
|
116
|
+
[EVENTS.POST_RESCHEDULED]: { success: true },
|
|
117
|
+
[EVENTS.ACCOUNT_DISCONNECTED]: { platform: "instagram", success: true },
|
|
118
|
+
[EVENTS.NOTIFICATION_PREFERENCES_UPDATED]: { success: true },
|
|
87
119
|
};
|
|
88
120
|
|
|
89
121
|
test("EVENTS values are 1:1 with EventProperties keys (no missing or typo'd event)", () => {
|
|
@@ -119,3 +151,168 @@ test("the 5 EMAIL_* events are locked and share the EmailEventProperties shape (
|
|
|
119
151
|
}
|
|
120
152
|
}
|
|
121
153
|
});
|
|
154
|
+
|
|
155
|
+
// ── 080 measurement baseline — wire values are FINAL once shipped ───────────────
|
|
156
|
+
|
|
157
|
+
test("080 paywall / onboarding wire names are locked (rename breaks the baseline dashboards)", () => {
|
|
158
|
+
assert.equal(EVENTS.PAYWALL_SHOWN, "paywall_shown");
|
|
159
|
+
assert.equal(EVENTS.PAYWALL_DISMISSED, "paywall_dismissed");
|
|
160
|
+
assert.equal(EVENTS.ONBOARDING_STEP_COMPLETED, "onboarding_step_completed");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("080 subscription-lifecycle wire names are locked (backend = source of truth)", () => {
|
|
164
|
+
assert.deepEqual(
|
|
165
|
+
[
|
|
166
|
+
EVENTS.BETA_SIGNUP_CONFIRMED,
|
|
167
|
+
EVENTS.PAYMENT_FAILED,
|
|
168
|
+
EVENTS.DUNNING_RECOVERED,
|
|
169
|
+
EVENTS.REFUND_ISSUED,
|
|
170
|
+
EVENTS.DISPUTE_OPENED,
|
|
171
|
+
EVENTS.CANCEL_INITIATED,
|
|
172
|
+
EVENTS.RETENTION_OFFER_SHOWN,
|
|
173
|
+
EVENTS.CANCEL_COMPLETED,
|
|
174
|
+
],
|
|
175
|
+
[
|
|
176
|
+
"beta_signup_confirmed",
|
|
177
|
+
"payment_failed",
|
|
178
|
+
"dunning_recovered",
|
|
179
|
+
"refund_issued",
|
|
180
|
+
"dispute_opened",
|
|
181
|
+
"cancel_initiated",
|
|
182
|
+
"retention_offer_shown",
|
|
183
|
+
"cancel_completed",
|
|
184
|
+
],
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Fidelity gate (T001): the exit_intent_* wire values are adopted VERBATIM from the
|
|
189
|
+
// 065 ExitIntentModal component. Verified against
|
|
190
|
+
// ad2app-frontend feature/ad2-1132-exit-intent
|
|
191
|
+
// src/components/social/exit-intent/ExitIntentModal.tsx —
|
|
192
|
+
// L38 const SURFACE = 'connect_wall'
|
|
193
|
+
// L67 trackLocal('exit_intent_shown', { surface: SURFACE })
|
|
194
|
+
// L79 trackLocal('exit_intent_dismissed', { surface: SURFACE })
|
|
195
|
+
// L99 trackLocal('exit_intent_captured', { surface: SURFACE })
|
|
196
|
+
// T003 swaps those literals to these constants; the values must not drift here.
|
|
197
|
+
test("080 exit_intent_* wire values match the 065 component verbatim (fidelity gate)", () => {
|
|
198
|
+
assert.equal(EVENTS.EXIT_INTENT_SHOWN, "exit_intent_shown");
|
|
199
|
+
assert.equal(EVENTS.EXIT_INTENT_DISMISSED, "exit_intent_dismissed");
|
|
200
|
+
assert.equal(EVENTS.EXIT_INTENT_CAPTURED, "exit_intent_captured");
|
|
201
|
+
const shown: EventProperties["exit_intent_shown"] = { surface: "connect_wall" };
|
|
202
|
+
assert.equal(shown.surface, "connect_wall");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("080 PAYWALL_HITS person prop is locked; hit_number stays the event-level truth", () => {
|
|
206
|
+
assert.equal(PERSON_PROPS.PAYWALL_HITS, "paywall_hits");
|
|
207
|
+
// The analytical counter lives on the event (person props are query-time-latest).
|
|
208
|
+
const shown: EventProperties["paywall_shown"] = {
|
|
209
|
+
surface: "upgrade_modal",
|
|
210
|
+
trigger: "limit_reached",
|
|
211
|
+
hit_number: 3,
|
|
212
|
+
app_locale: "pl",
|
|
213
|
+
};
|
|
214
|
+
assert.equal(shown.hit_number, 3);
|
|
215
|
+
assert.equal(shown.app_locale, "pl");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("080 trigger_source closed set attributes the revenue funnel across the three intent events", () => {
|
|
219
|
+
const sources: TriggerSource[] = [
|
|
220
|
+
"connect_wall",
|
|
221
|
+
"limit_reached",
|
|
222
|
+
"exit_intent",
|
|
223
|
+
"pricing_page",
|
|
224
|
+
"checkout_abandon_email",
|
|
225
|
+
"other",
|
|
226
|
+
];
|
|
227
|
+
// Each intent event accepts a trigger_source from the shared closed set.
|
|
228
|
+
const upgrade: EventProperties["upgrade_clicked"] = { surface: "compose", trigger_source: "connect_wall" };
|
|
229
|
+
const checkout: EventProperties["checkout_started"] = { plan: "pro", trigger_source: "exit_intent" };
|
|
230
|
+
const started: EventProperties["subscription_started"] = { plan: "pro", trigger_source: "checkout_abandon_email" };
|
|
231
|
+
assert.ok(sources.includes(upgrade.trigger_source!));
|
|
232
|
+
assert.ok(sources.includes(checkout.trigger_source!));
|
|
233
|
+
assert.ok(sources.includes(started.trigger_source!));
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* AD2-999 — per-platform publish instrumentation. Locks the new publish_failed
|
|
238
|
+
* wire name, its payload shape, the reason_class value set, and the optional
|
|
239
|
+
* `platform` attribution added to post_published.
|
|
240
|
+
*/
|
|
241
|
+
|
|
242
|
+
test("post_published and publish_failed wire names are locked (AD2-999)", () => {
|
|
243
|
+
assert.equal(EVENTS.POST_PUBLISHED, "post_published");
|
|
244
|
+
assert.equal(EVENTS.PUBLISH_FAILED, "publish_failed");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("publish_failed carries exactly platform, reason_class, post_id, platform_count (AD2-999)", () => {
|
|
248
|
+
const witness: EventProperties[typeof EVENTS.PUBLISH_FAILED] =
|
|
249
|
+
EVENT_PROPERTY_WITNESS[EVENTS.PUBLISH_FAILED];
|
|
250
|
+
assert.deepEqual(Object.keys(witness).sort(), [
|
|
251
|
+
"platform",
|
|
252
|
+
"platform_count",
|
|
253
|
+
"post_id",
|
|
254
|
+
"reason_class",
|
|
255
|
+
]);
|
|
256
|
+
assert.equal(typeof witness.platform, "string");
|
|
257
|
+
assert.equal(typeof witness.post_id, "string");
|
|
258
|
+
assert.equal(typeof witness.platform_count, "number");
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("PublishFailureReason is a stable closed set of snake_case buckets (AD2-999)", () => {
|
|
262
|
+
// The union is compile-time; this pins the wire values a renamed bucket would
|
|
263
|
+
// break. Kept in sync with the exported type via the typed `reasons` array —
|
|
264
|
+
// an added/removed union member fails `tsc --noEmit`, a renamed value fails here.
|
|
265
|
+
const reasons: PublishFailureReason[] = [
|
|
266
|
+
"auth",
|
|
267
|
+
"rate_limit",
|
|
268
|
+
"media",
|
|
269
|
+
"content",
|
|
270
|
+
"network",
|
|
271
|
+
"platform",
|
|
272
|
+
"unknown",
|
|
273
|
+
];
|
|
274
|
+
assert.equal(new Set(reasons).size, reasons.length, "duplicate reason_class");
|
|
275
|
+
for (const reason of reasons) assert.match(reason, SNAKE_CASE, `not snake_case: ${reason}`);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("post_published accepts optional single-platform attribution (AD2-999)", () => {
|
|
279
|
+
const single: EventProperties[typeof EVENTS.POST_PUBLISHED] = {
|
|
280
|
+
platforms: ["instagram"],
|
|
281
|
+
platform_count: 1,
|
|
282
|
+
status: "success",
|
|
283
|
+
platform: "instagram",
|
|
284
|
+
};
|
|
285
|
+
const multi: EventProperties[typeof EVENTS.POST_PUBLISHED] = {
|
|
286
|
+
platforms: ["instagram", "tiktok"],
|
|
287
|
+
platform_count: 2,
|
|
288
|
+
status: "partial",
|
|
289
|
+
};
|
|
290
|
+
assert.equal(single.platform, "instagram");
|
|
291
|
+
assert.equal(multi.platform, undefined);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// AD2-1154 FR-5 — Paweł's incident: post_published fired with NO platform
|
|
295
|
+
// property, so a partial publish (YT ok / TikTok fail) couldn't be sliced by
|
|
296
|
+
// which platform actually failed in PostHog. These are OPTIONAL additions —
|
|
297
|
+
// existing aggregate-only emitters (platforms/platform_count/status) stay
|
|
298
|
+
// valid; a future consumer swap fans this event out once per platform.
|
|
299
|
+
test("POST_PUBLISHED gains optional per-platform properties (AD2-1154 FR-5 lib prep)", () => {
|
|
300
|
+
const aggregateOnly: EventProperties[typeof EVENTS.POST_PUBLISHED] = {
|
|
301
|
+
platforms: ["youtube", "tiktok"],
|
|
302
|
+
platform_count: 2,
|
|
303
|
+
status: "partial",
|
|
304
|
+
};
|
|
305
|
+
assert.equal(aggregateOnly.platform, undefined);
|
|
306
|
+
|
|
307
|
+
const perPlatform: EventProperties[typeof EVENTS.POST_PUBLISHED] = {
|
|
308
|
+
platforms: ["tiktok"],
|
|
309
|
+
platform_count: 1,
|
|
310
|
+
status: "failed",
|
|
311
|
+
platform: "tiktok",
|
|
312
|
+
outcome: "failed",
|
|
313
|
+
reason_class: "media",
|
|
314
|
+
};
|
|
315
|
+
assert.equal(perPlatform.platform, "tiktok");
|
|
316
|
+
assert.equal(perPlatform.outcome, "failed");
|
|
317
|
+
assert.equal(perPlatform.reason_class, "media");
|
|
318
|
+
});
|