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,132 @@
|
|
|
1
|
+
import type { CampaignReceiver } from '../internal/receiver-file';
|
|
2
|
+
/**
|
|
3
|
+
* Most recipients one bulk campaign may carry.
|
|
4
|
+
*
|
|
5
|
+
* The same ceiling the panel has always enforced. It is not arbitrary: the list
|
|
6
|
+
* is encrypted into a single file and read back into memory by the campaign
|
|
7
|
+
* manager, so this is the point at which "one campaign" stops being a sensible
|
|
8
|
+
* unit of work.
|
|
9
|
+
*/
|
|
10
|
+
export declare const MAX_BULK_RECEIVERS = 1000000;
|
|
11
|
+
/**
|
|
12
|
+
* How a bulk audience is described.
|
|
13
|
+
*
|
|
14
|
+
* Four shapes, because there are four ways a person actually has this data to
|
|
15
|
+
* hand. All of them end as the same encrypted file; the difference is only how
|
|
16
|
+
* much parsing the SDK does on the way.
|
|
17
|
+
*/
|
|
18
|
+
export type BulkAudienceInput =
|
|
19
|
+
/**
|
|
20
|
+
* Just numbers, with no per-recipient variables.
|
|
21
|
+
*
|
|
22
|
+
* Accepts an array, or one string holding many -- a textarea's contents,
|
|
23
|
+
* separated by commas, newlines, semicolons or spaces, in any combination.
|
|
24
|
+
*/
|
|
25
|
+
{
|
|
26
|
+
numbers: string | readonly string[];
|
|
27
|
+
dedupe?: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Raw CSV or TSV text whose first row is the header.
|
|
31
|
+
*
|
|
32
|
+
* `recipientColumn` names the header holding the number; every column,
|
|
33
|
+
* including that one, also becomes a template variable. The delimiter is
|
|
34
|
+
* detected from the header row unless you pass one.
|
|
35
|
+
*/
|
|
36
|
+
| {
|
|
37
|
+
csv: string;
|
|
38
|
+
recipientColumn: string;
|
|
39
|
+
delimiter?: string;
|
|
40
|
+
dedupe?: boolean;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Rows you have already parsed -- from your own CSV reader, a spreadsheet API,
|
|
44
|
+
* or a database query.
|
|
45
|
+
*/
|
|
46
|
+
| {
|
|
47
|
+
rows: readonly Record<string, unknown>[];
|
|
48
|
+
recipientColumn: string;
|
|
49
|
+
dedupe?: boolean;
|
|
50
|
+
}
|
|
51
|
+
/** Receivers you have already built. The escape hatch; nothing is inferred. */
|
|
52
|
+
| {
|
|
53
|
+
receivers: readonly CampaignReceiver[];
|
|
54
|
+
};
|
|
55
|
+
/** What resolving an audience produced, including what it discarded. */
|
|
56
|
+
export interface ResolvedAudience {
|
|
57
|
+
receivers: CampaignReceiver[];
|
|
58
|
+
/** Entries that carried no usable digits and were skipped. */
|
|
59
|
+
skipped: number;
|
|
60
|
+
/** Entries dropped because an earlier entry had the same number. */
|
|
61
|
+
duplicates: number;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Normalises a column name into a template variable id.
|
|
65
|
+
*
|
|
66
|
+
* Lower-cased with every non-alphanumeric character replaced by `_`, which is
|
|
67
|
+
* what the panel's CSV reader and its paste-a-table path both did -- so a header
|
|
68
|
+
* of `First Name` is the variable `first_name` here exactly as it was there, and
|
|
69
|
+
* templates built against the old behaviour keep working.
|
|
70
|
+
*
|
|
71
|
+
* A blank header becomes `empty_<index>` rather than `_`, so two unnamed columns
|
|
72
|
+
* do not collapse into one variable.
|
|
73
|
+
*/
|
|
74
|
+
export declare const normaliseColumn: (header: unknown, index: number) => string;
|
|
75
|
+
/**
|
|
76
|
+
* Pulls the numbers out of one string, or tidies an array of them.
|
|
77
|
+
*
|
|
78
|
+
* Splits on commas, newlines, carriage returns, tabs, semicolons and spaces --
|
|
79
|
+
* all of them, in any mixture. The panel chose a single separator per paste
|
|
80
|
+
* (commas if the text contained any, otherwise newlines), which quietly mangled
|
|
81
|
+
* the common case of pasting a column from a spreadsheet into a field that
|
|
82
|
+
* already had comma-separated numbers: the newlines were never split on, so two
|
|
83
|
+
* numbers merged into one 24-digit number and the send went nowhere. Splitting on
|
|
84
|
+
* every separator cannot produce that, and for input with one separator it
|
|
85
|
+
* behaves identically.
|
|
86
|
+
*/
|
|
87
|
+
export declare const normaliseNumbers: (input: string | readonly string[]) => string[];
|
|
88
|
+
/**
|
|
89
|
+
* Splits delimited text into a header and rows, RFC 4180 style.
|
|
90
|
+
*
|
|
91
|
+
* A proper parser rather than `split(',')`, because a quoted field may contain
|
|
92
|
+
* the delimiter, a newline, or an escaped quote -- and a currency-formatted
|
|
93
|
+
* variable (`"1,299"`) is exactly the sort of value that appears in these files.
|
|
94
|
+
* Splitting naively turns one such row into two bad ones.
|
|
95
|
+
*
|
|
96
|
+
* Values stay strings. The panel parsed with `dynamicTyping`, which coerced
|
|
97
|
+
* numeric-looking cells to JavaScript numbers -- and that silently corrupted the
|
|
98
|
+
* one column that matters: a 16-digit number exceeds `Number.MAX_SAFE_INTEGER`
|
|
99
|
+
* and comes back with its last digits changed, while a number written with a
|
|
100
|
+
* leading zero loses it. Keeping text as text avoids both.
|
|
101
|
+
*/
|
|
102
|
+
export declare const parseDelimited: (text: string, delimiter?: string) => {
|
|
103
|
+
headers: string[];
|
|
104
|
+
rows: Record<string, string>[];
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Turns parsed rows into receivers.
|
|
108
|
+
*
|
|
109
|
+
* Every column becomes a variable, including the recipient column itself -- which
|
|
110
|
+
* is intentional and matches the panel: a template that greets someone by the
|
|
111
|
+
* number they were messaged on still works.
|
|
112
|
+
*
|
|
113
|
+
* A row whose recipient column holds no digits is skipped rather than failing the
|
|
114
|
+
* whole batch. In a file of ten thousand rows, a handful of blanks and a trailing
|
|
115
|
+
* footer line are normal, and refusing the campaign over them would mean the
|
|
116
|
+
* caller has to clean a file they may not control. The count comes back in
|
|
117
|
+
* {@link ResolvedAudience.skipped} so the omission is visible rather than silent.
|
|
118
|
+
*/
|
|
119
|
+
export declare const buildReceivers: (rows: readonly Record<string, unknown>[], recipientColumn: string, options?: {
|
|
120
|
+
dedupe?: boolean;
|
|
121
|
+
}) => ResolvedAudience;
|
|
122
|
+
/**
|
|
123
|
+
* Resolves any of the four audience shapes into one receiver list.
|
|
124
|
+
*
|
|
125
|
+
* De-duplicates by number unless `dedupe: false`. On by default because the cost
|
|
126
|
+
* of a duplicate here is not a wasted row in a table -- it is the same person
|
|
127
|
+
* receiving the same marketing message twice, billed twice, and more likely to
|
|
128
|
+
* report the sender. A caller who genuinely wants two sends to one number can
|
|
129
|
+
* say so.
|
|
130
|
+
*/
|
|
131
|
+
export declare const resolveAudience: (input: BulkAudienceInput) => ResolvedAudience;
|
|
132
|
+
export type { CampaignReceiver };
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveAudience = exports.buildReceivers = exports.parseDelimited = exports.normaliseNumbers = exports.normaliseColumn = exports.MAX_BULK_RECEIVERS = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Most recipients one bulk campaign may carry.
|
|
6
|
+
*
|
|
7
|
+
* The same ceiling the panel has always enforced. It is not arbitrary: the list
|
|
8
|
+
* is encrypted into a single file and read back into memory by the campaign
|
|
9
|
+
* manager, so this is the point at which "one campaign" stops being a sensible
|
|
10
|
+
* unit of work.
|
|
11
|
+
*/
|
|
12
|
+
exports.MAX_BULK_RECEIVERS = 1_000_000;
|
|
13
|
+
/** Digits only: no `+`, no spaces, no dashes, no parentheses. */
|
|
14
|
+
const digitsOf = (value) => (value === null || value === undefined ? '' : String(value).replace(/\D/g, ''));
|
|
15
|
+
/**
|
|
16
|
+
* Normalises a column name into a template variable id.
|
|
17
|
+
*
|
|
18
|
+
* Lower-cased with every non-alphanumeric character replaced by `_`, which is
|
|
19
|
+
* what the panel's CSV reader and its paste-a-table path both did -- so a header
|
|
20
|
+
* of `First Name` is the variable `first_name` here exactly as it was there, and
|
|
21
|
+
* templates built against the old behaviour keep working.
|
|
22
|
+
*
|
|
23
|
+
* A blank header becomes `empty_<index>` rather than `_`, so two unnamed columns
|
|
24
|
+
* do not collapse into one variable.
|
|
25
|
+
*/
|
|
26
|
+
const normaliseColumn = (header, index) => {
|
|
27
|
+
const name = String(header ?? '').trim();
|
|
28
|
+
if (!name)
|
|
29
|
+
return `empty_${index}`;
|
|
30
|
+
return name.toLowerCase().replace(/[^a-z0-9]/g, '_');
|
|
31
|
+
};
|
|
32
|
+
exports.normaliseColumn = normaliseColumn;
|
|
33
|
+
/**
|
|
34
|
+
* Pulls the numbers out of one string, or tidies an array of them.
|
|
35
|
+
*
|
|
36
|
+
* Splits on commas, newlines, carriage returns, tabs, semicolons and spaces --
|
|
37
|
+
* all of them, in any mixture. The panel chose a single separator per paste
|
|
38
|
+
* (commas if the text contained any, otherwise newlines), which quietly mangled
|
|
39
|
+
* the common case of pasting a column from a spreadsheet into a field that
|
|
40
|
+
* already had comma-separated numbers: the newlines were never split on, so two
|
|
41
|
+
* numbers merged into one 24-digit number and the send went nowhere. Splitting on
|
|
42
|
+
* every separator cannot produce that, and for input with one separator it
|
|
43
|
+
* behaves identically.
|
|
44
|
+
*/
|
|
45
|
+
const normaliseNumbers = (input) => {
|
|
46
|
+
const parts = typeof input === 'string' ? input.split(/[\s,;]+/) : input;
|
|
47
|
+
return parts.map(digitsOf).filter((value) => value.length > 0);
|
|
48
|
+
};
|
|
49
|
+
exports.normaliseNumbers = normaliseNumbers;
|
|
50
|
+
/**
|
|
51
|
+
* Splits delimited text into a header and rows, RFC 4180 style.
|
|
52
|
+
*
|
|
53
|
+
* A proper parser rather than `split(',')`, because a quoted field may contain
|
|
54
|
+
* the delimiter, a newline, or an escaped quote -- and a currency-formatted
|
|
55
|
+
* variable (`"1,299"`) is exactly the sort of value that appears in these files.
|
|
56
|
+
* Splitting naively turns one such row into two bad ones.
|
|
57
|
+
*
|
|
58
|
+
* Values stay strings. The panel parsed with `dynamicTyping`, which coerced
|
|
59
|
+
* numeric-looking cells to JavaScript numbers -- and that silently corrupted the
|
|
60
|
+
* one column that matters: a 16-digit number exceeds `Number.MAX_SAFE_INTEGER`
|
|
61
|
+
* and comes back with its last digits changed, while a number written with a
|
|
62
|
+
* leading zero loses it. Keeping text as text avoids both.
|
|
63
|
+
*/
|
|
64
|
+
const parseDelimited = (text, delimiter) => {
|
|
65
|
+
const rows = splitRows(text, delimiter ?? detectDelimiter(text));
|
|
66
|
+
const headerRow = rows.shift();
|
|
67
|
+
if (!headerRow)
|
|
68
|
+
return { headers: [], rows: [] };
|
|
69
|
+
const headers = headerRow.map(exports.normaliseColumn);
|
|
70
|
+
const parsed = rows
|
|
71
|
+
// A trailing newline, or a blank line in the middle, is not a record.
|
|
72
|
+
.filter((cells) => cells.some((cell) => cell.trim() !== ''))
|
|
73
|
+
.map((cells) => {
|
|
74
|
+
const row = {};
|
|
75
|
+
headers.forEach((header, index) => {
|
|
76
|
+
row[header] = cells[index] ?? '';
|
|
77
|
+
});
|
|
78
|
+
return row;
|
|
79
|
+
});
|
|
80
|
+
return { headers, rows: parsed };
|
|
81
|
+
};
|
|
82
|
+
exports.parseDelimited = parseDelimited;
|
|
83
|
+
/** Tab if the header row has one, else semicolon, else comma. */
|
|
84
|
+
const detectDelimiter = (text) => {
|
|
85
|
+
const firstLine = text.slice(0, text.search(/\r?\n/) === -1 ? undefined : text.search(/\r?\n/));
|
|
86
|
+
if (firstLine.includes('\t'))
|
|
87
|
+
return '\t';
|
|
88
|
+
if (firstLine.includes(';') && !firstLine.includes(','))
|
|
89
|
+
return ';';
|
|
90
|
+
return ',';
|
|
91
|
+
};
|
|
92
|
+
/** The parser proper: quotes, doubled quotes inside them, and newlines within a field. */
|
|
93
|
+
const splitRows = (text, delimiter) => {
|
|
94
|
+
const rows = [];
|
|
95
|
+
let row = [];
|
|
96
|
+
let field = '';
|
|
97
|
+
let quoted = false;
|
|
98
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
99
|
+
const char = text[i];
|
|
100
|
+
if (quoted) {
|
|
101
|
+
if (char !== '"') {
|
|
102
|
+
field += char;
|
|
103
|
+
}
|
|
104
|
+
else if (text[i + 1] === '"') {
|
|
105
|
+
// A doubled quote inside a quoted field is one literal quote.
|
|
106
|
+
field += '"';
|
|
107
|
+
i += 1;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
quoted = false;
|
|
111
|
+
}
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (char === '"' && field === '') {
|
|
115
|
+
quoted = true;
|
|
116
|
+
}
|
|
117
|
+
else if (char === delimiter) {
|
|
118
|
+
row.push(field);
|
|
119
|
+
field = '';
|
|
120
|
+
}
|
|
121
|
+
else if (char === '\n' || char === '\r') {
|
|
122
|
+
// Either line ending, and CRLF counts once.
|
|
123
|
+
if (char === '\r' && text[i + 1] === '\n')
|
|
124
|
+
i += 1;
|
|
125
|
+
row.push(field);
|
|
126
|
+
rows.push(row);
|
|
127
|
+
row = [];
|
|
128
|
+
field = '';
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
field += char;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (field !== '' || row.length) {
|
|
135
|
+
row.push(field);
|
|
136
|
+
rows.push(row);
|
|
137
|
+
}
|
|
138
|
+
return rows;
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Turns parsed rows into receivers.
|
|
142
|
+
*
|
|
143
|
+
* Every column becomes a variable, including the recipient column itself -- which
|
|
144
|
+
* is intentional and matches the panel: a template that greets someone by the
|
|
145
|
+
* number they were messaged on still works.
|
|
146
|
+
*
|
|
147
|
+
* A row whose recipient column holds no digits is skipped rather than failing the
|
|
148
|
+
* whole batch. In a file of ten thousand rows, a handful of blanks and a trailing
|
|
149
|
+
* footer line are normal, and refusing the campaign over them would mean the
|
|
150
|
+
* caller has to clean a file they may not control. The count comes back in
|
|
151
|
+
* {@link ResolvedAudience.skipped} so the omission is visible rather than silent.
|
|
152
|
+
*/
|
|
153
|
+
const buildReceivers = (rows, recipientColumn, options = {}) => {
|
|
154
|
+
const column = (0, exports.normaliseColumn)(recipientColumn, 0);
|
|
155
|
+
const receivers = [];
|
|
156
|
+
const seen = new Set();
|
|
157
|
+
let skipped = 0;
|
|
158
|
+
let duplicates = 0;
|
|
159
|
+
for (const row of rows) {
|
|
160
|
+
// Accept the column as written or as normalised, so a caller passing
|
|
161
|
+
// `'First Name'` against already-normalised rows still resolves.
|
|
162
|
+
const raw = row[recipientColumn] ?? row[column];
|
|
163
|
+
const waba = digitsOf(raw);
|
|
164
|
+
if (!waba) {
|
|
165
|
+
skipped += 1;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (options.dedupe !== false) {
|
|
169
|
+
if (seen.has(waba)) {
|
|
170
|
+
duplicates += 1;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
seen.add(waba);
|
|
174
|
+
}
|
|
175
|
+
receivers.push({
|
|
176
|
+
waba,
|
|
177
|
+
custom_fields: Object.keys(row).map((key, index) => ({
|
|
178
|
+
id: (0, exports.normaliseColumn)(key, index),
|
|
179
|
+
value: row[key] === null || row[key] === undefined ? '' : String(row[key]),
|
|
180
|
+
})),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return { receivers, skipped, duplicates };
|
|
184
|
+
};
|
|
185
|
+
exports.buildReceivers = buildReceivers;
|
|
186
|
+
/**
|
|
187
|
+
* Resolves any of the four audience shapes into one receiver list.
|
|
188
|
+
*
|
|
189
|
+
* De-duplicates by number unless `dedupe: false`. On by default because the cost
|
|
190
|
+
* of a duplicate here is not a wasted row in a table -- it is the same person
|
|
191
|
+
* receiving the same marketing message twice, billed twice, and more likely to
|
|
192
|
+
* report the sender. A caller who genuinely wants two sends to one number can
|
|
193
|
+
* say so.
|
|
194
|
+
*/
|
|
195
|
+
const resolveAudience = (input) => {
|
|
196
|
+
if ('receivers' in input) {
|
|
197
|
+
// Taken as given: a caller who built these chose the shape deliberately.
|
|
198
|
+
return { receivers: [...input.receivers], skipped: 0, duplicates: 0 };
|
|
199
|
+
}
|
|
200
|
+
if ('numbers' in input) {
|
|
201
|
+
const numbers = (0, exports.normaliseNumbers)(input.numbers);
|
|
202
|
+
const receivers = [];
|
|
203
|
+
const seen = new Set();
|
|
204
|
+
let duplicates = 0;
|
|
205
|
+
for (const waba of numbers) {
|
|
206
|
+
if (input.dedupe !== false) {
|
|
207
|
+
if (seen.has(waba)) {
|
|
208
|
+
duplicates += 1;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
seen.add(waba);
|
|
212
|
+
}
|
|
213
|
+
receivers.push({ waba, custom_fields: [] });
|
|
214
|
+
}
|
|
215
|
+
const supplied = typeof input.numbers === 'string' ? numbers.length + countBlank(input.numbers) : input.numbers.length;
|
|
216
|
+
return { receivers, skipped: Math.max(0, supplied - numbers.length - duplicates), duplicates };
|
|
217
|
+
}
|
|
218
|
+
const rows = 'csv' in input ? (0, exports.parseDelimited)(input.csv, input.delimiter).rows : input.rows;
|
|
219
|
+
return (0, exports.buildReceivers)(rows, input.recipientColumn, { dedupe: input.dedupe });
|
|
220
|
+
};
|
|
221
|
+
exports.resolveAudience = resolveAudience;
|
|
222
|
+
/** Entries in a pasted string that held no digits at all, for the skipped count. */
|
|
223
|
+
const countBlank = (text) => text.split(/[\s,;]+/).filter((part) => part !== '' && digitsOf(part) === '').length;
|
|
224
|
+
//# sourceMappingURL=campaign-audience.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"campaign-audience.js","sourceRoot":"","sources":["../../src/resources/campaign-audience.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { CampaignMessageRecord, CampaignMessageStats, CampaignMessageStatsQuery, CampaignRepliesQuery, CampaignReplyBreakdown, ListCampaignMessagesQuery } from '../types/campaign';
|
|
2
|
+
import type { Paginated } from '../types/common';
|
|
3
|
+
import type { HttpClient } from '../http';
|
|
4
|
+
/**
|
|
5
|
+
* Campaign reporting: what actually happened to each message.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* const stats = await crm.campaignMessages.stats({ campaign });
|
|
9
|
+
* // { total: 12480, sent: 12410, delivered: 11980, read: 7420, replied: 310, failed: 70 }
|
|
10
|
+
*
|
|
11
|
+
* const { data } = await crm.campaignMessages.list({ campaign, status: 'failed' });
|
|
12
|
+
* data[0].error; // why that one did not go
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* ## Where these numbers come from
|
|
16
|
+
*
|
|
17
|
+
* One row per attempted send, written by the campaign manager as it hands each
|
|
18
|
+
* message to a channel, with its `status` moved forward by the channel's
|
|
19
|
+
* delivery webhooks -- which are processed outside the CRM entirely. The report
|
|
20
|
+
* is **counted off those rows when you ask for it**, not rolled up on a
|
|
21
|
+
* schedule, so there is no "generated at" to reason about and no staleness
|
|
22
|
+
* window: the numbers are as current as the webhooks that have arrived.
|
|
23
|
+
*
|
|
24
|
+
* The flip side is that a campaign still going out has moving numbers. Reading
|
|
25
|
+
* `stats` seconds after launching under-counts everything, and that is correct
|
|
26
|
+
* rather than a lag.
|
|
27
|
+
*
|
|
28
|
+
* ## The funnel
|
|
29
|
+
*
|
|
30
|
+
* `status` is a single field holding the furthest stage reached, so the counts
|
|
31
|
+
* are cumulative and nest: `replied ⊆ read ⊆ delivered ⊆ sent`. For a stage's
|
|
32
|
+
* exclusive count, subtract the next one -- `delivered - read` is "delivered but
|
|
33
|
+
* unread". `failed` is outside the funnel and never inside `sent`.
|
|
34
|
+
*
|
|
35
|
+
* ## There is no export endpoint
|
|
36
|
+
*
|
|
37
|
+
* An export is {@link list} written to a file, so {@link iterate} does the
|
|
38
|
+
* paging and the caller formats the rows. Nothing is queued, nothing has to be
|
|
39
|
+
* polled, and no generated file expires before someone downloads it.
|
|
40
|
+
*/
|
|
41
|
+
export declare class CampaignMessages {
|
|
42
|
+
private readonly http;
|
|
43
|
+
constructor(http: HttpClient);
|
|
44
|
+
/**
|
|
45
|
+
* The campaign's funnel counts.
|
|
46
|
+
*
|
|
47
|
+
* `GET /v1/campaign-messages/stats?campaign=<id>` · `campaigns:read` · **200**
|
|
48
|
+
*
|
|
49
|
+
* ### Query
|
|
50
|
+
*
|
|
51
|
+
* | Field | Type | Required | Notes |
|
|
52
|
+
* | --- | --- | --- | --- |
|
|
53
|
+
* | `campaign` | string | yes | The campaign id. |
|
|
54
|
+
* | `message` | number | no | One message of a multi-message campaign, zero-based. |
|
|
55
|
+
*
|
|
56
|
+
* ```ts
|
|
57
|
+
* const overall = await crm.campaignMessages.stats({ campaign });
|
|
58
|
+
* const second = await crm.campaignMessages.stats({ campaign, message: 1 });
|
|
59
|
+
* ```
|
|
60
|
+
*
|
|
61
|
+
* No date window: a campaign is one send, so its whole life is the only
|
|
62
|
+
* meaningful window. A repeating campaign's runs are separate campaigns with
|
|
63
|
+
* their own ids.
|
|
64
|
+
*
|
|
65
|
+
* `404 not_found` if the campaign is not the caller's -- the same answer as a
|
|
66
|
+
* campaign that does not exist, so the error cannot be used to probe ids.
|
|
67
|
+
*/
|
|
68
|
+
stats(query: CampaignMessageStatsQuery): Promise<CampaignMessageStats>;
|
|
69
|
+
/**
|
|
70
|
+
* The rows behind the funnel. This is the report table.
|
|
71
|
+
*
|
|
72
|
+
* `GET /v1/campaign-messages?campaign=<id>` · `campaigns:read` · **200**
|
|
73
|
+
*
|
|
74
|
+
* ### Query
|
|
75
|
+
*
|
|
76
|
+
* | Field | Type | Default | Notes |
|
|
77
|
+
* | --- | --- | --- | --- |
|
|
78
|
+
* | `campaign` | string | — | Required. |
|
|
79
|
+
* | `message` | number | — | Zero-based index of one campaign message. |
|
|
80
|
+
* | `status` | `sent \| delivered \| read \| replied \| failed` | — | Cumulative; see below. Omit for every row. |
|
|
81
|
+
* | `exact` | boolean | `false` | Only rows sitting at `status` right now. |
|
|
82
|
+
* | `identifier` | string | — | Exact match on one recipient. Not a search. |
|
|
83
|
+
* | `reply_type` | string | — | Replied rows only; refused otherwise. |
|
|
84
|
+
* | `replied_within_hours` | number | — | Replied rows only; refused otherwise. |
|
|
85
|
+
* | `page` | number | `1` | 1-based. |
|
|
86
|
+
* | `rows` | number | `25` | 1–100. |
|
|
87
|
+
*
|
|
88
|
+
* Newest attempt first, ordered by when the row was written -- not by a stage
|
|
89
|
+
* timestamp, which would reorder the list as webhooks arrive and make paging
|
|
90
|
+
* skip and repeat rows.
|
|
91
|
+
*
|
|
92
|
+
* ```ts
|
|
93
|
+
* // the two queries a report screen actually makes
|
|
94
|
+
* const failures = await crm.campaignMessages.list({ campaign, status: 'failed', rows: 100 });
|
|
95
|
+
* const unread = await crm.campaignMessages.list({ campaign, status: 'delivered', exact: true });
|
|
96
|
+
* ```
|
|
97
|
+
*
|
|
98
|
+
* `reply_type` and `replied_within_hours` are **refused** with
|
|
99
|
+
* `400 validation_failed` unless `status` is `replied`, rather than quietly
|
|
100
|
+
* matching nothing.
|
|
101
|
+
*/
|
|
102
|
+
list(query: ListCampaignMessagesQuery): Promise<Paginated<CampaignMessageRecord>>;
|
|
103
|
+
/**
|
|
104
|
+
* Every matching row, paged through for you. What an export is.
|
|
105
|
+
*
|
|
106
|
+
* ```ts
|
|
107
|
+
* for await (const row of crm.campaignMessages.iterate({ campaign })) {
|
|
108
|
+
* csv.write([row.identifier, row.name, row.status, row.error ?? '']);
|
|
109
|
+
* }
|
|
110
|
+
* ```
|
|
111
|
+
*
|
|
112
|
+
* Pages at 100 rows unless told otherwise -- the server's maximum, and the
|
|
113
|
+
* fewest round trips. A campaign of a million recipients is ten thousand
|
|
114
|
+
* requests, so for exports that size prefer running it where the latency is
|
|
115
|
+
* low rather than from a browser.
|
|
116
|
+
*
|
|
117
|
+
* The collection is live: rows are written while a campaign is going out, and
|
|
118
|
+
* statuses change under an export that takes minutes. Offset paging over
|
|
119
|
+
* moving data can repeat or skip a row -- for a stable snapshot, export after
|
|
120
|
+
* the campaign completes.
|
|
121
|
+
*/
|
|
122
|
+
iterate(query: ListCampaignMessagesQuery): AsyncGenerator<CampaignMessageRecord>;
|
|
123
|
+
/**
|
|
124
|
+
* What people replied, grouped by the reply itself.
|
|
125
|
+
*
|
|
126
|
+
* `GET /v1/campaign-messages/replies?campaign=<id>` · `campaigns:read` · **200**
|
|
127
|
+
*
|
|
128
|
+
* ### Query
|
|
129
|
+
*
|
|
130
|
+
* | Field | Type | Default | Notes |
|
|
131
|
+
* | --- | --- | --- | --- |
|
|
132
|
+
* | `campaign` | string | — | Required. |
|
|
133
|
+
* | `message` | number | — | Zero-based index of one campaign message. |
|
|
134
|
+
* | `limit` | number | `50` | Distinct replies to return, 1–200. |
|
|
135
|
+
*
|
|
136
|
+
* ```ts
|
|
137
|
+
* const { groups, replied, distinct } = await crm.campaignMessages.replies({ campaign });
|
|
138
|
+
* groups; // [{ reply: 'Yes', replyType: 'button', count: 182 }, …]
|
|
139
|
+
* ```
|
|
140
|
+
*
|
|
141
|
+
* This is what a poll or a button message is sent to find out. Grouped on the
|
|
142
|
+
* reply **and** its type, so a tapped `Yes` and a typed `Yes` stay apart --
|
|
143
|
+
* they say different things about how engaged the recipient was.
|
|
144
|
+
*
|
|
145
|
+
* `distinct` against `groups.length` tells you whether the breakdown is
|
|
146
|
+
* complete. A campaign that asked an open question has as many groups as it
|
|
147
|
+
* has repliers, so a truncated answer there is normal rather than a problem.
|
|
148
|
+
*/
|
|
149
|
+
replies(query: CampaignRepliesQuery): Promise<CampaignReplyBreakdown>;
|
|
150
|
+
}
|