@cowliss/cli 0.9.0 → 0.11.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/{examples → blueprints}/abandoned-checkout/journeys/abandoned-checkout.ts +1 -1
- package/{examples → blueprints}/activity-decay/journeys/activity-decay.ts +1 -1
- package/blueprints/ecommerce/emails/_shell.tsx +133 -0
- package/blueprints/ecommerce/emails/abandoned-checkout-last-call.tsx +46 -0
- package/blueprints/ecommerce/emails/abandoned-checkout.tsx +46 -0
- package/blueprints/ecommerce/emails/order-thanks.tsx +44 -0
- package/blueprints/ecommerce/emails/replenishment.tsx +40 -0
- package/blueprints/ecommerce/emails/review-request.tsx +44 -0
- package/blueprints/ecommerce/emails/vip-thanks.tsx +51 -0
- package/blueprints/ecommerce/emails/welcome.tsx +48 -0
- package/blueprints/ecommerce/emails/winback-offer.tsx +41 -0
- package/blueprints/ecommerce/emails/winback-reminder.tsx +45 -0
- package/blueprints/ecommerce/journeys/_helpers.ts +14 -0
- package/blueprints/ecommerce/journeys/abandoned-checkout.ts +44 -0
- package/blueprints/ecommerce/journeys/first-order-thanks.ts +40 -0
- package/blueprints/ecommerce/journeys/replenishment.ts +39 -0
- package/blueprints/ecommerce/journeys/vip.ts +29 -0
- package/blueprints/ecommerce/journeys/welcome.ts +42 -0
- package/blueprints/ecommerce/journeys/winback.ts +35 -0
- package/blueprints/ecommerce/scenarios/abandoned-checkout.last-call.json +30 -0
- package/blueprints/ecommerce/scenarios/abandoned-checkout.recovered.json +24 -0
- package/blueprints/ecommerce/scenarios/abandoned-checkout.reminded.json +33 -0
- package/blueprints/ecommerce/scenarios/first-order-thanks.delivered.json +35 -0
- package/blueprints/ecommerce/scenarios/first-order-thanks.never-delivered.json +21 -0
- package/blueprints/ecommerce/scenarios/replenishment.due.json +21 -0
- package/blueprints/ecommerce/scenarios/replenishment.reordered.json +20 -0
- package/blueprints/ecommerce/scenarios/vip.crossed.json +22 -0
- package/blueprints/ecommerce/scenarios/welcome.new-shopper.json +21 -0
- package/blueprints/ecommerce/scenarios/winback.returned.json +26 -0
- package/blueprints/ecommerce/scenarios/winback.still-gone.json +30 -0
- package/blueprints/ecommerce/segments/first-time-buyers.ts +24 -0
- package/blueprints/ecommerce/segments/lapsed.ts +27 -0
- package/blueprints/ecommerce/segments/repeat-buyers.ts +18 -0
- package/blueprints/ecommerce/segments/vip-customers.ts +20 -0
- package/blueprints/mobile-game/emails/_shell.tsx +138 -0
- package/blueprints/mobile-game/emails/day-two-return.tsx +41 -0
- package/blueprints/mobile-game/emails/lapsed-offer.tsx +41 -0
- package/blueprints/mobile-game/emails/lapsed-reminder.tsx +48 -0
- package/blueprints/mobile-game/emails/purchase-receipt.tsx +47 -0
- package/blueprints/mobile-game/emails/spender-thanks.tsx +53 -0
- package/blueprints/mobile-game/emails/stuck-hint.tsx +48 -0
- package/blueprints/mobile-game/emails/tutorial-nudge.tsx +48 -0
- package/blueprints/mobile-game/journeys/_helpers.ts +14 -0
- package/blueprints/mobile-game/journeys/day-two-return.ts +40 -0
- package/blueprints/mobile-game/journeys/first-purchase.ts +34 -0
- package/blueprints/mobile-game/journeys/lapsed-player.ts +39 -0
- package/blueprints/mobile-game/journeys/onboarding.ts +35 -0
- package/blueprints/mobile-game/journeys/spender-care.ts +41 -0
- package/blueprints/mobile-game/journeys/stuck.ts +58 -0
- package/blueprints/mobile-game/scenarios/day-two-return.came-back.json +16 -0
- package/blueprints/mobile-game/scenarios/day-two-return.no-show.json +21 -0
- package/blueprints/mobile-game/scenarios/first-purchase.paid.json +21 -0
- package/blueprints/mobile-game/scenarios/lapsed-player.returned.json +26 -0
- package/blueprints/mobile-game/scenarios/lapsed-player.still-gone.json +30 -0
- package/blueprints/mobile-game/scenarios/onboarding.finished.json +16 -0
- package/blueprints/mobile-game/scenarios/onboarding.stalled.json +21 -0
- package/blueprints/mobile-game/scenarios/spender-care.crossed.json +22 -0
- package/blueprints/mobile-game/scenarios/stuck.cleared.json +16 -0
- package/blueprints/mobile-game/scenarios/stuck.three-fails.json +30 -0
- package/blueprints/mobile-game/segments/active-players.ts +30 -0
- package/blueprints/mobile-game/segments/lapsed-players.ts +29 -0
- package/blueprints/mobile-game/segments/new-players.ts +31 -0
- package/blueprints/mobile-game/segments/paying-players.ts +21 -0
- package/blueprints/saas-trial/emails/_shell.tsx +151 -0
- package/blueprints/saas-trial/emails/activated.tsx +61 -0
- package/blueprints/saas-trial/emails/activation-help.tsx +49 -0
- package/blueprints/saas-trial/emails/activation-nudge.tsx +48 -0
- package/blueprints/saas-trial/emails/invite-your-team.tsx +58 -0
- package/blueprints/saas-trial/emails/trial-converted.tsx +57 -0
- package/blueprints/saas-trial/emails/trial-ended.tsx +52 -0
- package/blueprints/saas-trial/emails/trial-ending-activated.tsx +53 -0
- package/blueprints/saas-trial/emails/trial-ending-unactivated.tsx +54 -0
- package/blueprints/saas-trial/emails/trial-welcome.tsx +57 -0
- package/blueprints/saas-trial/journeys/_helpers.ts +14 -0
- package/blueprints/saas-trial/journeys/activated.ts +42 -0
- package/blueprints/saas-trial/journeys/activation-nudge.ts +44 -0
- package/blueprints/saas-trial/journeys/invite-your-team.ts +42 -0
- package/blueprints/saas-trial/journeys/trial-ended.ts +39 -0
- package/blueprints/saas-trial/journeys/trial-ending.ts +58 -0
- package/blueprints/saas-trial/journeys/trial-welcome.ts +35 -0
- package/blueprints/saas-trial/scenarios/activated.first-project.json +14 -0
- package/blueprints/saas-trial/scenarios/activated.second-project.json +12 -0
- package/blueprints/saas-trial/scenarios/activation-nudge.activated-late.json +16 -0
- package/blueprints/saas-trial/scenarios/activation-nudge.activated.json +8 -0
- package/blueprints/saas-trial/scenarios/activation-nudge.stalled.json +23 -0
- package/blueprints/saas-trial/scenarios/invite-your-team.alone.json +16 -0
- package/blueprints/saas-trial/scenarios/invite-your-team.invited.json +8 -0
- package/blueprints/saas-trial/scenarios/trial-ended.converted.json +16 -0
- package/blueprints/saas-trial/scenarios/trial-ended.expired.json +13 -0
- package/blueprints/saas-trial/scenarios/trial-ending.activated.json +20 -0
- package/blueprints/saas-trial/scenarios/trial-ending.converted.json +12 -0
- package/blueprints/saas-trial/scenarios/trial-ending.unactivated.json +16 -0
- package/blueprints/saas-trial/scenarios/trial-welcome.started.json +13 -0
- package/blueprints/saas-trial/segments/activated-users.ts +25 -0
- package/blueprints/saas-trial/segments/converted.ts +28 -0
- package/blueprints/saas-trial/segments/trialing.ts +26 -0
- package/blueprints/saas-trial/segments/unactivated.ts +27 -0
- package/{examples → blueprints}/winback/journeys/winback.ts +18 -7
- package/blueprints/winback/scenarios/winback.bought-again.json +14 -0
- package/{examples/winback/scenarios/winback.json → blueprints/winback/scenarios/winback.lapsed.json} +2 -5
- package/dist/guest/{driver-poSdZIj8.js → driver-B_pjmy5g.js} +2 -1
- package/dist/guest/driver-emails.js +3 -2
- package/dist/guest/driver.d.ts +1 -1
- package/dist/guest/driver.js +1 -1
- package/dist/guest/{index-EqBCZnpq.d.ts → index-DADBwPaG.d.ts} +63 -12
- package/dist/guest/{journeys-F8Yk8s1P.js → journeys-v2-Djgs8U91.js} +87 -71
- package/dist/guest/journeys.d.ts +1 -1
- package/dist/guest/journeys.js +58 -1
- package/dist/guest/segments.d.ts +81 -0
- package/dist/guest/segments.js +35 -0
- package/dist/index.js +1191 -398
- package/package.json +6 -2
- /package/{examples → blueprints}/abandoned-checkout/emails/abandoned-checkout.tsx +0 -0
- /package/{examples → blueprints}/abandoned-checkout/scenarios/abandoned-checkout.recovered.json +0 -0
- /package/{examples → blueprints}/abandoned-checkout/scenarios/abandoned-checkout.timeout.json +0 -0
- /package/{examples → blueprints}/activity-decay/scenarios/activity-decay.json +0 -0
- /package/{examples → blueprints}/winback/emails/winback.tsx +0 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"user": {
|
|
3
|
+
"id": "usr_1",
|
|
4
|
+
"traits": { "email": "ada@example.com", "firstName": "Ada" }
|
|
5
|
+
},
|
|
6
|
+
"events": [{ "at": "5d", "event": "subscription.created" }],
|
|
7
|
+
"expect": [
|
|
8
|
+
{
|
|
9
|
+
"activity": "sendEmail",
|
|
10
|
+
"input": {
|
|
11
|
+
"template": "trial-converted",
|
|
12
|
+
"props": { "firstName": "Ada" }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"user": {
|
|
3
|
+
"id": "usr_1",
|
|
4
|
+
"traits": {
|
|
5
|
+
"email": "ada@example.com",
|
|
6
|
+
"firstName": "Ada",
|
|
7
|
+
"activated": true
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
"events": [],
|
|
11
|
+
"expect": [
|
|
12
|
+
{
|
|
13
|
+
"activity": "sendEmail",
|
|
14
|
+
"input": {
|
|
15
|
+
"template": "trial-ending-activated",
|
|
16
|
+
"props": { "firstName": "Ada" }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"user": {
|
|
3
|
+
"id": "usr_1",
|
|
4
|
+
"traits": { "email": "ada@example.com", "firstName": "Ada" }
|
|
5
|
+
},
|
|
6
|
+
"events": [],
|
|
7
|
+
"expect": [
|
|
8
|
+
{
|
|
9
|
+
"activity": "sendEmail",
|
|
10
|
+
"input": {
|
|
11
|
+
"template": "trial-ending-unactivated",
|
|
12
|
+
"props": { "firstName": "Ada" }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineSegment } from "@cowliss/cli/segments";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reached the activation milestone: created at least one project, ever.
|
|
5
|
+
* No trial predicate, so someone who activates and then subscribes stays
|
|
6
|
+
* in — this is the audience you compare everything else against, and it
|
|
7
|
+
* would tell you nothing if people dropped out of it on payday.
|
|
8
|
+
*
|
|
9
|
+
* Named `activated-users` and not `activated`: a segment name and a
|
|
10
|
+
* journey key share one namespace per organization, and the journey that
|
|
11
|
+
* congratulates someone for getting here is called `activated`.
|
|
12
|
+
*
|
|
13
|
+
* Change `project.created` to your own milestone here and in
|
|
14
|
+
* `unactivated.ts` together. A milestone that means one thing in one file
|
|
15
|
+
* and another in the other is how a person ends up nudged to do what they
|
|
16
|
+
* have already done.
|
|
17
|
+
*/
|
|
18
|
+
export default defineSegment({
|
|
19
|
+
description: "Created at least one project.",
|
|
20
|
+
tags: ["saas", "activation"],
|
|
21
|
+
match: "all",
|
|
22
|
+
predicates: [
|
|
23
|
+
{ kind: "event", name: "project.created", op: "performed", atLeast: 1 },
|
|
24
|
+
],
|
|
25
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { defineSegment } from "@cowliss/cli/segments";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Paying: sent `subscription.created` at least once.
|
|
5
|
+
*
|
|
6
|
+
* No journey in this blueprint enrolls from it, and that is on purpose.
|
|
7
|
+
* It is here because it is the number the business watches, because it is
|
|
8
|
+
* what `trialing` is defined against, and because it is the audience you
|
|
9
|
+
* want on hand the day you write the programme that comes after the trial
|
|
10
|
+
* — onboarding, expansion, renewal — which is a different set of files
|
|
11
|
+
* from this one.
|
|
12
|
+
*
|
|
13
|
+
* A segment does not need a journey to earn its place. The dashboard
|
|
14
|
+
* counts it, and every other audience here is drawn relative to it.
|
|
15
|
+
*/
|
|
16
|
+
export default defineSegment({
|
|
17
|
+
description: "Subscribed to a paid plan.",
|
|
18
|
+
tags: ["saas", "revenue"],
|
|
19
|
+
match: "all",
|
|
20
|
+
predicates: [
|
|
21
|
+
{
|
|
22
|
+
kind: "event",
|
|
23
|
+
name: "subscription.created",
|
|
24
|
+
op: "performed",
|
|
25
|
+
atLeast: 1,
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineSegment } from "@cowliss/cli/segments";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Started a trial and has not subscribed. Both predicates matter: the
|
|
5
|
+
* first is the trial, the second is what takes someone out of it the
|
|
6
|
+
* moment they pay, so the trial journeys stop on their own rather than
|
|
7
|
+
* checking for a subscription at every step.
|
|
8
|
+
*
|
|
9
|
+
* A file rather than a trigger's inline definition (ADR 0023) because the
|
|
10
|
+
* dashboard reads it too: this is the audience whose size is the answer to
|
|
11
|
+
* "how many trials are open right now".
|
|
12
|
+
*
|
|
13
|
+
* `trial.started` is the one event you have to send yourself; the other
|
|
14
|
+
* three are things your product already does. If yours is called
|
|
15
|
+
* `Trial Started` or `signup_completed`, change the name here and in
|
|
16
|
+
* `unactivated.ts` rather than renaming the event in your own app.
|
|
17
|
+
*/
|
|
18
|
+
export default defineSegment({
|
|
19
|
+
description: "In a trial: started one and has not subscribed.",
|
|
20
|
+
tags: ["saas", "trial"],
|
|
21
|
+
match: "all",
|
|
22
|
+
predicates: [
|
|
23
|
+
{ kind: "event", name: "trial.started", op: "performed", atLeast: 1 },
|
|
24
|
+
{ kind: "event", name: "subscription.created", op: "notPerformed" },
|
|
25
|
+
],
|
|
26
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { defineSegment } from "@cowliss/cli/segments";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Started a trial and has not reached the activation milestone yet. This
|
|
5
|
+
* is the audience the whole programme is built around: what people in it
|
|
6
|
+
* do next decides your conversion rate.
|
|
7
|
+
*
|
|
8
|
+
* `project.created` is the milestone, and it is the one line in this
|
|
9
|
+
* blueprint most people will change. It stands in for the moment someone
|
|
10
|
+
* has actually used your product rather than looked at it, so pick the
|
|
11
|
+
* event that means that for yours: a workspace created, a first API call,
|
|
12
|
+
* a repository connected, a document shared. Whatever you pick, name it
|
|
13
|
+
* here and in `activated-users.ts`, and the four journeys follow.
|
|
14
|
+
*
|
|
15
|
+
* Nobody leaves this audience by waiting. They leave it by activating, and
|
|
16
|
+
* they never come back, so a journey enrolling from it runs once per
|
|
17
|
+
* person.
|
|
18
|
+
*/
|
|
19
|
+
export default defineSegment({
|
|
20
|
+
description: "In a trial and has not created a project yet.",
|
|
21
|
+
tags: ["saas", "activation"],
|
|
22
|
+
match: "all",
|
|
23
|
+
predicates: [
|
|
24
|
+
{ kind: "event", name: "trial.started", op: "performed", atLeast: 1 },
|
|
25
|
+
{ kind: "event", name: "project.created", op: "notPerformed" },
|
|
26
|
+
],
|
|
27
|
+
});
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { defineJourney } from "@cowliss/cli/journeys";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Winback: `purchase_completed` starts
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* and one winback email goes out.
|
|
4
|
+
* Winback: `purchase_completed` starts an execution and marks the profile as
|
|
5
|
+
* having bought recently; every further purchase inside 30 days restarts the
|
|
6
|
+
* execution, which resets the silence clock with a fresh, empty journal;
|
|
7
|
+
* after 30 days of silence the flag is unset and one winback email goes out.
|
|
8
|
+
*
|
|
9
|
+
* `purchase_completed` is a name your own app sends through `track`, not one
|
|
10
|
+
* Cowliss defines: the only events Cowliss writes itself are the reserved
|
|
11
|
+
* `system.` ones. Nothing here runs until your app tracks a purchase.
|
|
8
12
|
*
|
|
9
13
|
* `marketing` and no `enrollment`, so each customer is won back once, ever:
|
|
10
14
|
* the purchase that answers the email does not start a second execution.
|
|
@@ -12,7 +16,7 @@ import { defineJourney } from "@cowliss/cli/journeys";
|
|
|
12
16
|
* customer back every time they lapse, say how long to leave between
|
|
13
17
|
* attempts: `enrollment: { cooldown: "180d" }`.
|
|
14
18
|
*
|
|
15
|
-
*
|
|
19
|
+
* Blueprint: add it with `cow add blueprint winback`; rename the purchase event,
|
|
16
20
|
* the flag trait, and the email to your domain's vocabulary.
|
|
17
21
|
*/
|
|
18
22
|
export default defineJourney({
|
|
@@ -23,8 +27,15 @@ export default defineJourney({
|
|
|
23
27
|
tags: ["demo", "retention"],
|
|
24
28
|
run: async (_event, api) => {
|
|
25
29
|
await api.traits.set("purchased_recently", true);
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
const purchase = await api.waitForEvent("purchase_completed", {
|
|
31
|
+
timeout: "30d",
|
|
32
|
+
});
|
|
33
|
+
if (purchase) {
|
|
34
|
+
// Another purchase inside 30 days: restart to reset the silence timer
|
|
35
|
+
// with a fresh, empty journal. Looping here instead would journal one
|
|
36
|
+
// entry per purchase, so the best customer is the one whose execution
|
|
37
|
+
// eventually fails on the 1,000-call cap.
|
|
38
|
+
await api.restart({ event: purchase });
|
|
28
39
|
}
|
|
29
40
|
await api.traits.unset("purchased_recently");
|
|
30
41
|
const me = await api.profile.get();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"user": {
|
|
3
|
+
"id": "usr_1",
|
|
4
|
+
"traits": { "email": "ada@example.com", "firstName": "Ada" }
|
|
5
|
+
},
|
|
6
|
+
"events": [{ "at": "10d", "event": "purchase_completed" }],
|
|
7
|
+
"expect": [
|
|
8
|
+
{
|
|
9
|
+
"activity": "setTrait",
|
|
10
|
+
"input": { "key": "purchased_recently", "value": true }
|
|
11
|
+
},
|
|
12
|
+
{ "activity": "restart" }
|
|
13
|
+
]
|
|
14
|
+
}
|
package/{examples/winback/scenarios/winback.json → blueprints/winback/scenarios/winback.lapsed.json}
RENAMED
|
@@ -3,10 +3,7 @@
|
|
|
3
3
|
"id": "usr_1",
|
|
4
4
|
"traits": { "email": "ada@example.com", "firstName": "Ada" }
|
|
5
5
|
},
|
|
6
|
-
"events": [
|
|
7
|
-
{ "at": "10d", "event": "purchase_completed" },
|
|
8
|
-
{ "at": "25d", "event": "purchase_completed" }
|
|
9
|
-
],
|
|
6
|
+
"events": [],
|
|
10
7
|
"expect": [
|
|
11
8
|
{
|
|
12
9
|
"activity": "setTrait",
|
|
@@ -15,7 +12,7 @@
|
|
|
15
12
|
{ "activity": "unsetTrait", "input": { "key": "purchased_recently" } },
|
|
16
13
|
{
|
|
17
14
|
"activity": "sendEmail",
|
|
18
|
-
"input": { "template": "winback" }
|
|
15
|
+
"input": { "template": "winback", "props": { "firstName": "Ada" } }
|
|
19
16
|
}
|
|
20
17
|
]
|
|
21
18
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as journeyStepOutputSchema, r as manifestOutputSchema, t as guestInputSchema } from "./journeys-v2-Djgs8U91.js";
|
|
2
|
+
import { CapabilityError } from "./journeys.js";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
|
|
4
5
|
//#region src/guest/api.ts
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { i as templateRenderOutputSchema, t as guestInputSchema } from "./journeys-v2-Djgs8U91.js";
|
|
2
|
+
import { CapabilityError } from "./journeys.js";
|
|
3
|
+
import { n as readManifest, t as asTemplate } from "./driver-B_pjmy5g.js";
|
|
3
4
|
import { createElement } from "react";
|
|
4
5
|
import { renderToStaticMarkup } from "react-dom/server.browser";
|
|
5
6
|
|
package/dist/guest/driver.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { c as TemplateRenderOutput, o as JourneyStepOutput, s as ManifestOutput } from "./index-DADBwPaG.js";
|
|
2
2
|
import { n as Template } from "./emails-5GindgMQ.js";
|
|
3
3
|
//#region src/guest/driver.d.ts
|
|
4
4
|
/**
|
package/dist/guest/driver.js
CHANGED
|
@@ -284,6 +284,8 @@ declare const manifestOutputSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
284
284
|
withinDays: z.ZodOptional<z.ZodNumber>;
|
|
285
285
|
}, z.core.$strip>], "kind">>;
|
|
286
286
|
}, z.core.$strict>;
|
|
287
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
288
|
+
segment: z.ZodString;
|
|
287
289
|
}, z.core.$strict>]>;
|
|
288
290
|
purpose: z.ZodString;
|
|
289
291
|
enrollment: z.ZodOptional<z.ZodObject<{
|
|
@@ -328,18 +330,19 @@ type Duration = z.infer<typeof durationSchema>;
|
|
|
328
330
|
* row carries its `app_id`, and the runtime compares that with the event's.
|
|
329
331
|
* An `appId` here would be a second way to say it, and the way to leak.
|
|
330
332
|
*
|
|
331
|
-
* A segment trigger
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
333
|
+
* A segment trigger says which audience in two ways (ADR 0016, ADR 0023).
|
|
334
|
+
* Inline, carrying the predicate list itself: the push materializes one
|
|
335
|
+
* segment row per journey that does, owned by the journey and named after
|
|
336
|
+
* its key. Or by name, naming a `segments/<key>.ts` file of the same push:
|
|
337
|
+
* the name resolves inside the tree at build time and the push writes that
|
|
338
|
+
* file's row, so there is still no order to get wrong and no row outside the
|
|
339
|
+
* repository a trigger can point at. Either way the definition is data —
|
|
340
|
+
* validated here, carried on the version, never compiled and never executed.
|
|
337
341
|
*
|
|
338
|
-
*
|
|
339
|
-
* `appId` key
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
* the only narrowing there is.
|
|
342
|
+
* Every member is strict, so a journey holding the retired `source` or
|
|
343
|
+
* `appId` key fails to build instead of silently triggering on every app or
|
|
344
|
+
* on nothing. There is deliberately no pipe filter and no app filter: the
|
|
345
|
+
* journey's own app is the only narrowing there is.
|
|
343
346
|
*/
|
|
344
347
|
declare const triggerSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
345
348
|
event: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>;
|
|
@@ -375,6 +378,8 @@ declare const triggerSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
375
378
|
withinDays: z.ZodOptional<z.ZodNumber>;
|
|
376
379
|
}, z.core.$strip>], "kind">>;
|
|
377
380
|
}, z.core.$strict>;
|
|
381
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
382
|
+
segment: z.ZodString;
|
|
378
383
|
}, z.core.$strict>]>;
|
|
379
384
|
type Trigger = z.infer<typeof triggerSchema>;
|
|
380
385
|
/**
|
|
@@ -383,5 +388,51 @@ type Trigger = z.infer<typeof triggerSchema>;
|
|
|
383
388
|
* everything downstream reads the parsed `Trigger`.
|
|
384
389
|
*/
|
|
385
390
|
type TriggerInput = z.input<typeof triggerSchema>;
|
|
391
|
+
/**
|
|
392
|
+
* One `segments/<key>.ts` file (ADR 0023): an audience several journeys of
|
|
393
|
+
* one repository enroll from, written once instead of pasted into each
|
|
394
|
+
* trigger. It carries no bundle and no digest, because it compiles to
|
|
395
|
+
* nothing: a definition is a predicate list the evaluator folds, so there is
|
|
396
|
+
* no code for an execution to pin and therefore no version of it.
|
|
397
|
+
*
|
|
398
|
+
* Plain rather than strict, for the same reason the other two entries are: a
|
|
399
|
+
* stored manifest has to keep parsing after a field goes away (ADR 0011).
|
|
400
|
+
*/
|
|
401
|
+
declare const manifestSegmentSchema: z.ZodObject<{
|
|
402
|
+
key: z.ZodString;
|
|
403
|
+
tags: z.ZodPipe<z.ZodDefault<z.ZodArray<z.ZodString>>, z.ZodTransform<string[], string[]>>;
|
|
404
|
+
description: z.ZodOptional<z.ZodString>;
|
|
405
|
+
definition: z.ZodObject<{
|
|
406
|
+
match: z.ZodDefault<z.ZodEnum<{
|
|
407
|
+
all: "all";
|
|
408
|
+
any: "any";
|
|
409
|
+
}>>;
|
|
410
|
+
predicates: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
411
|
+
kind: z.ZodLiteral<"trait">;
|
|
412
|
+
name: z.ZodString;
|
|
413
|
+
op: z.ZodEnum<{
|
|
414
|
+
contains: "contains";
|
|
415
|
+
eq: "eq";
|
|
416
|
+
exists: "exists";
|
|
417
|
+
gt: "gt";
|
|
418
|
+
gte: "gte";
|
|
419
|
+
lt: "lt";
|
|
420
|
+
lte: "lte";
|
|
421
|
+
neq: "neq";
|
|
422
|
+
notExists: "notExists";
|
|
423
|
+
}>;
|
|
424
|
+
value: z.ZodOptional<z.ZodUnknown>;
|
|
425
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
426
|
+
kind: z.ZodLiteral<"event">;
|
|
427
|
+
name: z.ZodString;
|
|
428
|
+
op: z.ZodEnum<{
|
|
429
|
+
notPerformed: "notPerformed";
|
|
430
|
+
performed: "performed";
|
|
431
|
+
}>;
|
|
432
|
+
atLeast: z.ZodDefault<z.ZodNumber>;
|
|
433
|
+
withinDays: z.ZodOptional<z.ZodNumber>;
|
|
434
|
+
}, z.core.$strip>], "kind">>;
|
|
435
|
+
}, z.core.$strict>;
|
|
436
|
+
}, z.core.$strip>;
|
|
386
437
|
//#endregion
|
|
387
|
-
export {
|
|
438
|
+
export { GuestEvent as a, TemplateRenderOutput as c, manifestSegmentSchema as i, Trigger as n, JourneyStepOutput as o, TriggerInput as r, ManifestOutput as s, Duration as t };
|
|
@@ -81,6 +81,12 @@ const EXECUTION_LIMITS = {
|
|
|
81
81
|
const PUSH_LIMITS = {
|
|
82
82
|
journeys: 100,
|
|
83
83
|
templates: 200,
|
|
84
|
+
/**
|
|
85
|
+
* Segment files (ADR 0023). A segment is a predicate list, not a bundle, so
|
|
86
|
+
* the ceiling is about how many audiences one program describes rather than
|
|
87
|
+
* about bytes; a journey's own inline definition costs nothing here.
|
|
88
|
+
*/
|
|
89
|
+
segments: 100,
|
|
84
90
|
bundleBytes: 2097152,
|
|
85
91
|
sourceBytes: 5242880
|
|
86
92
|
};
|
|
@@ -284,20 +290,25 @@ const patternSchema = z.union([onePatternSchema, z.array(onePatternSchema).min(1
|
|
|
284
290
|
* row carries its `app_id`, and the runtime compares that with the event's.
|
|
285
291
|
* An `appId` here would be a second way to say it, and the way to leak.
|
|
286
292
|
*
|
|
287
|
-
* A segment trigger
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
+
* A segment trigger says which audience in two ways (ADR 0016, ADR 0023).
|
|
294
|
+
* Inline, carrying the predicate list itself: the push materializes one
|
|
295
|
+
* segment row per journey that does, owned by the journey and named after
|
|
296
|
+
* its key. Or by name, naming a `segments/<key>.ts` file of the same push:
|
|
297
|
+
* the name resolves inside the tree at build time and the push writes that
|
|
298
|
+
* file's row, so there is still no order to get wrong and no row outside the
|
|
299
|
+
* repository a trigger can point at. Either way the definition is data —
|
|
300
|
+
* validated here, carried on the version, never compiled and never executed.
|
|
293
301
|
*
|
|
294
|
-
*
|
|
295
|
-
* `appId` key
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
302
|
+
* Every member is strict, so a journey holding the retired `source` or
|
|
303
|
+
* `appId` key fails to build instead of silently triggering on every app or
|
|
304
|
+
* on nothing. There is deliberately no pipe filter and no app filter: the
|
|
305
|
+
* journey's own app is the only narrowing there is.
|
|
306
|
+
*/
|
|
307
|
+
const triggerSchema = z.union([
|
|
308
|
+
z.strictObject({ event: patternSchema }),
|
|
309
|
+
z.strictObject({ segment: segmentDefinitionSchema }),
|
|
310
|
+
z.strictObject({ segment: journeyKeySchema })
|
|
311
|
+
]);
|
|
301
312
|
/**
|
|
302
313
|
* The address half of a from-header: a local part, an `@`, and a dotted
|
|
303
314
|
* domain. Deliberately narrower than RFC 5322 (no quoted local parts, no
|
|
@@ -452,6 +463,40 @@ const manifestTemplateSchema = z.object({
|
|
|
452
463
|
propsSchema: z.record(z.string(), z.unknown()),
|
|
453
464
|
bundle: digestSchema
|
|
454
465
|
});
|
|
466
|
+
/**
|
|
467
|
+
* One `segments/<key>.ts` file (ADR 0023): an audience several journeys of
|
|
468
|
+
* one repository enroll from, written once instead of pasted into each
|
|
469
|
+
* trigger. It carries no bundle and no digest, because it compiles to
|
|
470
|
+
* nothing: a definition is a predicate list the evaluator folds, so there is
|
|
471
|
+
* no code for an execution to pin and therefore no version of it.
|
|
472
|
+
*
|
|
473
|
+
* Plain rather than strict, for the same reason the other two entries are: a
|
|
474
|
+
* stored manifest has to keep parsing after a field goes away (ADR 0011).
|
|
475
|
+
*/
|
|
476
|
+
const manifestSegmentSchema = z.object({
|
|
477
|
+
key: journeyKeySchema,
|
|
478
|
+
/** The author's labels, the way a journey and a template carry them. */
|
|
479
|
+
tags: tagsSchema,
|
|
480
|
+
/** The author's own sentence about who is in it; absent when unwritten. */
|
|
481
|
+
description: z.string().trim().max(500, `description must be at most ${500} characters`).optional(),
|
|
482
|
+
definition: segmentDefinitionSchema
|
|
483
|
+
});
|
|
484
|
+
/**
|
|
485
|
+
* One event pattern the tree reacts to, read off the tree itself: a trigger,
|
|
486
|
+
* a `waitForEvent`, or an event predicate in a segment. Nobody declares it —
|
|
487
|
+
* a second source of truth would disagree with the code the first time an
|
|
488
|
+
* author edited a trigger — so what a build could not read literally is
|
|
489
|
+
* simply absent, which makes the list incomplete and never wrong.
|
|
490
|
+
*
|
|
491
|
+
* The pattern is stored as written: `order.*` is one entry, not an
|
|
492
|
+
* expansion, because it is what the author asked for and what any reader
|
|
493
|
+
* has to match against. `usedBy` names the journeys and segments that named
|
|
494
|
+
* it, sorted, so a reader can say who is waiting.
|
|
495
|
+
*/
|
|
496
|
+
const manifestEventSchema = z.object({
|
|
497
|
+
pattern: onePatternSchema,
|
|
498
|
+
usedBy: z.array(z.string())
|
|
499
|
+
});
|
|
455
500
|
function uniqueKeys(items, ctx, path) {
|
|
456
501
|
const seen = /* @__PURE__ */ new Set();
|
|
457
502
|
for (const [index, item] of items.entries()) {
|
|
@@ -487,6 +532,19 @@ const manifestSchema = z.object({
|
|
|
487
532
|
journeys: z.array(manifestJourneySchema).max(PUSH_LIMITS.journeys),
|
|
488
533
|
templates: z.array(manifestTemplateSchema).max(PUSH_LIMITS.templates),
|
|
489
534
|
/**
|
|
535
|
+
* The `segments/` files this project pushes (ADR 0023). Defaulted rather
|
|
536
|
+
* than required: every manifest stored before segments were files carries
|
|
537
|
+
* none, and a project with no shared audience still writes the field.
|
|
538
|
+
*/
|
|
539
|
+
segments: z.array(manifestSegmentSchema).max(PUSH_LIMITS.segments).default([]),
|
|
540
|
+
/**
|
|
541
|
+
* The event patterns this tree reacts to, collected from the journeys
|
|
542
|
+
* and segments above rather than declared. Defaulted for the same reason
|
|
543
|
+
* `segments` is: every manifest stored before the collection existed
|
|
544
|
+
* carries none.
|
|
545
|
+
*/
|
|
546
|
+
events: z.array(manifestEventSchema).default([]),
|
|
547
|
+
/**
|
|
490
548
|
* The consent purposes this project declares, copied from `cow.json`.
|
|
491
549
|
* Optional rather than defaulted: a push made before purposes existed
|
|
492
550
|
* carries none, and its stored manifest still parses.
|
|
@@ -497,7 +555,22 @@ const manifestSchema = z.object({
|
|
|
497
555
|
}).superRefine((manifest, ctx) => {
|
|
498
556
|
uniqueKeys(manifest.journeys, ctx, "journeys");
|
|
499
557
|
uniqueKeys(manifest.templates, ctx, "templates");
|
|
558
|
+
uniqueKeys(manifest.segments, ctx, "segments");
|
|
500
559
|
uniqueKeys(manifest.purposes ?? [], ctx, "purposes");
|
|
560
|
+
const segmentKeys = new Set(manifest.segments.map((segment) => segment.key));
|
|
561
|
+
for (const [index, journey] of manifest.journeys.entries()) {
|
|
562
|
+
const named = "segment" in journey.trigger && typeof journey.trigger.segment === "string" ? journey.trigger.segment : null;
|
|
563
|
+
if (named !== null && !segmentKeys.has(named)) ctx.addIssue({
|
|
564
|
+
code: "custom",
|
|
565
|
+
message: `journey "${journey.key}" enrolls from the segment "${named}", which this push does not carry. Push the segments/${named}.ts file with it, or write the predicate list in the trigger.`,
|
|
566
|
+
path: [
|
|
567
|
+
"journeys",
|
|
568
|
+
index,
|
|
569
|
+
"trigger",
|
|
570
|
+
"segment"
|
|
571
|
+
]
|
|
572
|
+
});
|
|
573
|
+
}
|
|
501
574
|
for (const [index, journey] of manifest.journeys.entries()) if (journey.enrollment && journey.purpose === "transactional") ctx.addIssue({
|
|
502
575
|
code: "custom",
|
|
503
576
|
message: `journey "${journey.key}": a transactional journey enrolls on every trigger, so it takes no enrollment cooldown`,
|
|
@@ -876,61 +949,4 @@ const journeyScenarioSchema = z.object({
|
|
|
876
949
|
});
|
|
877
950
|
|
|
878
951
|
//#endregion
|
|
879
|
-
|
|
880
|
-
/**
|
|
881
|
-
* A capability that failed host-side (an unknown webhook, invalid
|
|
882
|
-
* props, a profile that is not there). The failure is journaled, so a
|
|
883
|
-
* journey that catches it takes the same branch on every replay.
|
|
884
|
-
*/
|
|
885
|
-
var CapabilityError = class extends Error {
|
|
886
|
-
code;
|
|
887
|
-
constructor(code, message) {
|
|
888
|
-
super(message);
|
|
889
|
-
this.name = "CapabilityError";
|
|
890
|
-
this.code = code;
|
|
891
|
-
}
|
|
892
|
-
};
|
|
893
|
-
/**
|
|
894
|
-
* The config half of a journey, validated with the same schema the release
|
|
895
|
-
* manifest is validated with, so a bad trigger, a malformed purpose key, or
|
|
896
|
-
* a `from` that is not an address fails at build time rather than at the
|
|
897
|
-
* first execution. Whether the org declares that purpose, and whether it may
|
|
898
|
-
* send from that domain, are questions only the push can answer.
|
|
899
|
-
*/
|
|
900
|
-
const journeyConfigSchema = manifestJourneySchema.pick({
|
|
901
|
-
trigger: true,
|
|
902
|
-
purpose: true,
|
|
903
|
-
enrollment: true,
|
|
904
|
-
description: true,
|
|
905
|
-
from: true,
|
|
906
|
-
tags: true
|
|
907
|
-
});
|
|
908
|
-
/**
|
|
909
|
-
* Author a journey. Throws at definition time on an invalid config.
|
|
910
|
-
*
|
|
911
|
-
* There is no environment gate here: an organization has one data space, and
|
|
912
|
-
* a journey runs wherever it is on and nowhere else (ADR 0011, ADR 0013).
|
|
913
|
-
*
|
|
914
|
-
* There is no app gate either. A journey belongs to the one app `cow.json`
|
|
915
|
-
* names, so it only ever sees that app's events, profiles and segments
|
|
916
|
-
* (ADR 0022); a trigger naming an app is refused rather than honoured.
|
|
917
|
-
*/
|
|
918
|
-
function defineJourney(input) {
|
|
919
|
-
const config = journeyConfigSchema.parse({
|
|
920
|
-
trigger: input.trigger,
|
|
921
|
-
purpose: input.purpose,
|
|
922
|
-
enrollment: input.enrollment,
|
|
923
|
-
description: input.description,
|
|
924
|
-
from: input.from,
|
|
925
|
-
tags: input.tags
|
|
926
|
-
});
|
|
927
|
-
if (config.from === void 0) throw new Error("defineJourney needs a \"from\": the address this journey sends as, on a domain your organization verified. Add and verify it on the Domains page (Settings, Domains). For example from: \"Billing <billing@your-domain.com>\".");
|
|
928
|
-
return {
|
|
929
|
-
...config,
|
|
930
|
-
from: config.from,
|
|
931
|
-
run: input.run
|
|
932
|
-
};
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
//#endregion
|
|
936
|
-
export { manifestOutputSchema as a, journeyStepOutputSchema as i, defineJourney as n, templateRenderOutputSchema as o, guestInputSchema as r, CapabilityError as t };
|
|
952
|
+
export { manifestJourneySchema as a, templateRenderOutputSchema as i, journeyStepOutputSchema as n, manifestSegmentSchema as o, manifestOutputSchema as r, guestInputSchema as t };
|
package/dist/guest/journeys.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as GuestEvent, n as Trigger, r as TriggerInput, t as Duration } from "./index-DADBwPaG.js";
|
|
2
2
|
//#region src/guest/journeys.d.ts
|
|
3
3
|
/** The event a journey runs for, or the one a `waitForEvent` resolved with. */
|
|
4
4
|
type Event = GuestEvent;
|