ab-ecosystem-cpass 1.0.5 → 1.0.7
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 +62 -1
- package/dist/client.d.ts +24 -0
- package/dist/client.js +32 -5
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.js +20 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/bytes.d.ts +38 -0
- package/dist/internal/bytes.js +84 -0
- package/dist/internal/bytes.js.map +1 -0
- package/dist/internal/device.d.ts +24 -0
- package/dist/internal/device.js +91 -0
- package/dist/internal/device.js.map +1 -0
- package/dist/internal/md5.d.ts +4 -0
- package/dist/internal/md5.js +109 -0
- package/dist/internal/md5.js.map +1 -0
- package/dist/internal/receiver-file.d.ts +106 -0
- package/dist/internal/receiver-file.js +128 -0
- package/dist/internal/receiver-file.js.map +1 -0
- package/dist/resources/audience/notifications.d.ts +171 -0
- package/dist/resources/audience/notifications.js +215 -0
- package/dist/resources/audience/notifications.js.map +1 -0
- package/dist/resources/campaign-audience.d.ts +132 -0
- package/dist/resources/campaign-audience.js +224 -0
- package/dist/resources/campaign-audience.js.map +1 -0
- package/dist/resources/campaign-messages.d.ts +150 -0
- package/dist/resources/campaign-messages.js +172 -0
- package/dist/resources/campaign-messages.js.map +1 -0
- package/dist/resources/campaigns.d.ts +239 -0
- package/dist/resources/campaigns.js +300 -0
- package/dist/resources/campaigns.js.map +1 -0
- package/dist/types/audience.d.ts +82 -0
- package/dist/types/campaign.d.ts +319 -0
- package/dist/types/campaign.js +3 -0
- package/dist/types/campaign.js.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -1
- package/docs/audience/notifications.md +263 -0
- package/docs/campaign/campaign-messages.md +273 -0
- package/docs/campaign/campaigns.md +449 -0
- package/package.json +1 -1
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# Campaign messages (reporting)
|
|
2
|
+
|
|
3
|
+
`crm.campaignMessages` — `/v1/campaign-messages`
|
|
4
|
+
|
|
5
|
+
What actually happened to each message a campaign sent. One row per attempted
|
|
6
|
+
send: who it went to, how far it got, what came back, and why it failed.
|
|
7
|
+
|
|
8
|
+
| Action | Route | Scope |
|
|
9
|
+
| --- | --- | --- |
|
|
10
|
+
| [`stats`](#stats) | `GET /v1/campaign-messages/stats` | `campaigns:read` |
|
|
11
|
+
| [`list`](#list) | `GET /v1/campaign-messages` | `campaigns:read` |
|
|
12
|
+
| [`iterate`](#iterate) | the same, paged for you | `campaigns:read` |
|
|
13
|
+
| [`replies`](#replies) | `GET /v1/campaign-messages/replies` | `campaigns:read` |
|
|
14
|
+
|
|
15
|
+
## Where these numbers come from
|
|
16
|
+
|
|
17
|
+
The campaign manager writes one row as it hands each message to a channel. The
|
|
18
|
+
channel's delivery webhooks — processed **outside the CRM** — then move that
|
|
19
|
+
row's `status` forward as the message is delivered, read and replied to.
|
|
20
|
+
|
|
21
|
+
So reporting is a **read of those rows, counted when you ask**. There is no
|
|
22
|
+
report collection, no roll-up job, and nothing to regenerate: no `generatedAt` to
|
|
23
|
+
reason about, and no window in which a stored number is quietly wrong because a
|
|
24
|
+
webhook has arrived but the roll-up has not run.
|
|
25
|
+
|
|
26
|
+
The flip side is that a campaign still going out has moving numbers. Reading
|
|
27
|
+
`stats` seconds after launch under-counts everything, and that is the truth
|
|
28
|
+
rather than a lag.
|
|
29
|
+
|
|
30
|
+
Nothing in this module writes. There is no endpoint to correct a status: the
|
|
31
|
+
service that owns the lifecycle is the one receiving the webhooks.
|
|
32
|
+
|
|
33
|
+
## The funnel
|
|
34
|
+
|
|
35
|
+
`status` is a **single field holding the furthest stage reached** — not a set of
|
|
36
|
+
flags. A row at `read` is also delivered and sent; a row at `replied` is all
|
|
37
|
+
three. So the counts nest:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
replied ⊆ read ⊆ delivered ⊆ sent failed ∥ (outside the funnel)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Which means:
|
|
44
|
+
|
|
45
|
+
- **Cumulative by default.** `delivered` is everything that got at least as far
|
|
46
|
+
as delivered — the number a "delivered" tile shows.
|
|
47
|
+
- **Subtract for a stage's own count.** `delivered - read` is "delivered but
|
|
48
|
+
unread"; `read - replied` is "read but silent".
|
|
49
|
+
- **`failed` is never inside `sent`.** A failed message was never sent anywhere;
|
|
50
|
+
it is counted on its own.
|
|
51
|
+
- **There is no `queued`.** A row exists only once a send has been attempted, so
|
|
52
|
+
"waiting to go out" is not a state this collection can have. The campaign's own
|
|
53
|
+
`status` is where that lives.
|
|
54
|
+
|
|
55
|
+
`total` counts every row, failures included, and is the number of attempted
|
|
56
|
+
sends.
|
|
57
|
+
|
|
58
|
+
## The row
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
interface CampaignMessageRecord {
|
|
62
|
+
id: string; // the row's own id — what a `selection` retarget takes
|
|
63
|
+
campaignId: string;
|
|
64
|
+
messageIndex: number; // which message of the campaign, zero-based
|
|
65
|
+
providerMessageId: string; // '' when the send failed
|
|
66
|
+
channelId: string;
|
|
67
|
+
contactId: string; // the recipient's contact id
|
|
68
|
+
identifier: string; // the destination: a WhatsApp number, a Telegram id…
|
|
69
|
+
name: string; // '' when the contact had no name
|
|
70
|
+
status: 'sent' | 'delivered' | 'read' | 'replied' | 'failed';
|
|
71
|
+
sentAt?: string; // set per stage reached, absent for stages not reached
|
|
72
|
+
deliveredAt?: string;
|
|
73
|
+
readAt?: string;
|
|
74
|
+
repliedAt?: string;
|
|
75
|
+
failedAt?: string;
|
|
76
|
+
reply: string; // '' unless replied
|
|
77
|
+
replyType: string; // 'text', 'button', … '' unless replied
|
|
78
|
+
repliedIn: number; // seconds between the send and the reply; 0 unless replied
|
|
79
|
+
error?: string; // failed rows only
|
|
80
|
+
createdAt: string; // when the send was attempted
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`id` is worth singling out: it is the handle a **`selection` retarget** campaign
|
|
85
|
+
takes. Collect the ids of the rows you want and the campaign manager resolves
|
|
86
|
+
them back to recipients, which is how "re-send to everyone who did not read it"
|
|
87
|
+
is built.
|
|
88
|
+
|
|
89
|
+
`CampaignMessageRecord` is not `CampaignMessage`, despite the names. A
|
|
90
|
+
`CampaignMessage` is one of the messages a campaign is *defined* to send — a
|
|
91
|
+
channel, a payload, its polls. A `CampaignMessageRecord` is a record of one of
|
|
92
|
+
those having gone to one person, and there is one per recipient per message.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## stats
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
crm.campaignMessages.stats(query: CampaignMessageStatsQuery): Promise<CampaignMessageStats>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`GET /v1/campaign-messages/stats` · `campaigns:read` · **200**
|
|
103
|
+
|
|
104
|
+
### Query
|
|
105
|
+
|
|
106
|
+
| Field | Type | Required | Notes |
|
|
107
|
+
| --- | --- | --- | --- |
|
|
108
|
+
| `campaign` | string | yes | The campaign id, as a 24-character ObjectId. |
|
|
109
|
+
| `message` | number | no | One message of a multi-message campaign, zero-based. |
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
const overall = await crm.campaignMessages.stats({ campaign });
|
|
113
|
+
// { total: 12480, sent: 12410, delivered: 11980, read: 7420, replied: 310, failed: 70 }
|
|
114
|
+
|
|
115
|
+
const second = await crm.campaignMessages.stats({ campaign, message: 1 });
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**No date window**, deliberately. A campaign is a single send — its rows are
|
|
119
|
+
written over the minutes it takes to go out and then only change state — so "the
|
|
120
|
+
campaign's whole life" is the only window that means anything. A repeating
|
|
121
|
+
campaign's runs are separate campaigns, each with its own id.
|
|
122
|
+
|
|
123
|
+
One aggregation, not five counts. Legacy issued a `countDocuments` per stage,
|
|
124
|
+
which is five passes for numbers that all come from one grouping, and could
|
|
125
|
+
return a `read` higher than its own `delivered` when a webhook landed between
|
|
126
|
+
two of the passes.
|
|
127
|
+
|
|
128
|
+
`404 not_found` when the campaign is not the caller's — the same answer as a
|
|
129
|
+
campaign that does not exist, so the error cannot be used to probe whether
|
|
130
|
+
someone else's id is real.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## list
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
crm.campaignMessages.list(query: ListCampaignMessagesQuery): Promise<Paginated<CampaignMessageRecord>>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`GET /v1/campaign-messages` · `campaigns:read` · **200**
|
|
141
|
+
|
|
142
|
+
### Query
|
|
143
|
+
|
|
144
|
+
| Field | Type | Default | Notes |
|
|
145
|
+
| --- | --- | --- | --- |
|
|
146
|
+
| `campaign` | string | — | Required. |
|
|
147
|
+
| `message` | number | — | Zero-based index of one campaign message. |
|
|
148
|
+
| `status` | `sent` `delivered` `read` `replied` `failed` | — | Cumulative; see below. Omit for every row. |
|
|
149
|
+
| `exact` | boolean | `false` | Only rows sitting at `status` right now. |
|
|
150
|
+
| `identifier` | string | — | Exact match on one recipient. Not a search. |
|
|
151
|
+
| `reply_type` | string | — | Replied rows only; refused otherwise. |
|
|
152
|
+
| `replied_within_hours` | number | — | Replied rows only; refused otherwise. |
|
|
153
|
+
| `page` | number | `1` | 1-based. |
|
|
154
|
+
| `rows` | number | `25` | 1–100. |
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
// the two queries a report screen actually makes
|
|
158
|
+
const failures = await crm.campaignMessages.list({ campaign, status: 'failed', rows: 100 });
|
|
159
|
+
failures.data[0].error; // why that one did not go
|
|
160
|
+
|
|
161
|
+
const unread = await crm.campaignMessages.list({ campaign, status: 'delivered', exact: true });
|
|
162
|
+
unread.pagination.total; // equals stats.delivered - stats.read
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### `status` and `exact`
|
|
166
|
+
|
|
167
|
+
`status` filters **cumulatively**, so a drill-down returns exactly the rows the
|
|
168
|
+
matching stat counted: `status: 'delivered'` gives you the `delivered` number's
|
|
169
|
+
rows. `exact: true` switches to the single stage — "delivered but never read" —
|
|
170
|
+
which is the drill-down for `delivered - read`.
|
|
171
|
+
|
|
172
|
+
`exact` has no effect without `status`, and none on `failed`, which has nothing
|
|
173
|
+
beyond it.
|
|
174
|
+
|
|
175
|
+
### Reply filters
|
|
176
|
+
|
|
177
|
+
`reply_type` and `replied_within_hours` are **refused** with
|
|
178
|
+
`400 validation_failed` unless `status` is `replied`. They cannot match anything
|
|
179
|
+
on a non-replied row, and a filter that cannot match is a caller mistake worth
|
|
180
|
+
reporting rather than an empty page.
|
|
181
|
+
|
|
182
|
+
`replied_within_hours: 2` means the reply arrived within two hours of the send —
|
|
183
|
+
useful for separating people who answered immediately from people who came back
|
|
184
|
+
the next day.
|
|
185
|
+
|
|
186
|
+
### Ordering
|
|
187
|
+
|
|
188
|
+
Newest attempt first, by when the row was **written**, not by a stage timestamp.
|
|
189
|
+
Sorting on `deliveredAt` would reorder the list as webhooks arrive, and offset
|
|
190
|
+
paging over a list that reorders itself skips and repeats rows.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## iterate
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
crm.campaignMessages.iterate(query: ListCampaignMessagesQuery): AsyncGenerator<CampaignMessageRecord>
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Every matching row, paged through for you. **This is what an export is** — there
|
|
201
|
+
is no export endpoint, nothing to queue, nothing to poll, and no generated file
|
|
202
|
+
that expires before someone downloads it.
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
for await (const row of crm.campaignMessages.iterate({ campaign })) {
|
|
206
|
+
csv.write([row.identifier, row.name, row.status, row.error ?? '']);
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Pages at 100 rows unless you pass `rows` — the server's maximum, and so the
|
|
211
|
+
fewest round trips. A million recipients is ten thousand requests, so run an
|
|
212
|
+
export that size somewhere with low latency to the gateway rather than from a
|
|
213
|
+
browser tab.
|
|
214
|
+
|
|
215
|
+
The collection is live. Rows are written while a campaign goes out and statuses
|
|
216
|
+
change under an export that takes minutes, and offset paging over moving data can
|
|
217
|
+
repeat or miss a row. For a stable snapshot, export after the campaign completes.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## replies
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
crm.campaignMessages.replies(query: CampaignRepliesQuery): Promise<CampaignReplyBreakdown>
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
`GET /v1/campaign-messages/replies` · `campaigns:read` · **200**
|
|
228
|
+
|
|
229
|
+
### Query
|
|
230
|
+
|
|
231
|
+
| Field | Type | Default | Notes |
|
|
232
|
+
| --- | --- | --- | --- |
|
|
233
|
+
| `campaign` | string | — | Required. |
|
|
234
|
+
| `message` | number | — | Zero-based index of one campaign message. |
|
|
235
|
+
| `limit` | number | `50` | Distinct replies to return, 1–200. |
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
const { groups, replied, distinct } = await crm.campaignMessages.replies({ campaign });
|
|
239
|
+
|
|
240
|
+
groups; // [{ reply: 'Yes', replyType: 'button', count: 182 }, { reply: 'No', … }]
|
|
241
|
+
replied; // 310 — every replied row, whether or not its group is listed
|
|
242
|
+
distinct; // 4 — distinct replies in total
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
What a poll or a button message is sent to find out. Grouped on the reply **and**
|
|
246
|
+
its type, so a tapped `Yes` and a typed `Yes` stay apart — they say different
|
|
247
|
+
things about how engaged the recipient was. Most common first, ties broken by the
|
|
248
|
+
reply text so the order is stable between reads.
|
|
249
|
+
|
|
250
|
+
`distinct` against `groups.length` tells you whether the breakdown is complete.
|
|
251
|
+
A campaign that asked an open question has as many groups as it has repliers, so
|
|
252
|
+
a truncated answer there is normal rather than a problem — raise `limit`, or
|
|
253
|
+
treat the tail as "other".
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Operational note: the index this needs
|
|
258
|
+
|
|
259
|
+
Every read here matches on the campaign first, and the collection — which belongs
|
|
260
|
+
to the campaign manager — declares no index on it. Without one, each report is a
|
|
261
|
+
scan of every campaign message ever sent:
|
|
262
|
+
|
|
263
|
+
```js
|
|
264
|
+
db.newcampaignmessages.createIndex(
|
|
265
|
+
{ cid: 1, cmid: 1, status: 1 },
|
|
266
|
+
{ name: 'Campaign Report', background: true },
|
|
267
|
+
);
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
One compound index serves all three reads. This service deliberately does **not**
|
|
271
|
+
create it: building an index on a multi-million row collection owned by another
|
|
272
|
+
service, as a side effect of a process starting, is not something that should
|
|
273
|
+
happen by accident.
|