agumail-js 0.1.1 → 0.2.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 +87 -38
- package/dist/index.cjs +134 -48
- package/dist/index.d.cts +84 -13
- package/dist/index.d.ts +84 -13
- package/dist/index.js +134 -48
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,7 +36,8 @@ Both calls resolve with what AguMail counted:
|
|
|
36
36
|
|
|
37
37
|
```ts
|
|
38
38
|
const added = await agumail.identify({ email: 'sam@example.com', firstName: 'Sam' });
|
|
39
|
-
// { created: 1, updated: 0, suppressedSkipped: 0, overCapacity: 0, confirmationsSent: 0
|
|
39
|
+
// { created: 1, updated: 0, suppressedSkipped: 0, overCapacity: 0, confirmationsSent: 0,
|
|
40
|
+
// unknownContacts: 0, conflicts: 0, conflicted: [] }
|
|
40
41
|
|
|
41
42
|
const recorded = await agumail.track({ email: 'sam@example.com', event: 'Created first project' });
|
|
42
43
|
// { created: 1, duplicates: 0, unknownContacts: 0, uniqueId: '0b6f9d3e-...' }
|
|
@@ -47,7 +48,8 @@ const recorded = await agumail.track({ email: 'sam@example.com', event: 'Created
|
|
|
47
48
|
- `overCapacity`: your plan has no room for another contact, so they weren't added.
|
|
48
49
|
- `confirmationsSent`: confirmation emails AguMail sent, for someone coming back after unsubscribing (see [Email settings in your app](#email-settings-in-your-app)).
|
|
49
50
|
- `duplicates`: an event with this `uniqueId` was already recorded, so this one wasn't.
|
|
50
|
-
- `unknownContacts`: AguMail doesn't have this person, so the event was skipped.
|
|
51
|
+
- `unknownContacts`: AguMail doesn't have this person, so the event was skipped. For a contact, it counts those sent with only a `userId` that nobody has.
|
|
52
|
+
- `conflicts` and `conflicted`: contacts refused because their email belongs to someone else (see [Identify by your app's user id](#identify-by-your-apps-user-id)).
|
|
51
53
|
- `uniqueId`: the id the event was sent with: yours, or one the package made up.
|
|
52
54
|
|
|
53
55
|
## Never break a signup
|
|
@@ -100,6 +102,8 @@ const agumail = new AguMail(process.env.AGUMAIL_API_KEY, {
|
|
|
100
102
|
| `over_capacity` | Contacts or signups weren't added because your plan has no room for more contacts. |
|
|
101
103
|
| `signups_limited` | Signups over the limit of 10 new addresses per visitor IP a minute were dropped. |
|
|
102
104
|
| `history` | Events dated more than 72 hours ago, sent without `backfill: true`, were stored but start no flows. |
|
|
105
|
+
| `unknown_user_ids` | Contacts sent with only a `userId` that nobody has were skipped. Send the email too, so AguMail can add them. |
|
|
106
|
+
| `conflicts` | Contacts whose email belongs to someone else were refused. `warning.conflicted` lists them. |
|
|
103
107
|
|
|
104
108
|
Each warning fires at most once per call (or flush), and `warning.count` adds up the records it covers. Warnings fire for calls that throw and calls that don't. Duplicates and `suppressedSkipped` are normal, so they never warn. Both callbacks may be async, and neither can make a call fail.
|
|
105
109
|
|
|
@@ -163,6 +167,46 @@ if (result.error?.uniqueIds) {
|
|
|
163
167
|
}
|
|
164
168
|
```
|
|
165
169
|
|
|
170
|
+
## Identify by your app's user id
|
|
171
|
+
|
|
172
|
+
If your app gives each user an id, send it as `userId` beside the email. AguMail then knows the person by that id: when they change their email in your app, your next `identify` moves the same contact to the new address. Their status, tags, history and flows stay with them, and they are not counted twice.
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
await agumail.identify({ userId: user.id, email: user.email, firstName: user.firstName });
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
- `userId` is your own id for the person: text up to 200 characters, sent exactly as you give it, or a whole number, which is sent as text. A contact keeps the first id it is sent with.
|
|
179
|
+
- A contact AguMail already has by email takes the id the first time you send both.
|
|
180
|
+
- With a `userId`, the email is optional: `identify({ userId: user.id, ... })` updates that contact. If nobody has that id, the record is skipped and counted in `unknownContacts`, because a new contact needs an email.
|
|
181
|
+
- `track` takes `userId` in place of `email`, or beside it. With a `userId`, AguMail finds the person by the id alone, and an event never changes anyone's email or id.
|
|
182
|
+
- The `contact` you pass to `track` goes with the event's `userId` and `email`. That upsert works like `identify` (without touching their subscription), so with both it can move the contact to that email.
|
|
183
|
+
- A new address that is on your suppression list stays blocked: the contact keeps its status, and nothing is sent to that address.
|
|
184
|
+
|
|
185
|
+
AguMail never merges two contacts. A record that would mix two people up is refused without an error, and the rest of the call still lands:
|
|
186
|
+
|
|
187
|
+
- `email_taken`: the new email already belongs to another contact.
|
|
188
|
+
- `user_id_mismatch`: the contact with that email already has a different `userId`.
|
|
189
|
+
|
|
190
|
+
`conflicts` counts them and `conflicted` lists each one. `index` is its place in the array you passed, from 0, and for `trackMany`, the place of the event that carried the `contact`:
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
const result = await agumail.identifyMany(users.map((u) => ({ userId: u.id, email: u.email })));
|
|
194
|
+
// { ..., conflicts: 1, conflicted: [{ index: 1, userId: 'user_77', email: 'alex@example.com', reason: 'email_taken' }] }
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`onWarning` gets a `conflicts` warning with the same list in `warning.conflicted`, and an `unknown_user_ids` warning for ids nobody has. For [the queue](#batching-with-the-queue), these warnings are how you hear about them, since nobody reads the result of a flush the queue starts itself. In a flush, `index` is the place among the contacts that flush sent, in the order you queued them.
|
|
198
|
+
|
|
199
|
+
To ask where someone stands by their id, use `contacts.statusByUserId`. It takes one id or an array, like `contacts.status`:
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
const sam = await agumail.contacts.statusByUserId(user.id);
|
|
203
|
+
// { email: 'sam@example.com', userId: 'user_8412', status: 'active', subscribed: true }
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
For an id nobody has, `email` and `status` are `null`.
|
|
207
|
+
|
|
208
|
+
`ping()` returns a `shop.userId` too. That one is your AguMail account's id, not one of your users' ids.
|
|
209
|
+
|
|
166
210
|
## Contact properties
|
|
167
211
|
|
|
168
212
|
`properties` become the contact's custom fields in AguMail. AguMail keeps them as text, so the package converts for you:
|
|
@@ -187,7 +231,7 @@ await agumail.contacts.unsubscribe(user.email); // they switched it off
|
|
|
187
231
|
await agumail.contacts.subscribe(user.email); // they switched it on
|
|
188
232
|
```
|
|
189
233
|
|
|
190
|
-
Call these only when the signed-in person changes the setting themselves, never from a sync. When they switch it back on:
|
|
234
|
+
Call these only when the signed-in person changes the setting themselves, never from a sync. They take email addresses, not user ids, so a switch-off holds even for someone AguMail doesn't have yet. When they switch it back on:
|
|
191
235
|
|
|
192
236
|
- If they switched it off in your app, they are subscribed again straight away.
|
|
193
237
|
- If they clicked unsubscribe in one of your emails, or marked one as spam, AguMail sends them one confirmation email (at most one a day), and they are back when they click it. `confirmationsSent` in the result counts these, so you can tell them to check their inbox.
|
|
@@ -199,10 +243,10 @@ To show the switch in the right position, ask AguMail. `subscribed` is the value
|
|
|
199
243
|
|
|
200
244
|
```ts
|
|
201
245
|
const sam = await agumail.contacts.status('sam@example.com');
|
|
202
|
-
// { email: 'sam@example.com', status: 'active', subscribed: true }
|
|
246
|
+
// { email: 'sam@example.com', userId: 'user_8412', status: 'active', subscribed: true }
|
|
203
247
|
```
|
|
204
248
|
|
|
205
|
-
`status` is `null` for an address that isn't on your list. Pass an array to ask about as many addresses as you like: the package asks 500 at a time and keeps your order.
|
|
249
|
+
`status` is `null` for an address that isn't on your list, and `userId` is `null` when the contact has none. Pass an array to ask about as many addresses as you like: the package asks 500 at a time and keeps your order. To ask by your app's user ids, use [`contacts.statusByUserId`](#identify-by-your-apps-user-id).
|
|
206
250
|
|
|
207
251
|
## Examples
|
|
208
252
|
|
|
@@ -305,6 +349,7 @@ export const addToAguMail: OnAfterSignupHook = async ({ user }) => {
|
|
|
305
349
|
// Not awaited, and never throws: AguMail can't hold up or fail a signup.
|
|
306
350
|
void agumail.identify(
|
|
307
351
|
{
|
|
352
|
+
userId: user.id,
|
|
308
353
|
email: user.email,
|
|
309
354
|
firstName: user.firstName,
|
|
310
355
|
lastName: user.lastName,
|
|
@@ -323,7 +368,7 @@ import { addToAguMail } from './src/auth/agumail' with { type: 'ref' };
|
|
|
323
368
|
onAfterSignup: addToAguMail,
|
|
324
369
|
```
|
|
325
370
|
|
|
326
|
-
Then record what they do from your actions:
|
|
371
|
+
Then record what they do from your actions. AguMail knows them by their Wasp user id, so that is all an event needs:
|
|
327
372
|
|
|
328
373
|
```ts
|
|
329
374
|
// src/campaigns/operations.ts
|
|
@@ -332,18 +377,17 @@ import { noThrow } from 'agumail-js';
|
|
|
332
377
|
import { agumail } from '../server/agumail';
|
|
333
378
|
|
|
334
379
|
export const createCampaign: CreateCampaign<{ name: string }, { id: string }> = async ({ name }, context) => {
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
380
|
+
const user = context.user!;
|
|
381
|
+
const campaign = await context.entities.Campaign.create({ data: { name, userId: user.id } });
|
|
382
|
+
void agumail.track(
|
|
383
|
+
{
|
|
384
|
+
userId: user.id,
|
|
385
|
+
event: 'Created campaign',
|
|
386
|
+
properties: { campaignId: campaign.id },
|
|
387
|
+
uniqueId: `campaign_created:${campaign.id}`,
|
|
388
|
+
},
|
|
389
|
+
noThrow
|
|
390
|
+
);
|
|
347
391
|
return campaign;
|
|
348
392
|
};
|
|
349
393
|
```
|
|
@@ -361,6 +405,7 @@ agumail.queue.track({ email: user.email, event: 'Signed up' });
|
|
|
361
405
|
- The queue sends itself (a flush) once 500 records are waiting, or 5 seconds after the first one was queued. `flushAt` and `flushIntervalMs` change both.
|
|
362
406
|
- A flush sends every queued contact first, 500 to a request, then every event, so an event never arrives before its contact. The `contact` you pass to `queue.track` goes out with the contacts.
|
|
363
407
|
- A flush that still fails after its retries goes to `onError`, and its records are dropped, not queued again.
|
|
408
|
+
- A queued contact AguMail refuses because its email belongs to someone else is a `conflicts` warning, and a `userId` nobody has is an `unknown_user_ids` warning (see [Identify by your app's user id](#identify-by-your-apps-user-id)).
|
|
364
409
|
- The queue lives in memory, and its timer never keeps a Node process running. Records still queued when the process stops are lost, so flush first.
|
|
365
410
|
|
|
366
411
|
`flush()` sends everything queued now, and resolves when that's done. It never rejects:
|
|
@@ -443,7 +488,7 @@ const { loaderUrl, forms } = await agumail.forms.list();
|
|
|
443
488
|
Check these in order:
|
|
444
489
|
|
|
445
490
|
1. **Is the person Subscribed?** Flows started by an event only email Subscribed people. Add them with `identify`, which subscribes them, not with `subscribed: false` and not only through `track`'s `contact`. `agumail.contacts.status(email)` tells you where they stand.
|
|
446
|
-
2. **Did AguMail have them before the event?** An event for someone AguMail doesn't have is skipped and counted in `unknownContacts`, and `onWarning` gets `unknown_contacts`. Identify them first and wait for it, or pass `contact`, or use the queue.
|
|
491
|
+
2. **Did AguMail have them before the event?** An event for someone AguMail doesn't have is skipped and counted in `unknownContacts`, and `onWarning` gets `unknown_contacts`. Identify them first and wait for it, or pass `contact`, or use the queue. An event sent by `userId` needs a contact with that id, so identify them with both their `userId` and their email.
|
|
447
492
|
3. **Did it happen in the last 72 hours?** An `occurredAt` older than that counts as history: stored, but no flow starts. `onWarning` gets `history`.
|
|
448
493
|
4. **Was the uniqueId new?** An id AguMail has seen before, for any event, counts in `duplicates` and starts nothing.
|
|
449
494
|
5. **Does the flow's trigger name the same event?** "Upgraded plan" and "upgraded_plan" match, but "Upgraded" doesn't.
|
|
@@ -460,6 +505,7 @@ Check these in order:
|
|
|
460
505
|
| Names | first and last name up to 100 characters, phone up to 40 |
|
|
461
506
|
| Event name | up to 60 characters |
|
|
462
507
|
| `uniqueId` | up to 120 characters |
|
|
508
|
+
| `userId` | up to 200 characters |
|
|
463
509
|
|
|
464
510
|
Every `identify` or `track` is one request (`track` with `contact` is two), so an app sending more than about 2 a second will be slowed down. Use `identifyMany` and `trackMany` for bulk work, and [the queue](#batching-with-the-queue) for a busy app.
|
|
465
511
|
|
|
@@ -506,7 +552,7 @@ Retried errors are tried 3 more times, with growing waits. If AguMail asks for a
|
|
|
506
552
|
|
|
507
553
|
The package refuses to run in a browser, and that includes test setups that fake one, such as jsdom. Run tests that use it in a Node environment, or mock the package.
|
|
508
554
|
|
|
509
|
-
For tests and local development, `disabled: true` sends nothing and needs no key. Calls still check their input and options, so a bad record still fails. They resolve with empty results: zero counts, a generated `uniqueId`, and `status: null` for every address. No warnings fire.
|
|
555
|
+
For tests and local development, `disabled: true` sends nothing and needs no key. Calls still check their input and options, so a bad record still fails. They resolve with empty results: zero counts, a generated `uniqueId`, and `status: null` for every address and user id. No warnings fire.
|
|
510
556
|
|
|
511
557
|
```ts
|
|
512
558
|
const agumail = new AguMail(process.env.AGUMAIL_API_KEY, {
|
|
@@ -550,32 +596,36 @@ Every call also takes `{ throwOnError, timeoutMs, signal }`, and `identifyMany`
|
|
|
550
596
|
## API
|
|
551
597
|
|
|
552
598
|
```ts
|
|
553
|
-
agumail.identify(contact, options?)
|
|
554
|
-
agumail.identifyMany(contacts, options?)
|
|
555
|
-
agumail.track(event, options?)
|
|
556
|
-
agumail.trackMany(events, options?)
|
|
557
|
-
agumail.contacts.status(email, options?)
|
|
558
|
-
agumail.contacts.status(emails, options?)
|
|
559
|
-
agumail.contacts.
|
|
560
|
-
agumail.contacts.
|
|
561
|
-
agumail.
|
|
562
|
-
agumail.
|
|
563
|
-
agumail.
|
|
564
|
-
agumail.
|
|
565
|
-
agumail.
|
|
566
|
-
agumail.
|
|
599
|
+
agumail.identify(contact, options?) // Promise<ContactCounts>
|
|
600
|
+
agumail.identifyMany(contacts, options?) // Promise<ContactCounts>
|
|
601
|
+
agumail.track(event, options?) // Promise<TrackResult>
|
|
602
|
+
agumail.trackMany(events, options?) // Promise<TrackManyResult>
|
|
603
|
+
agumail.contacts.status(email, options?) // Promise<ContactStatus>
|
|
604
|
+
agumail.contacts.status(emails, options?) // Promise<ContactStatus[]>
|
|
605
|
+
agumail.contacts.statusByUserId(userId, options?) // Promise<ContactStatus>
|
|
606
|
+
agumail.contacts.statusByUserId(userIds, options?) // Promise<ContactStatus[]>
|
|
607
|
+
agumail.contacts.subscribe(emails, options?) // Promise<ContactCounts>
|
|
608
|
+
agumail.contacts.unsubscribe(emails, options?) // Promise<ContactCounts>
|
|
609
|
+
agumail.forms.list(options?) // Promise<FormList>
|
|
610
|
+
agumail.signups.create(signups, options?) // Promise<SignupCounts>
|
|
611
|
+
agumail.ping(options?) // Promise<PingResult>
|
|
612
|
+
agumail.queue.identify(contact) // void
|
|
613
|
+
agumail.queue.track(event) // void
|
|
614
|
+
agumail.flush(options?) // Promise<FlushResult>
|
|
567
615
|
```
|
|
568
616
|
|
|
569
617
|
With `throwOnError: false` (or `noThrow`), a call that resolves to `T` resolves to `{ data: T, error: null } | { data: null, error: AguMailError }` instead. The methods work taken off the client too: `const { identify, track } = agumail`.
|
|
570
618
|
|
|
571
619
|
```ts
|
|
572
|
-
ContactCounts // { created, updated, suppressedSkipped, overCapacity, confirmationsSent
|
|
620
|
+
ContactCounts // { created, updated, suppressedSkipped, overCapacity, confirmationsSent?,
|
|
621
|
+
// unknownContacts?, conflicts?, conflicted?: ContactConflict[] }
|
|
622
|
+
ContactConflict // { index, userId, email, reason: 'email_taken' | 'user_id_mismatch' }
|
|
573
623
|
TrackResult // { created, duplicates, unknownContacts, uniqueId, contact?: ContactCounts }
|
|
574
624
|
TrackManyResult // { created, duplicates, unknownContacts, uniqueIds, contacts?: ContactCounts }
|
|
575
|
-
ContactStatus // { email, status, subscribed }
|
|
625
|
+
ContactStatus // { email, userId, status, subscribed }
|
|
576
626
|
FormList // { loaderUrl, forms: { slug, name, doubleOptIn, publishedAt }[] }
|
|
577
627
|
SignupCounts // { received, overCapacity, limited }
|
|
578
|
-
PingResult // { shop: { userId, shopName } }
|
|
628
|
+
PingResult // { shop: { userId, shopName } }: userId is your AguMail account's id
|
|
579
629
|
FlushResult // { contacts: ContactCounts, events: EventCounts, error: AguMailError | null }
|
|
580
630
|
```
|
|
581
631
|
|
|
@@ -583,7 +633,6 @@ These types, and `Contact`, `TrackEvent`, `Signup` and the rest, are exported, a
|
|
|
583
633
|
|
|
584
634
|
## Coming
|
|
585
635
|
|
|
586
|
-
- Identifying contacts by your own user id, not only by email.
|
|
587
636
|
- Deleting a contact.
|
|
588
637
|
- Verifying AguMail's webhooks, for unsubscribes and bounces.
|
|
589
638
|
- Syncing customers from Stripe and users from auth providers.
|