@walkeros/cli 4.7.0-next-1790245733116 → 4.7.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/CHANGELOG.md +136 -48
- package/README.md +11 -5
- package/dist/cli.js +9186 -6548
- package/dist/dev.d.ts +7 -0
- package/dist/dev.js +17 -2
- package/dist/dev.js.map +1 -1
- package/dist/examples/README.md +47 -213
- package/dist/examples/customers/README.md +18 -0
- package/dist/examples/customers/cust-1001 +1 -0
- package/dist/examples/customers/cust-42 +1 -0
- package/dist/examples/flow-complete.json +4760 -592
- package/dist/examples/flow-complete.md +884 -225
- package/dist/examples/index.d.ts +2681 -1
- package/dist/examples/index.js +6882 -659
- package/dist/examples/index.js.map +1 -1
- package/dist/examples/shared/README.md +22 -0
- package/dist/index.d.ts +297 -24
- package/dist/index.js +2767 -884
- package/dist/index.js.map +1 -1
- package/examples/README.md +47 -213
- package/examples/customers/README.md +18 -0
- package/examples/customers/cust-1001 +1 -0
- package/examples/customers/cust-42 +1 -0
- package/examples/flow-complete.json +4760 -592
- package/examples/flow-complete.md +884 -225
- package/examples/shared/README.md +22 -0
- package/package.json +31 -8
- package/dist/examples/docker-compose.runner.yml +0 -44
- package/examples/docker-compose.runner.yml +0 -44
|
@@ -1,255 +1,914 @@
|
|
|
1
|
-
# flow-complete.json
|
|
1
|
+
# flow-complete.json: the guide
|
|
2
|
+
|
|
3
|
+
One real ecommerce setup in one file, taught in 15 chapters. Every feature below
|
|
4
|
+
points into [`flow-complete.json`](./flow-complete.json) with an RFC 6901 JSON
|
|
5
|
+
Pointer, and every feature id comes from the manifest (`@walkeros/cli/examples`,
|
|
6
|
+
`flowCompleteManifest`), so the guide, the docs and the talk name the same
|
|
7
|
+
things.
|
|
8
|
+
|
|
9
|
+
- **Scenario:** a shop with `page view`, `product view`, `product impression`,
|
|
10
|
+
`product add`, `order complete` and `session start`.
|
|
11
|
+
- **Three flows:** `web` in the browser, `server` behind it, `warehouse` reading
|
|
12
|
+
the Pub/Sub topic into BigQuery.
|
|
13
|
+
- **How to read a chapter:** purpose, features (id, pointer, one line), the
|
|
14
|
+
command lines, the MCP tools, how GTM does it, the doc pages.
|
|
15
|
+
- **Where to run the commands:** the walkerOS repo root, where `npx walkeros`
|
|
16
|
+
resolves the workspace CLI. Elsewhere install `@walkeros/cli` (binary
|
|
17
|
+
`walkeros`) and `@walkeros/runner` (binary `runneros`). Lines below are
|
|
18
|
+
written without `npx`.
|
|
19
|
+
- **Environment for simulation:** the CLI reads the shell environment, not a
|
|
20
|
+
`.env` file. Run this once in the repo root before the server lines: `GCP_SA`
|
|
21
|
+
holds a service account JSON (a throwaway key is enough for simulation: every
|
|
22
|
+
Google client runs on its mock), `FINGERPRINT_SALT`, `EMAIL_SALT` and
|
|
23
|
+
`META_ACCESS_TOKEN` have no defaults on purpose, and `CUSTOMERS_DIR` points
|
|
24
|
+
the customers store at the demo data. Every other value is `$env.NAME:default`
|
|
25
|
+
and works out of the box.
|
|
2
26
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
27
|
+
```bash
|
|
28
|
+
export CUSTOMERS_DIR="$PWD/packages/cli/examples/customers"
|
|
29
|
+
export FINGERPRINT_SALT=demo-fingerprint-salt EMAIL_SALT=demo-email-salt META_ACCESS_TOKEN=demo-meta-token
|
|
30
|
+
export GCP_SA="$(node -e "const { generateKeyPairSync } = require('crypto'); const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048, privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, publicKeyEncoding: { type: 'spki', format: 'pem' } }); console.log(JSON.stringify({ type: 'service_account', project_id: 'demo-project', client_email: 'demo@demo-project.iam.gserviceaccount.com', private_key: privateKey }))")"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- **There is no `walkeros simulate` and no `walkeros run`:** simulate with
|
|
34
|
+
`walkeros push ... --simulate <kind>.<name>`, run with `walkeros bundle` then
|
|
35
|
+
`runneros start`.
|
|
6
36
|
|
|
7
|
-
|
|
37
|
+
```text
|
|
38
|
+
web usercentrics, session, dataLayer, browser
|
|
39
|
+
-> collector.next: pageGroup
|
|
40
|
+
-> ga4 (to Google, main property) gtm (dataLayer) collect (to server)
|
|
8
41
|
|
|
42
|
+
server express /collect /g/collect /walker.js
|
|
43
|
+
before: one [ G-SUBSITE hit -> ga4Decode -> ga4Consent | *.js -> file (stop) | other /g/collect (stop) ]
|
|
44
|
+
next: dedup
|
|
45
|
+
-> collector.next: fingerprint, bot, not impression -> enrich [ loadUser, sessionSave, sessionLoad, validate ]
|
|
46
|
+
-> pubsub (every event, email pseudonymized)
|
|
47
|
+
-> meta, piwikpro, datamanager (each behind eventFilter)
|
|
48
|
+
|
|
49
|
+
warehouse pubsub pull -> bigquery (keeps everything)
|
|
9
50
|
```
|
|
10
|
-
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
11
|
-
│ WEB FLOW │
|
|
12
|
-
│ │
|
|
13
|
-
│ Browser Source ─┐ │
|
|
14
|
-
│ DataLayer Source ──▶ [Enricher] ──▶ Collector ──▶ GA4 │
|
|
15
|
-
│ Demo Source ────┘ ├──▶ API │
|
|
16
|
-
│ └──▶ Debug │
|
|
17
|
-
└─────────────────────────────────────────────────────────────────────────────┘
|
|
18
51
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
52
|
+
## Part I: see it work
|
|
53
|
+
|
|
54
|
+
## tour: One file, one event
|
|
55
|
+
|
|
56
|
+
- **Purpose:** see the whole file pass the strict check, then watch one event
|
|
57
|
+
come out of a source.
|
|
58
|
+
- **Features:**
|
|
59
|
+
- `schema-url` at `/$schema`: The JSON Schema gives editors and agents
|
|
60
|
+
completion and checks for every field.
|
|
61
|
+
- `format-version` at `/version`: version 4 is the flow file format this file
|
|
62
|
+
is written in.
|
|
63
|
+
- `flows-multi` at `/flows`: One file holds three flows: web in the browser,
|
|
64
|
+
server behind it, warehouse reading the topic.
|
|
65
|
+
- `platform` at `/flows/web/config/platform`: platform decides how a flow is
|
|
66
|
+
built: a browser bundle or a server artifact.
|
|
67
|
+
- `flow-config` at `/flows/web/config`: config holds what the build needs:
|
|
68
|
+
platform, url, settings, bundle, observe.
|
|
69
|
+
- `flow-sources` at `/flows/web/sources`: Sources turn whatever happens (a CMP
|
|
70
|
+
decision, a dataLayer push, a click) into events.
|
|
71
|
+
- `flow-transformers` at `/flows/server/transformers`: Transformers are named
|
|
72
|
+
hops that validate, enrich, filter or remember events.
|
|
73
|
+
- `flow-destinations` at `/flows/server/destinations`: Destinations fan out:
|
|
74
|
+
every event reaches each destination that accepts it.
|
|
75
|
+
- `flow-collector` at `/flows/server/collector`: The collector sits between
|
|
76
|
+
sources and destinations and runs its own chain once per event.
|
|
77
|
+
- **CLI:** strict validation checks the schema, every reference, every package
|
|
78
|
+
version, every step's settings and every example link; with the published
|
|
79
|
+
packages of this release this file gives 0 errors and 0 warnings. Three steps
|
|
80
|
+
import a named export, which has no settings schema yet, so they are listed as
|
|
81
|
+
skipped and `--strict` exits 2 on them.
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
walkeros validate packages/cli/examples/flow-complete.json --strict
|
|
85
|
+
walkeros push packages/cli/examples/flow-complete.json -f web -e '{"name":"page view"}' --simulate source.browser
|
|
32
86
|
```
|
|
33
87
|
|
|
34
|
-
|
|
88
|
+
- The second line starts the browser source in a simulated page and prints the
|
|
89
|
+
page view it captures (`trigger: "load"`, `source.type: "browser"`).
|
|
90
|
+
- **MCP:** `flow_validate`, `flow_simulate`, `flow_load`.
|
|
91
|
+
- **In GTM:** preview mode on one container; here one file holds the browser,
|
|
92
|
+
the server and the warehouse, and all three are checked together.
|
|
93
|
+
- **Docs:** `getting-started/flow/index`, `collector/index`.
|
|
94
|
+
|
|
95
|
+
## web-entry: Start where the shop already is
|
|
96
|
+
|
|
97
|
+
- **Purpose:** capture events from what the page already has: the Usercentrics
|
|
98
|
+
banner, the dataLayer the shop pushes to, data-elb tagging.
|
|
99
|
+
- **Features:**
|
|
100
|
+
- `cmp-usercentrics` at `/flows/web/sources/usercentrics`: The Usercentrics
|
|
101
|
+
source turns the banner decision into a walker consent command.
|
|
102
|
+
- `session-source` at `/flows/web/sources/session`: The session source sets
|
|
103
|
+
user.session and user.device and emits session start with the raw click id.
|
|
104
|
+
- `datalayer-source` at `/flows/web/sources/dataLayer`: The dataLayer source
|
|
105
|
+
reads the pushes the shop already makes, so nothing on the page changes.
|
|
106
|
+
- `source-require` at `/flows/web/sources/dataLayer/config/require`: require
|
|
107
|
+
holds the source until consent and session are known.
|
|
108
|
+
- `source-mapping` at `/flows/web/sources/dataLayer/config/mapping`: Source
|
|
109
|
+
mapping renames dataLayer add_to_cart to product add and picks its fields.
|
|
110
|
+
- `browser-source` at `/flows/web/sources/browser`: The browser source reads
|
|
111
|
+
data-elb tagging: page views, clicks and impressions.
|
|
112
|
+
- `source-primary` at `/flows/web/sources/browser/primary`: primary names the
|
|
113
|
+
source whose instance the flow exposes, here the browser elb.
|
|
114
|
+
- `browser-pageview` at `/flows/web/sources/browser/config/settings/pageview`:
|
|
115
|
+
pageview: true sends a page view on load without tagging.
|
|
116
|
+
- `collector-globals` at `/flows/web/collector/globals`: globals.language is
|
|
117
|
+
the default the page can override; the contract requires it.
|
|
118
|
+
- `collector-globals-static` at `/flows/web/collector/globalsStatic`:
|
|
119
|
+
globalsStatic survives a run reset; the site id belongs there.
|
|
120
|
+
- `collector-custom` at `/flows/web/collector/custom`: custom carries values
|
|
121
|
+
for the pipeline that are not event data.
|
|
122
|
+
- `config-url` at `/flows/web/config/url`: url says where a flow lives; other
|
|
123
|
+
flows reach it with $flow.
|
|
124
|
+
- `config-settings` at `/flows/web/config/settings`: Platform settings:
|
|
125
|
+
windowCollector names the global collector instance.
|
|
126
|
+
- `require: ["consent", "session"]` on the dataLayer source means nothing is
|
|
127
|
+
read before the banner decision and the session are known.
|
|
128
|
+
- The browser source is `primary`: its `elb` is the one the page calls.
|
|
129
|
+
- Usercentrics and session examples are shown in the browser (their simulation
|
|
130
|
+
in jsdom is still waiting), the dataLayer and browser examples simulate.
|
|
131
|
+
- **CLI:** the tour line above (`--simulate source.browser`); the same form
|
|
132
|
+
works for `source.dataLayer`.
|
|
133
|
+
- **MCP:** `package_get` (source schemas and examples), `flow_simulate`.
|
|
134
|
+
- **In GTM:** a consent initialization trigger plus a CMP template, and the
|
|
135
|
+
container reading the same dataLayer.
|
|
136
|
+
- **Docs:** `guides/session`, `collector/index`.
|
|
137
|
+
|
|
138
|
+
## server-entry: A first-party endpoint, and a GA4 migration path
|
|
139
|
+
|
|
140
|
+
- **Purpose:** one express source answers the browser (`/collect`), GA4 hits of
|
|
141
|
+
a sub-site (`/g/collect`) and script requests (`/walker.js`).
|
|
142
|
+
- **Features:**
|
|
143
|
+
- `server-url` at `/flows/server/config/url`: The full collect endpoint, local
|
|
144
|
+
by default, SERVER_URL in production.
|
|
145
|
+
- `express-paths` at `/flows/server/sources/express/config/settings/paths`:
|
|
146
|
+
One route per job, each with its own methods: /collect, /g/collect,
|
|
147
|
+
/walker.js.
|
|
148
|
+
- `express-port` at `/flows/server/sources/express/config/settings/port`: port
|
|
149
|
+
lets push --simulate start the source; runneros mounts the handler on its
|
|
150
|
+
own port.
|
|
151
|
+
- `express-cors` at `/flows/server/sources/express/config/settings/cors`: CORS
|
|
152
|
+
response headers for the shop origin; express adds X-Content-Type-Options
|
|
153
|
+
itself.
|
|
154
|
+
- `source-ingest` at `/flows/server/sources/express/config/ingest`: ingest
|
|
155
|
+
keeps the request context (ip, path, headers) for later steps without
|
|
156
|
+
putting it on the event.
|
|
157
|
+
- `ingest-headers` at
|
|
158
|
+
`/flows/server/sources/express/config/ingest/map/userAgent`: Request headers
|
|
159
|
+
are read by path: headers.user-agent, headers.accept-language.
|
|
160
|
+
- `source-async` at `/flows/server/sources/express/config/async`: POST answers
|
|
161
|
+
at once; GET waits so a step can answer with real content.
|
|
162
|
+
- `ga4-decode` at `/flows/server/transformers/ga4Decode`: GA4 hits from a
|
|
163
|
+
sub-site not yet migrated (its own property) become events; the main site
|
|
164
|
+
never sends here and hits of any other property are not decoded, so nothing
|
|
165
|
+
counts twice.
|
|
166
|
+
- **CORS and request headers:** `settings.cors` sets the CORS response headers
|
|
167
|
+
for the shop origin; `config.ingest` reads request headers by path
|
|
168
|
+
(`headers.user-agent`, `headers.accept-language`) into `ingest`, where later
|
|
169
|
+
steps read them without putting them on the event.
|
|
170
|
+
- **GA4 migration path, no double counting:**
|
|
171
|
+
- GA4 hits from a sub-site not yet migrated (its own property) become events;
|
|
172
|
+
the main site never sends here and hits of any other property are not
|
|
173
|
+
decoded, so nothing counts twice.
|
|
174
|
+
- The sub-site property is `G-SUBSITE`: its gtag transport points at our
|
|
175
|
+
server, and the route decodes only hits whose `tid` is `G-SUBSITE`.
|
|
176
|
+
- The main-site property is `G-MAINSITE`: the web `ga4` destination sends it
|
|
177
|
+
straight to Google, never to this server.
|
|
178
|
+
- Sub-site traffic arrives only via `/g/collect`, main-site traffic only via
|
|
179
|
+
`/collect`.
|
|
180
|
+
- walkerOS has no server GA4 destination, so decoded hits are forwarded to
|
|
181
|
+
other tools, never back to GA4.
|
|
182
|
+
- Decoded hits carry the browser language (`ul`, e.g. `en-us`) in
|
|
183
|
+
`user.language`; `ga4Consent` derives `globals.language` from it (`en`, or
|
|
184
|
+
`na` when the hit has none), so a decoded hit with analytics consent passes
|
|
185
|
+
the contract and reaches the vendors like any other event.
|
|
186
|
+
- **CLI:** build the server artifact, then run it. `GCP_SA` must be set (Pub/Sub
|
|
187
|
+
and Data Manager read it as credentials); no step calls Google at start, so a
|
|
188
|
+
throwaway key is enough to become ready and serve `walker.js`, while pushes to
|
|
189
|
+
Pub/Sub and Data Manager need a real one. The artifact carries `shared/` but
|
|
190
|
+
not the demo customers: set `CUSTOMERS_DIR` to the absolute path of
|
|
191
|
+
`packages/cli/examples/customers` in `.env`. The env file must be private
|
|
192
|
+
(`chmod 600 .env`), `runneros` refuses a group-readable one.
|
|
35
193
|
|
|
36
|
-
|
|
194
|
+
```bash
|
|
195
|
+
walkeros bundle packages/cli/examples/flow-complete.json -f server -o dist/server.mjs
|
|
196
|
+
chmod 600 .env
|
|
197
|
+
runneros start dist/server.mjs -p 8080 --env-file .env
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
- One GA4 hit from the sub-site and the script (the event reaches Pub/Sub only
|
|
201
|
+
with a real key; the same requests run in the flow's integration test over
|
|
202
|
+
HTTP):
|
|
37
203
|
|
|
38
204
|
```bash
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
# Open http://localhost:3000 - demo events fire automatically
|
|
205
|
+
curl -X POST 'http://localhost:8080/g/collect?v=2&tid=G-SUBSITE&cid=555.1&en=page_view&dl=https%3A%2F%2Fsub.example.com%2F'
|
|
206
|
+
curl -I http://localhost:8080/walker.js
|
|
42
207
|
```
|
|
43
208
|
|
|
44
|
-
|
|
209
|
+
- **MCP:** `flow_bundle`, `package_get` (`@walkeros/transformer-ga4`,
|
|
210
|
+
`@walkeros/server-source-express`).
|
|
211
|
+
- **In GTM:** a server container client for GA4.
|
|
212
|
+
- **Docs:** `guides/reference-syntax` (ingest paths).
|
|
213
|
+
|
|
214
|
+
## step-envelope: One step shape
|
|
215
|
+
|
|
216
|
+
- **Purpose:** every source, transformer, destination and store has the same
|
|
217
|
+
envelope, and its `examples` are the contract of the step.
|
|
218
|
+
- **Features:**
|
|
219
|
+
- `step-package` at `/flows/server/destinations/meta/package`: package names
|
|
220
|
+
the npm package that implements a step.
|
|
221
|
+
- `step-import` at `/flows/server/destinations/pubsub/import`: import picks a
|
|
222
|
+
named export when one package ships several steps.
|
|
223
|
+
- `step-code` at `/flows/web/transformers/pageGroup/code`: code defines a step
|
|
224
|
+
inline, no package needed.
|
|
225
|
+
- `code-push` at `/flows/web/transformers/pageGroup/code/push`: push is the
|
|
226
|
+
step function, written as $code.
|
|
227
|
+
- `code-type` at `/flows/web/transformers/pageGroup/code/type`: type names the
|
|
228
|
+
inline step in logs and Observe.
|
|
229
|
+
- `step-config` at `/flows/server/destinations/meta/config`: config is the
|
|
230
|
+
same envelope for every step kind: settings, mapping, consent, reliability.
|
|
231
|
+
- `step-settings` at `/flows/server/destinations/meta/config/settings`:
|
|
232
|
+
settings are the package options, checked against the package schema.
|
|
233
|
+
- `step-env` at `/flows/server/transformers/file/env`: env injects what a step
|
|
234
|
+
uses at runtime, here the assets store.
|
|
235
|
+
- `step-examples` at `/flows/web/destinations/ga4/examples`: examples are the
|
|
236
|
+
contract of a step: this in gives this out, checked by simulation.
|
|
237
|
+
- `example-title` at `/flows/web/destinations/ga4/examples/purchase/title`:
|
|
238
|
+
title names the example.
|
|
239
|
+
- `example-description` at
|
|
240
|
+
`/flows/web/destinations/ga4/examples/purchase/description`: description is
|
|
241
|
+
the one teaching sentence the docs render.
|
|
242
|
+
- `example-public` at `/flows/web/destinations/ga4/examples/purchase/public`:
|
|
243
|
+
public marks the one example per step the docs show first.
|
|
244
|
+
- `example-in` at `/flows/web/destinations/ga4/examples/purchase/in`: in is
|
|
245
|
+
what the step receives.
|
|
246
|
+
- `example-out` at `/flows/web/destinations/ga4/examples/purchase/out`: out is
|
|
247
|
+
what the step does, as effects: gtag calls, a return, a request.
|
|
248
|
+
- `example-mapping` at
|
|
249
|
+
`/flows/web/destinations/ga4/examples/purchase/mapping`: mapping shows the
|
|
250
|
+
rule the example exercises.
|
|
251
|
+
- `example-trigger` at
|
|
252
|
+
`/flows/web/sources/browser/examples/productImpression/trigger`: trigger
|
|
253
|
+
says how a source example fires: load, click, impression.
|
|
254
|
+
- `example-command` at
|
|
255
|
+
`/flows/web/destinations/ga4/examples/consentUpdate/command`: command routes
|
|
256
|
+
in through a walker command instead of an event, here consent.
|
|
257
|
+
- **CLI:** check one step against its package schema, then run a destination
|
|
258
|
+
example by hand.
|
|
45
259
|
|
|
46
260
|
```bash
|
|
47
|
-
|
|
48
|
-
|
|
261
|
+
walkeros validate packages/cli/examples/flow-complete.json -f web --path destinations.collect
|
|
262
|
+
walkeros push packages/cli/examples/flow-complete.json -f web -e '{"name":"order complete","data":{"id":"ORD-100","total":149.8,"currency":"EUR"}}' --simulate destination.ga4
|
|
263
|
+
```
|
|
49
264
|
|
|
50
|
-
|
|
51
|
-
|
|
265
|
+
- The second line prints `mapping: none (skipped before mapping)`: the event has
|
|
266
|
+
no functional consent, so GA4 never sees it (see `consent-privacy`).
|
|
267
|
+
- Any example `in` is a ready fixture:
|
|
268
|
+
`jq .flows.server.destinations.meta.examples.purchase.in packages/cli/examples/flow-complete.json > events/order-complete.json`.
|
|
269
|
+
- **MCP:** `flow_examples`, `flow_simulate`, `package_get`.
|
|
270
|
+
- **In GTM:** a tag template with its fields; here the same envelope carries
|
|
271
|
+
settings, mapping, consent and reliability for every step kind.
|
|
272
|
+
- **Docs:** `getting-started/flow/step-examples`.
|
|
273
|
+
|
|
274
|
+
## Part II: shape the data
|
|
275
|
+
|
|
276
|
+
## mapping: Vendor mapping
|
|
277
|
+
|
|
278
|
+
- **Purpose:** turn walkerOS events into vendor calls, one rule per entity and
|
|
279
|
+
action, with values read, computed, looped and defaulted (London prompt 2).
|
|
280
|
+
- **Features:**
|
|
281
|
+
- `ga4-purchase` at
|
|
282
|
+
`/flows/web/destinations/ga4/config/mapping/order/complete`: order complete
|
|
283
|
+
becomes a GA4 purchase with value, currency and items.
|
|
284
|
+
- `destination-mapping` at `/flows/web/destinations/ga4/config/mapping`:
|
|
285
|
+
mapping holds one rule per entity and action.
|
|
286
|
+
- `rule-name` at
|
|
287
|
+
`/flows/web/destinations/ga4/config/mapping/order/complete/name`: name is
|
|
288
|
+
the vendor event name.
|
|
289
|
+
- `rule-data` at
|
|
290
|
+
`/flows/web/destinations/ga4/config/mapping/order/complete/data`: data
|
|
291
|
+
builds the vendor parameters from the event.
|
|
292
|
+
- `value-map` at
|
|
293
|
+
`/flows/web/destinations/ga4/config/mapping/order/complete/data/map`: map
|
|
294
|
+
builds an object key by key.
|
|
295
|
+
- `value-key` at
|
|
296
|
+
`/flows/web/destinations/ga4/config/mapping/order/complete/data/map/coupon/key`:
|
|
297
|
+
key reads a path from the event.
|
|
298
|
+
- `value-fn` at
|
|
299
|
+
`/flows/web/destinations/ga4/config/mapping/order/complete/data/map/value/fn`:
|
|
300
|
+
fn computes a value from the event, here the total rounded to cents. fn and
|
|
301
|
+
key are alternatives: next to a key, fn never runs.
|
|
302
|
+
- `value-loop` at
|
|
303
|
+
`/flows/web/destinations/ga4/config/mapping/order/complete/data/map/items/loop`:
|
|
304
|
+
loop turns nested products into GA4 items.
|
|
305
|
+
- `value-condition` at
|
|
306
|
+
`/flows/web/destinations/ga4/config/mapping/order/complete/data/map/coupon/condition`:
|
|
307
|
+
A value condition sends coupon only when the order has one.
|
|
308
|
+
- `value-fallback` at
|
|
309
|
+
`/flows/web/destinations/ga4/config/mapping/order/complete/data/map/currency`:
|
|
310
|
+
An array of values is a fallback list: the event currency, else the flow
|
|
311
|
+
default.
|
|
312
|
+
- `value-set` at
|
|
313
|
+
`/flows/web/destinations/ga4/config/mapping/product/view/data/map/items/set`:
|
|
314
|
+
set builds an array, here the one item of a product view.
|
|
315
|
+
- `value-value` at
|
|
316
|
+
`/flows/server/destinations/meta/config/mapping/product/add/data/map/currency/value`:
|
|
317
|
+
value is a constant.
|
|
318
|
+
- `rule-condition` at
|
|
319
|
+
`/flows/web/destinations/ga4/config/mapping/order/complete/condition`: A
|
|
320
|
+
rule condition skips the rule, here for orders without a total.
|
|
321
|
+
- `rule-settings` at
|
|
322
|
+
`/flows/server/destinations/piwikpro/config/mapping/order/complete/settings`:
|
|
323
|
+
Rule settings are per-event package options: the Piwik PRO goal for orders.
|
|
324
|
+
- `mapping-wildcard-ignore` at
|
|
325
|
+
`/flows/server/destinations/meta/config/mapping/*/*/ignore`: A wildcard rule
|
|
326
|
+
with ignore sends Meta only what is mapped.
|
|
327
|
+
- `destination-include` at `/flows/web/destinations/ga4/config/include`:
|
|
328
|
+
include flattens whole sections into parameters, here the globals.
|
|
329
|
+
- `rule-extend` at
|
|
330
|
+
`/flows/server/transformers/ga4Decode/config/settings/mapping/purchase/extend`:
|
|
331
|
+
extend patches a package default rule instead of replacing it.
|
|
332
|
+
- `rule-remove` at
|
|
333
|
+
`/flows/server/transformers/ga4Decode/config/settings/mapping/purchase/remove`:
|
|
334
|
+
remove drops fields from the default rule output.
|
|
335
|
+
- **CLI:** the GA4 purchase, with consent and one product:
|
|
52
336
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
-H "Content-Type: application/json" \
|
|
56
|
-
-d '{
|
|
57
|
-
"name": "order complete",
|
|
58
|
-
"data": {"id": "ORD-999", "total": 99.99, "currency": "EUR"},
|
|
59
|
-
"user": {"email": "test@example.com", "id": "U-123"},
|
|
60
|
-
"nested": [{"entity": "product", "data": {"id": "P1", "name": "Item", "price": 99.99, "quantity": 1}}]
|
|
61
|
-
}'
|
|
337
|
+
```bash
|
|
338
|
+
walkeros push packages/cli/examples/flow-complete.json -f web -e '{"name":"order complete","data":{"id":"ORD-100","total":149.8,"currency":"EUR"},"nested":[{"entity":"product","data":{"id":"SKU-1","name":"Linen shirt","price":74.9,"quantity":2}}],"consent":{"functional":true}}' --simulate destination.ga4
|
|
62
339
|
```
|
|
63
340
|
|
|
64
|
-
|
|
341
|
+
- Output:
|
|
342
|
+
`call window.gtag("event","purchase",{... "transaction_id":"ORD-100","value":149.8,"currency":"EUR","items":[{"item_id":"SKU-1",...}],"send_to":"G-MAINSITE"})`,
|
|
343
|
+
with the globals flattened in by `include`.
|
|
344
|
+
- **MCP:** `package_get` (mapping hints), `flow_simulate`.
|
|
345
|
+
- **In GTM:** a GA4 event tag with an ecommerce variable per field.
|
|
346
|
+
- **Docs:** `getting-started/flow/step-examples`.
|
|
347
|
+
|
|
348
|
+
## consent-privacy: Consent and personal data
|
|
349
|
+
|
|
350
|
+
- **Purpose:** consent at the destination, the rule and the field; `policy`
|
|
351
|
+
before mapping; what happens to the email and the IP address.
|
|
352
|
+
- **Features:**
|
|
353
|
+
- `cmp-category-map` at
|
|
354
|
+
`/flows/web/sources/usercentrics/config/settings/categoryMap`: categoryMap
|
|
355
|
+
maps CMP categories onto exactly the consent keys the contract names.
|
|
356
|
+
- `cmp-explicit-only` at
|
|
357
|
+
`/flows/web/sources/usercentrics/config/settings/explicitOnly`: explicitOnly
|
|
358
|
+
publishes consent only after a real decision, so consent.functional present
|
|
359
|
+
means the user decided.
|
|
360
|
+
- `session-consent` at `/flows/web/sources/session/config/settings/consent`:
|
|
361
|
+
Storage for session ids waits for the functional consent key.
|
|
362
|
+
- `ga4-consent-map` at `/flows/server/transformers/ga4Consent`: A mapping hop
|
|
363
|
+
after the decoder copies GA4 analytics storage into functional and derives
|
|
364
|
+
the page language from the browser language (na when the hit has none);
|
|
365
|
+
refused pings stay functional false and fail the contract.
|
|
366
|
+
- `destination-consent` at `/flows/server/destinations/meta/config/consent`:
|
|
367
|
+
Destination consent: Meta only gets events with marketing consent.
|
|
368
|
+
- `rule-consent` at
|
|
369
|
+
`/flows/web/destinations/ga4/config/mapping/order/complete/consent`: Rule
|
|
370
|
+
consent on purchases: functional, like the destination; marketing consent
|
|
371
|
+
belongs to Meta and Data Manager.
|
|
372
|
+
- `value-consent` at
|
|
373
|
+
`/flows/web/destinations/collect/config/policy/user.email/consent`: Field
|
|
374
|
+
consent: the email is only kept with marketing consent.
|
|
375
|
+
- `destination-policy` at `/flows/web/destinations/ga4/config/policy`: policy
|
|
376
|
+
rewrites the event before mapping, here a currency fallback.
|
|
377
|
+
- `rule-policy` at
|
|
378
|
+
`/flows/server/destinations/meta/config/mapping/order/complete/policy`: Rule
|
|
379
|
+
policy sets the event id to the order id, so Meta can match it with the
|
|
380
|
+
browser pixel.
|
|
381
|
+
- `meta-hashing` at
|
|
382
|
+
`/flows/server/destinations/meta/config/settings/user_data/em`: Meta
|
|
383
|
+
normalizes and hashes em itself; the flow never handles a hash.
|
|
384
|
+
- `value-validate` at
|
|
385
|
+
`/flows/server/destinations/meta/config/settings/user_data/em/validate`:
|
|
386
|
+
validate drops a value that does not look like an email.
|
|
387
|
+
- `ingest-ip-raw` at `/flows/server/sources/express/config/ingest/map/ip`: The
|
|
388
|
+
raw ip stays in ingest: Meta needs it, fingerprint anonymizes it itself.
|
|
389
|
+
- `email-pseudonymize` at `/flows/server/transformers/pseudonymize`: Before
|
|
390
|
+
Pub/Sub the email becomes a stable keyed hash (HMAC-SHA256, EMAIL_SALT);
|
|
391
|
+
Meta and Data Manager keep the raw email and hash it themselves.
|
|
392
|
+
- `pubsub-before` at `/flows/server/destinations/pubsub/before`: A conditional
|
|
393
|
+
next on Pub/Sub runs pseudonymize only when an email exists; other events
|
|
394
|
+
pass untouched.
|
|
395
|
+
- `ga4-como` at `/flows/web/destinations/ga4/config/settings/como`: como turns
|
|
396
|
+
walker consent into Google Consent Mode calls.
|
|
397
|
+
- **Consent keys:** there are no collector consent defaults, only granted keys
|
|
398
|
+
travel. `consent.functional` is set only by Usercentrics with `explicitOnly`,
|
|
399
|
+
so its presence means the user decided.
|
|
400
|
+
- **Meta hashes `user_data` itself:** `em` is the raw email; the Meta package
|
|
401
|
+
normalizes and hashes it before sending, the flow never builds a hash for
|
|
402
|
+
Meta.
|
|
403
|
+
- **Email in the warehouse:** before Pub/Sub, the `pseudonymize` step
|
|
404
|
+
(`@walkeros/server-transformer-fingerprint`, HMAC-SHA256 with `EMAIL_SALT`, no
|
|
405
|
+
rotation) replaces `user.email` with a keyed hash, so the warehouse never
|
|
406
|
+
stores a raw email; Meta and Data Manager receive the raw email and hash it
|
|
407
|
+
themselves.
|
|
408
|
+
- **CLI:** the step-envelope line shows the consent skip; this one shows Meta
|
|
409
|
+
and Piwik PRO with request context from `--ingest`:
|
|
65
410
|
|
|
66
411
|
```bash
|
|
67
|
-
|
|
68
|
-
|
|
412
|
+
walkeros push packages/cli/examples/flow-complete.json -f server -e events/order-complete.json --ingest '{"ip":"203.0.113.7","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/128.0 Safari/537.36"}' --simulate destination.meta --simulate destination.piwikpro
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
- Output:
|
|
416
|
+
`call sendServer("https://graph.facebook.com/v22.0/1234567890/events", ... "event_id":"ORD-100" ... "client_ip_address":"203.0.113.7" ...)`
|
|
417
|
+
and the Piwik PRO hit; credentials never appear in the output.
|
|
418
|
+
- **MCP:** `flow_simulate`, `package_get` (`@walkeros/server-destination-meta`).
|
|
419
|
+
- **In GTM:** consent settings per tag plus Consent Mode.
|
|
420
|
+
- **Docs:** `guides/session`.
|
|
421
|
+
|
|
422
|
+
## gtm-fed: GTM stays, fed by walkerOS
|
|
423
|
+
|
|
424
|
+
- **Purpose:** keep the GTM container and feed it walkerOS events, without the
|
|
425
|
+
dataLayer source reading them back.
|
|
426
|
+
- **Features:**
|
|
427
|
+
- `gtm-mode` at `/flows/web/destinations/gtm/config/settings/gtm`: GTM stays:
|
|
428
|
+
the gtag destination in GTM mode pushes walkerOS events into its dataLayer.
|
|
429
|
+
- `destination-data` at `/flows/web/destinations/gtm/config/data`: Destination
|
|
430
|
+
data sends consent, globals, user and data with every GTM push.
|
|
431
|
+
- `gtm-names` at
|
|
432
|
+
`/flows/web/destinations/gtm/config/mapping/product/add/name`: GTM-bound
|
|
433
|
+
names start with elb, so the dataLayer source can skip them.
|
|
434
|
+
- `datalayer-filter` at `/flows/web/sources/dataLayer/config/settings/filter`:
|
|
435
|
+
The filter skips gtag arguments pushes and elb names: without it the page
|
|
436
|
+
loops.
|
|
437
|
+
- `gtm-echo-stop` at `/flows/web/destinations/gtm/before`: An event read from
|
|
438
|
+
the dataLayer never goes back to GTM.
|
|
439
|
+
- `load-script` at `/flows/web/destinations/ga4/config/loadScript`: loadScript
|
|
440
|
+
lets the destination load gtag.js itself.
|
|
441
|
+
- **Loop guard, two parts:** the dataLayer `filter` skips gtag `arguments`
|
|
442
|
+
pushes and every name starting with `elb `; the `gtm` destination's `before`
|
|
443
|
+
stops events whose `source.type` is `dataLayer`, so GTM never gets back what
|
|
444
|
+
it pushed.
|
|
445
|
+
- The `gtm` examples are shown in the browser (their simulation needs consent
|
|
446
|
+
seeding and is still waiting).
|
|
447
|
+
- **CLI:** none of its own; `walkeros validate ... --strict` checks the
|
|
448
|
+
destination schema.
|
|
449
|
+
- **MCP:** `package_get` (`@walkeros/web-destination-gtag`).
|
|
450
|
+
- **In GTM:** triggers on `elb product add` and `elb order complete`, variables
|
|
451
|
+
reading `consent`, `globals`, `user` and `data`.
|
|
452
|
+
- **Docs:** `getting-started/flow/routing`.
|
|
453
|
+
|
|
454
|
+
## references: Values from outside the file
|
|
455
|
+
|
|
456
|
+
- **Purpose:** every id, token, topic and dataset comes from a reference;
|
|
457
|
+
resolution order step over flow over root.
|
|
458
|
+
- **Features:**
|
|
459
|
+
- `root-variables` at `/variables`: Root variables are shared by all flows; a
|
|
460
|
+
flow or a step can override them.
|
|
461
|
+
- `flow-variables` at `/flows/server/variables`: Flow variables win over root
|
|
462
|
+
variables for one flow.
|
|
463
|
+
- `step-variables` at `/flows/server/destinations/piwikpro/variables`: Step
|
|
464
|
+
variables win over flow and root variables for one step.
|
|
465
|
+
- `ref-var-whole` at
|
|
466
|
+
`/flows/web/destinations/ga4/config/policy/data.currency/1/value`: A
|
|
467
|
+
whole-string $var keeps the variable type.
|
|
468
|
+
- `ref-var-deep` at
|
|
469
|
+
`/flows/server/destinations/pubsub/config/settings/projectId`:
|
|
470
|
+
$var.gcp.project reads a path inside a variable.
|
|
471
|
+
- `ref-var-inline` at
|
|
472
|
+
`/flows/warehouse/sources/pubsub/config/setup/deadLetterPolicy/deadLetterTopic`:
|
|
473
|
+
Inside a longer string $var is replaced in place, here in a full topic name.
|
|
474
|
+
- `ref-env` at `/flows/server/transformers/fingerprint/config/settings/salt`:
|
|
475
|
+
$env without a default must be set: a secret salt never gets a public
|
|
476
|
+
default.
|
|
477
|
+
- `ref-env-default` at `/variables/gcp/project`: $env.NAME:default works out
|
|
478
|
+
of the box and takes the environment value when set.
|
|
479
|
+
- `ref-secret` at `/flows/server/destinations/pubsub/config/credentials`:
|
|
480
|
+
$secret is read at start and never baked into an artifact; web flows reject
|
|
481
|
+
it.
|
|
482
|
+
- `ref-flow` at `/flows/web/destinations/collect/config/settings/url`:
|
|
483
|
+
$flow.server.url points the browser at the server flow; it is whole-string
|
|
484
|
+
only.
|
|
485
|
+
- `ref-code` at `/flows/web/destinations/collect/config/settings/transform`:
|
|
486
|
+
$code: puts a function into JSON.
|
|
487
|
+
- `ref-contract` at
|
|
488
|
+
`/flows/server/transformers/validate/config/settings/contract/0`:
|
|
489
|
+
$contract.server hands the resolved contract to the validate step.
|
|
490
|
+
- `ref-store` at `/flows/server/transformers/file/env/store`: $store.assets
|
|
491
|
+
injects a store into a step.
|
|
492
|
+
- `bundle-env` at `/flows/web/config/bundle/env`: Web $env is resolved at
|
|
493
|
+
build time from this declared set, so every build is the same.
|
|
494
|
+
- **Kinds:** `$var` (whole, deep, inline), `$env.NAME` and `$env.NAME:default`,
|
|
495
|
+
`$secret`, `$flow`, `$code:`, `$contract`, `$store`.
|
|
496
|
+
- **Secrets:** `$secret` is used only for `GCP_SA`, on the server and in the
|
|
497
|
+
warehouse; web flows reject `$secret`.
|
|
498
|
+
- **CLI:** `runneros` reads a private env file:
|
|
69
499
|
|
|
70
|
-
|
|
71
|
-
|
|
500
|
+
```bash
|
|
501
|
+
runneros start dist/server.mjs -p 8080 --env-file .env
|
|
72
502
|
```
|
|
73
503
|
|
|
74
|
-
|
|
504
|
+
- **MCP:** `flow_validate` (unresolved inline references are named).
|
|
505
|
+
- **In GTM:** constant and lookup table variables, per workspace.
|
|
506
|
+
- **Docs:** `guides/reference-syntax`.
|
|
507
|
+
|
|
508
|
+
## Part III: build the pipeline
|
|
509
|
+
|
|
510
|
+
## chains-routing: Chains and routes
|
|
511
|
+
|
|
512
|
+
- **Purpose:** decide per event which steps run: `source.before` on the raw
|
|
513
|
+
request, `source.next`, `collector.next` once per event, `destination.before`
|
|
514
|
+
per destination (London prompt 3).
|
|
515
|
+
- **Features:**
|
|
516
|
+
- `web-collector-next` at `/flows/web/collector/next`: On the web
|
|
517
|
+
collector.next runs the inline pageGroup step once per event.
|
|
518
|
+
- `ga4-other-dropped` at `/flows/server/sources/express/before/one/2`: GA4
|
|
519
|
+
hits for any other property stop at the route instead of reaching the
|
|
520
|
+
collector undecoded.
|
|
521
|
+
- `source-before` at `/flows/server/sources/express/before`: source.before
|
|
522
|
+
runs on the raw request, before an event exists.
|
|
523
|
+
- `route-one` at `/flows/server/sources/express/before/one`: one takes the
|
|
524
|
+
first matching branch; no match falls through.
|
|
525
|
+
- `match-and` at `/flows/server/sources/express/before/one/0/match/and`: and
|
|
526
|
+
needs every condition.
|
|
527
|
+
- `op-prefix` at
|
|
528
|
+
`/flows/server/sources/express/before/one/0/match/and/0/operator`: prefix
|
|
529
|
+
matches the start of a value.
|
|
530
|
+
- `op-regex` at
|
|
531
|
+
`/flows/server/sources/express/before/one/0/match/and/1/operator`: regex
|
|
532
|
+
matches a pattern: only hits of the sub-site property are decoded.
|
|
533
|
+
- `op-suffix` at `/flows/server/sources/express/before/one/1/match/operator`:
|
|
534
|
+
suffix matches the end of a value: script requests end in .js.
|
|
535
|
+
- `source-next` at `/flows/server/sources/express/next`: source.next names the
|
|
536
|
+
hop after the source, here dedup.
|
|
537
|
+
- `collector-next` at `/flows/server/collector/next`: collector.next runs once
|
|
538
|
+
per event before the fan-out: fingerprint and bot for all, enrich for all
|
|
539
|
+
but impressions.
|
|
540
|
+
- `route-next-match` at `/flows/server/collector/next/2`: A conditional next
|
|
541
|
+
(match plus next) runs the target only when the match holds; otherwise the
|
|
542
|
+
chain goes on.
|
|
543
|
+
- `match-not` at `/flows/server/collector/next/2/match/not`: not inverts a
|
|
544
|
+
condition: everything that is not an impression.
|
|
545
|
+
- `code-free-hop` at `/flows/server/transformers/enrich`: A step with only
|
|
546
|
+
next is a named chain other routes can call.
|
|
547
|
+
- `route-sequence` at `/flows/server/transformers/enrich/next`: A list mixing
|
|
548
|
+
conditional next entries and step ids is a sequence: each match decides in
|
|
549
|
+
turn, validate always runs last.
|
|
550
|
+
- `transformer-next` at `/flows/server/transformers/eventFilter/next`: A
|
|
551
|
+
transformer next continues the chain after the step.
|
|
552
|
+
- `op-exists` at `/flows/server/transformers/enrich/next/0/match/operator`:
|
|
553
|
+
exists checks that a path has a value: only logged-in events load the
|
|
554
|
+
customer.
|
|
555
|
+
- `op-eq` at `/flows/server/transformers/enrich/next/1/match/operator`: eq
|
|
556
|
+
compares as strings.
|
|
557
|
+
- `destination-before` at `/flows/server/destinations/meta/before`:
|
|
558
|
+
destination.before runs for one destination only; a stop there skips just
|
|
559
|
+
that one.
|
|
560
|
+
- `event-filter` at `/flows/server/transformers/eventFilter`: eventFilter
|
|
561
|
+
keeps impressions, likely bots and invalid events from Meta, Piwik PRO and
|
|
562
|
+
Data Manager; Pub/Sub keeps everything.
|
|
563
|
+
- `route-stop` at `/flows/server/transformers/eventFilter/next/stop`: stop
|
|
564
|
+
with a match drops the event at that point.
|
|
565
|
+
- `match-or` at `/flows/server/transformers/eventFilter/next/match/or`: or
|
|
566
|
+
needs any condition.
|
|
567
|
+
- `event-filter-impression` at
|
|
568
|
+
`/flows/server/transformers/eventFilter/next/match/or/0`: Impressions stay
|
|
569
|
+
out of Meta and Piwik PRO but reach the warehouse.
|
|
570
|
+
- `op-gt` at
|
|
571
|
+
`/flows/server/transformers/eventFilter/next/match/or/1/operator`: gt
|
|
572
|
+
compares numbers.
|
|
573
|
+
- `stop-unconditional` at `/flows/server/transformers/file/next`: An
|
|
574
|
+
unconditional stop: a script request never becomes an event.
|
|
575
|
+
- **Words:** a conditional next is `{ match, next }`; `one` takes the first
|
|
576
|
+
matching branch; `stop` ends the chain (with a `match`, only when it holds).
|
|
577
|
+
- **Not used here, with a pointer:** `many`, `destination.next` and
|
|
578
|
+
`transformer.before` have no genuine use in this scenario; see `routeCases` in
|
|
579
|
+
`@walkeros/core/dev` (`core/src/examples/route-cases.ts`).
|
|
580
|
+
- **CLI:** the collector chain for an impression (fingerprint and bot run,
|
|
581
|
+
`enrich` does not), and `eventFilter` alone (no events come out):
|
|
75
582
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
#### Mapping - Value Extraction
|
|
81
|
-
|
|
82
|
-
| Feature | Location | Example |
|
|
83
|
-
| ----------------- | ----------------- | ------------------------------------------------------------ |
|
|
84
|
-
| Key extraction | GA4 page_view | `"page_title": "data.title"` |
|
|
85
|
-
| Static value | Meta ViewContent | `"content_type": { "value": "product" }` |
|
|
86
|
-
| Key with fallback | GA4 add_to_cart | `{ "key": "data.currency", "value": "$variables.currency" }` |
|
|
87
|
-
| Nested key (deep) | dataLayer mapping | `"items.0.item_id"` |
|
|
88
|
-
| Fallback array | GA4 view_item | `[{ "key": "data.sku" }, { "key": "data.id" }]` |
|
|
89
|
-
|
|
90
|
-
#### Mapping - Structure
|
|
583
|
+
```bash
|
|
584
|
+
walkeros push packages/cli/examples/flow-complete.json -f server -e '{"name":"product impression","trigger":"impression","data":{"id":"SKU-1"}}' --simulate collector.default
|
|
585
|
+
walkeros push packages/cli/examples/flow-complete.json -f server -e '{"name":"product impression","trigger":"impression","data":{"id":"SKU-1"}}' --simulate transformer.eventFilter
|
|
586
|
+
```
|
|
91
587
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
588
|
+
- **MCP:** `flow_simulate`, `flow_validate`.
|
|
589
|
+
- **In GTM:** trigger conditions and exceptions per tag.
|
|
590
|
+
- **Docs:** `getting-started/flow/routing`.
|
|
591
|
+
|
|
592
|
+
## contract: The contract and the validate step
|
|
593
|
+
|
|
594
|
+
- **Purpose:** write down what every event must carry, check it at runtime, and
|
|
595
|
+
keep invalid events from the vendors (London prompt 4).
|
|
596
|
+
- **Features:**
|
|
597
|
+
- `contract-default` at `/contract/default`: The contract names what every
|
|
598
|
+
event must carry; the validate step and validate --strict read it.
|
|
599
|
+
- `contract-extend` at `/contract/server/extend`: server extends default with
|
|
600
|
+
what the server adds: the visitor hash.
|
|
601
|
+
- `contract-server-hash` at
|
|
602
|
+
`/contract/server/schema/properties/user/properties/hash`: The description
|
|
603
|
+
tells analysts what user.hash is and how to count it.
|
|
604
|
+
- `contract-tagging` at `/contract/default/tagging`: tagging is the contract
|
|
605
|
+
version the tagging follows.
|
|
606
|
+
- `contract-description` at `/contract/default/description`: description says
|
|
607
|
+
what the contract is for.
|
|
608
|
+
- `contract-schema` at `/contract/default/schema`: schema applies to every
|
|
609
|
+
event: functional consent is a hard gate, language a required global.
|
|
610
|
+
- `contract-events` at `/contract/default/events`: events hold one JSON Schema
|
|
611
|
+
per entity and action.
|
|
612
|
+
- `contract-wildcard` at `/contract/default/events/product/*`: product \*
|
|
613
|
+
applies to every product action.
|
|
614
|
+
- `validate-transformer` at `/flows/server/transformers/validate`: validate
|
|
615
|
+
checks each event against $contract.server at runtime.
|
|
616
|
+
- `validate-pass` at
|
|
617
|
+
`/flows/server/transformers/validate/config/settings/mode`: mode pass marks
|
|
618
|
+
source.valid instead of dropping, so the warehouse keeps violations; strict
|
|
619
|
+
is the hard gate.
|
|
620
|
+
- `event-filter-valid` at
|
|
621
|
+
`/flows/server/transformers/eventFilter/next/match/or/2`: Only events that
|
|
622
|
+
match the contract reach the vendors.
|
|
623
|
+
- **Gate semantics:** `validate` runs in `mode: "pass"`: it marks `source.valid`
|
|
624
|
+
and never drops, so the warehouse keeps violations. The `eventFilter` clause
|
|
625
|
+
`event.source.valid eq false` stops invalid events before Meta, Piwik PRO and
|
|
626
|
+
Data Manager only. `mode: "strict"` is the hard-gate variant: it drops invalid
|
|
627
|
+
events for every destination, the warehouse included.
|
|
628
|
+
- **CLI:** `validate` on the flow already checks the contract (every `extend`
|
|
629
|
+
resolved, paths under `contract.`); `-t contract` on the flow file runs that
|
|
630
|
+
contract check alone. Then run the validate step on an order without a visitor
|
|
631
|
+
hash (out: `source.valid: false`):
|
|
101
632
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
| Consent-gated field | API destination | `{ "key": "user.email", "consent": { "marketing": true } }` |
|
|
107
|
-
| Ignore rule | GA4/API test events | `{ "ignore": true }` |
|
|
108
|
-
| Wildcard action (\*) | GA4 test, Meta | `"test": { "*": { "ignore": true } }` |
|
|
109
|
-
| Wildcard entity (\*) | Meta click handler | `"*": { "click": { "name": "CustomEvent" } }` |
|
|
110
|
-
|
|
111
|
-
#### Variables
|
|
112
|
-
|
|
113
|
-
| Feature | Location | Example |
|
|
114
|
-
| -------------------- | ---------------- | ---------------------------------------------- |
|
|
115
|
-
| Root-level variables | Root | `"currency": "EUR"` |
|
|
116
|
-
| Flow-level variables | server.variables | `"metaPixelId": "$env.META_PIXEL_ID:..."` |
|
|
117
|
-
| Environment variable | Variables | `"$env.GA4_MEASUREMENT_ID:G-DEMO123456"` |
|
|
118
|
-
| Env with default | Variables | `"$env.API_URL:http://localhost:8080/collect"` |
|
|
119
|
-
| $var reference | GA4 settings | `"$var.ga4MeasurementId"` |
|
|
633
|
+
```bash
|
|
634
|
+
walkeros validate packages/cli/examples/flow-complete.json -t contract
|
|
635
|
+
walkeros push packages/cli/examples/flow-complete.json -f server -e '{"name":"order complete","data":{"id":"ORD-100","total":149.8,"currency":"EUR"},"globals":{"language":"en"},"consent":{"functional":true}}' --simulate transformer.validate
|
|
636
|
+
```
|
|
120
637
|
|
|
121
|
-
|
|
638
|
+
- **MCP:** `flow_validate`, `flow_simulate`.
|
|
639
|
+
- **In GTM:** no equivalent; a custom template per tag at best.
|
|
640
|
+
- **Docs:** `getting-started/flow/contract`, `transformers/validate`.
|
|
641
|
+
|
|
642
|
+
## quality: Duplicates, bots and a cookieless id
|
|
643
|
+
|
|
644
|
+
- **Purpose:** drop transport resends, send each order to Meta once (London
|
|
645
|
+
prompt 5), score bots, and give Piwik PRO a cookieless visitor id.
|
|
646
|
+
- **Features:**
|
|
647
|
+
- `dedup-by-event-id` at `/flows/server/transformers/dedup/cache`: A resend
|
|
648
|
+
with the same event id is stopped for an hour.
|
|
649
|
+
- `cache-stop` at `/flows/server/transformers/dedup/cache/stop`: stop: true
|
|
650
|
+
turns a cache hit into a drop.
|
|
651
|
+
- `cache-namespace` at `/flows/server/transformers/dedup/cache/namespace`:
|
|
652
|
+
namespace keeps cache keys of different steps apart.
|
|
653
|
+
- `cache-rules` at `/flows/server/transformers/dedup/cache/rules`: rules say
|
|
654
|
+
which events are cached, by which key, for how long.
|
|
655
|
+
- `cache-key` at `/flows/server/transformers/dedup/cache/rules/0/key`: key is
|
|
656
|
+
a list of paths; together they identify a duplicate.
|
|
657
|
+
- `cache-ttl` at `/flows/server/transformers/dedup/cache/rules/0/ttl`: ttl in
|
|
658
|
+
seconds.
|
|
659
|
+
- `order-dedup-meta` at `/flows/server/destinations/meta/cache`: A thank-you
|
|
660
|
+
page reload gets a new event id for the same order; Meta gets each order
|
|
661
|
+
once.
|
|
662
|
+
- `cache-rule-match` at `/flows/server/destinations/meta/cache/rules/0/match`:
|
|
663
|
+
A rule match limits the cache to order complete.
|
|
664
|
+
- `bot-score` at `/flows/server/transformers/bot`: bot annotates user.botScore
|
|
665
|
+
and botCategory from the request; it never drops.
|
|
666
|
+
- `event-filter-bot` at
|
|
667
|
+
`/flows/server/transformers/eventFilter/next/match/or/1`: A botScore above
|
|
668
|
+
50 keeps the event from the vendors.
|
|
669
|
+
- `fingerprint` at `/flows/server/transformers/fingerprint`: A cookieless
|
|
670
|
+
visitor id rotating daily, for Piwik PRO; it is not hashing of personal
|
|
671
|
+
data.
|
|
672
|
+
- `piwik-visitor-id` at
|
|
673
|
+
`/flows/server/destinations/piwikpro/config/settings/visitorId`: Device,
|
|
674
|
+
then session, then the cookieless hash: the first one present is the visitor
|
|
675
|
+
id.
|
|
676
|
+
- `piwikpro` at `/flows/server/destinations/piwikpro`: Piwik PRO from the
|
|
677
|
+
server, EU hosted, with the cookieless id as fallback.
|
|
678
|
+
- **Fingerprint is a cookieless visitor id for Piwik PRO:** a daily rotating
|
|
679
|
+
HMAC over the anonymized IP, browser and site; it is not hashing of personal
|
|
680
|
+
data.
|
|
681
|
+
- **CLI:** fingerprint with request context, and the collector chain with the
|
|
682
|
+
bot result mocked as a likely bot:
|
|
122
683
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
| Source-level mapping | dataLayer | `"mapping": { "gtag": { "add_to_cart": {...} } }` |
|
|
128
|
-
| Pre-collector chain | dataLayer | `"next": "enricher"` |
|
|
129
|
-
| Pre-collector array | http (server) | `"next": ["filter"]` (array form) |
|
|
130
|
-
| Require (deferred init) | dataLayer | `"require": ["session"]` |
|
|
131
|
-
| Demo source events | demo | Pre-configured test events |
|
|
684
|
+
```bash
|
|
685
|
+
walkeros push packages/cli/examples/flow-complete.json -f server -e '{"name":"page view","data":{"title":"Home"}}' --ingest '{"ip":"203.0.113.7","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/128.0 Safari/537.36","origin":"https://www.example.com"}' --simulate transformer.fingerprint
|
|
686
|
+
walkeros push packages/cli/examples/flow-complete.json -f server -e '{"name":"page view","data":{"title":"Home"}}' --simulate collector.default --mock collector.next.bot='{"name":"page view","data":{"title":"Home"},"user":{"botScore":80,"botCategory":"automation"}}'
|
|
687
|
+
```
|
|
132
688
|
|
|
133
|
-
|
|
689
|
+
- **MCP:** `flow_simulate`, `package_get` (`@walkeros/server-transformer-bot`,
|
|
690
|
+
`@walkeros/server-transformer-fingerprint`).
|
|
691
|
+
- **In GTM:** a custom template with a Firestore lookup for order dedup; no
|
|
692
|
+
built-in bot score.
|
|
693
|
+
- **Docs:** `collector/cache`, `transformers/bot`, `transformers/fingerprint`.
|
|
694
|
+
|
|
695
|
+
## state-stores: State, stores and first-party walker.js
|
|
696
|
+
|
|
697
|
+
- **Purpose:** remember across requests, read from stores, and serve the web
|
|
698
|
+
bundle from the server.
|
|
699
|
+
- **Features:**
|
|
700
|
+
- `flow-stores` at `/flows/server/stores`: Stores hold data the steps read:
|
|
701
|
+
script files and customer records.
|
|
702
|
+
- `state-set` at `/flows/server/transformers/sessionSave/state`: state set
|
|
703
|
+
remembers the gclid and start time of a session in the built-in cache.
|
|
704
|
+
- `state-get` at `/flows/server/transformers/sessionLoad/state`: state get
|
|
705
|
+
restores it on the order, per instance.
|
|
706
|
+
- `state-key` at `/flows/server/transformers/sessionSave/state/key`: The key
|
|
707
|
+
falls back from the session id to the visitor hash.
|
|
708
|
+
- `state-value` at `/flows/server/transformers/sessionSave/state/value`: value
|
|
709
|
+
maps what is stored.
|
|
710
|
+
- `state-store` at `/flows/server/transformers/loadUser/state/store`: store
|
|
711
|
+
reads from a named store instead of the built-in cache.
|
|
712
|
+
- `state-mapping` at `/flows/server/transformers/loadUser/state/mapping`:
|
|
713
|
+
mapping picks fields from the stored row and merges them into the target.
|
|
714
|
+
- `load-user` at `/flows/server/transformers/loadUser`: loadUser puts the
|
|
715
|
+
customer lifetime value and segment on logged-in events.
|
|
716
|
+
- `store-fs` at `/flows/server/stores/assets`: An fs store serves files from a
|
|
717
|
+
folder.
|
|
718
|
+
- `store-file` at `/flows/server/stores/assets/config/file`: file: true
|
|
719
|
+
returns bytes exactly as stored.
|
|
720
|
+
- `store-customers` at `/flows/server/stores/customers`: Customer records come
|
|
721
|
+
from an fs store with fake demo data; in production a Sheets or GCS store
|
|
722
|
+
takes its place, loadUser stays as it is.
|
|
723
|
+
- `store-cache` at `/flows/server/stores/customers/cache`: Store cache
|
|
724
|
+
memoizes customer reads for five minutes.
|
|
725
|
+
- `file-transformer` at `/flows/server/transformers/file`: walker.js is served
|
|
726
|
+
first party from the server flow.
|
|
727
|
+
- `file-headers` at `/flows/server/transformers/file/config/settings/headers`:
|
|
728
|
+
Default response headers for served files: caching and resource policy.
|
|
729
|
+
- `gclid-to-ads` at
|
|
730
|
+
`/flows/server/destinations/datamanager/config/mapping/order/complete/data/map/gclid`:
|
|
731
|
+
The gclid saved at session start reaches Google Ads with the purchase.
|
|
732
|
+
- **Built-in `__cache`:** `sessionSave` and `sessionLoad` omit `store`, so they
|
|
733
|
+
use the LRU cache every collector creates. It is per instance: a second
|
|
734
|
+
instance does not see it; a shared store is a later build.
|
|
735
|
+
- **`$store` and store `cache`:** `$store.assets` injects the fs store into
|
|
736
|
+
`file`; the `customers` store memoizes customer reads for five minutes.
|
|
737
|
+
- **Customers:** `customers` is an fs store over fake demo data
|
|
738
|
+
(`packages/cli/examples/customers`, one JSON record per customer id), outside
|
|
739
|
+
root `include`, so no customer data ships in a bundle. `basePath` is relative
|
|
740
|
+
to the working directory, so `CUSTOMERS_DIR` holds it as an absolute path (the
|
|
741
|
+
setup block at the top exports it). In production a Sheets or GCS store takes
|
|
742
|
+
its place; `loadUser` does not change.
|
|
743
|
+
- **walker.js first party:** the `file` transformer answers `GET /walker.js`
|
|
744
|
+
from the `assets` store with `settings.headers` (`Cache-Control`,
|
|
745
|
+
`Cross-Origin-Resource-Policy`) and ends with `next: { stop: true }`. Express
|
|
746
|
+
`settings.cors` adds the CORS headers.
|
|
747
|
+
- **Why `X-Content-Type-Options: nosniff` is not in the file:** express sets it
|
|
748
|
+
on every response itself.
|
|
749
|
+
- The `file` example is proven over HTTP in the integration test; its simulation
|
|
750
|
+
is still waiting.
|
|
751
|
+
- **CLI:** a known customer gets its lifetime value and segment
|
|
752
|
+
(`user.ltv: 420`, `user.segment: "loyal"`):
|
|
753
|
+
|
|
754
|
+
```bash
|
|
755
|
+
walkeros push packages/cli/examples/flow-complete.json -f server -e '{"name":"order complete","data":{"id":"ORD-100","total":149.8,"currency":"EUR"},"user":{"id":"cust-42"}}' --simulate transformer.loadUser
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
- Build the web bundle straight into the shared folder (web builds ignore root
|
|
759
|
+
`include`):
|
|
760
|
+
|
|
761
|
+
```bash
|
|
762
|
+
walkeros bundle packages/cli/examples/flow-complete.json -f web -o packages/cli/examples/shared/walker.js
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
- **MCP:** `flow_bundle`, `package_get` (`@walkeros/server-store-fs`; for the
|
|
766
|
+
production swap `@walkeros/server-store-sheets`).
|
|
767
|
+
- **In GTM:** a server container can serve `gtm.js` first party; there is no
|
|
768
|
+
general store.
|
|
769
|
+
- **Docs:** `collector/state`, `collector/cache`, `transformers/file`.
|
|
770
|
+
|
|
771
|
+
## Part IV: ship and operate
|
|
772
|
+
|
|
773
|
+
## delivery: Reliable delivery and the warehouse
|
|
774
|
+
|
|
775
|
+
- **Purpose:** every event raw into Pub/Sub, a warehouse flow into BigQuery,
|
|
776
|
+
vendors with breakers and timeouts.
|
|
777
|
+
- **Features:**
|
|
778
|
+
- `collect-api` at `/flows/web/destinations/collect`: collect sends every
|
|
779
|
+
event to the first-party server.
|
|
780
|
+
- `collect-transform` at
|
|
781
|
+
`/flows/web/destinations/collect/config/settings/transform`: transform trims
|
|
782
|
+
the payload before it leaves the browser.
|
|
783
|
+
- `pubsub-raw` at `/flows/server/destinations/pubsub`: Pub/Sub gets every
|
|
784
|
+
event raw, impressions, bots and violations included.
|
|
785
|
+
- `destination-setup` at `/flows/server/destinations/pubsub/config/setup`:
|
|
786
|
+
setup provisions the topic, EU storage regions included.
|
|
787
|
+
- `destination-credentials` at
|
|
788
|
+
`/flows/server/destinations/datamanager/config/credentials`:
|
|
789
|
+
config.credentials takes the service account from $secret.
|
|
790
|
+
- `breaker` at `/flows/server/destinations/pubsub/config/breaker`: breaker
|
|
791
|
+
pauses a failing destination instead of hammering it.
|
|
792
|
+
- `timeout` at `/flows/server/destinations/pubsub/config/timeout`: timeout
|
|
793
|
+
bounds each push.
|
|
794
|
+
- `dlq-max` at `/flows/server/destinations/pubsub/config/dlqMax`: dlqMax
|
|
795
|
+
bounds the dead letter queue of failed events.
|
|
796
|
+
- `batch` at `/flows/warehouse/destinations/bigquery/config/batch`: batch
|
|
797
|
+
buffers rows by size and age.
|
|
798
|
+
- `web-batch` at `/flows/web/destinations/collect/config/batch`: The browser
|
|
799
|
+
sends events in batches.
|
|
800
|
+
- `rule-batch` at
|
|
801
|
+
`/flows/web/destinations/collect/config/mapping/product/impression/batch`:
|
|
802
|
+
Impressions get their own, bigger buffer.
|
|
803
|
+
- `queue-backfill` at `/flows/web/destinations/gtm/config/queue`: queue
|
|
804
|
+
backfills events from before the destination started, not a retry queue.
|
|
805
|
+
- `destination-require` at `/flows/web/destinations/gtm/config/require`:
|
|
806
|
+
require starts GTM only after the consent decision.
|
|
807
|
+
- `meta-capi` at `/flows/server/destinations/meta`: Meta Conversions API from
|
|
808
|
+
the server for add to cart and purchase.
|
|
809
|
+
- `datamanager` at `/flows/server/destinations/datamanager`: Google Ads
|
|
810
|
+
conversions through the Data Manager API.
|
|
811
|
+
- `warehouse-flow` at `/flows/warehouse`: A separate flow reads the topic into
|
|
812
|
+
BigQuery; the warehouse keeps everything.
|
|
813
|
+
- `pubsub-pull` at `/flows/warehouse/sources/pubsub`: The pull source reads
|
|
814
|
+
the subscription and acks after the push.
|
|
815
|
+
- `source-setup` at `/flows/warehouse/sources/pubsub/config/setup`: Source
|
|
816
|
+
setup provisions the subscription with a dead letter topic.
|
|
817
|
+
- `source-credentials` at
|
|
818
|
+
`/flows/warehouse/sources/pubsub/config/credentials`: Sources take
|
|
819
|
+
credentials too.
|
|
820
|
+
- `bigquery` at `/flows/warehouse/destinations/bigquery`: Every event becomes
|
|
821
|
+
one BigQuery row.
|
|
822
|
+
- **`queue` is backfill, not retry:** on `gtm`, events from before the consent
|
|
823
|
+
decision are delivered once `require` is met.
|
|
824
|
+
- **`dlqMax`** bounds the dead letter queue of events that failed; `breaker`
|
|
825
|
+
pauses a failing destination; `timeout` bounds each push.
|
|
826
|
+
- **The warehouse keeps everything:** bots, impressions and contract violations
|
|
827
|
+
land in BigQuery; filtering is a vendor decision.
|
|
828
|
+
- **CLI:** Pub/Sub and BigQuery on their mocks, then provisioning (needs GCP,
|
|
829
|
+
not run for this guide):
|
|
830
|
+
|
|
831
|
+
```bash
|
|
832
|
+
walkeros push packages/cli/examples/flow-complete.json -f server -e events/order-complete.json --simulate destination.pubsub
|
|
833
|
+
walkeros push packages/cli/examples/flow-complete.json -f warehouse -e '{"name":"page view","data":{"title":"Home"},"source":{"type":"express"}}' --simulate destination.bigquery
|
|
834
|
+
walkeros setup destination.pubsub -c packages/cli/examples/flow-complete.json -f server
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
- Output: `call PubSub.topic.publishMessage(...)` with the event, and
|
|
838
|
+
`call JSONWriter.appendRows(...)` with one row.
|
|
839
|
+
- **MCP:** `flow_simulate`, `package_get` (`@walkeros/server-destination-gcp`).
|
|
840
|
+
- **In GTM:** a server container with a BigQuery or Pub/Sub custom tag.
|
|
841
|
+
- **Docs:** `getting-started/flow/index`.
|
|
842
|
+
|
|
843
|
+
## build-run: Build and run
|
|
844
|
+
|
|
845
|
+
- **Purpose:** turn the file into artifacts: one `walker.js`, one server
|
|
846
|
+
artifact per server flow.
|
|
847
|
+
- **Features:**
|
|
848
|
+
- `bundle-packages` at `/flows/server/config/bundle/packages`: bundle.packages
|
|
849
|
+
lists what a flow is built from.
|
|
850
|
+
- `package-version` at
|
|
851
|
+
`/flows/server/config/bundle/packages/@walkeros~1collector/version`:
|
|
852
|
+
validate --strict warns for a listed package without a version; an unlisted
|
|
853
|
+
step package is added as latest.
|
|
854
|
+
- `root-include` at `/include`: include ships the shared folder, walker.js
|
|
855
|
+
included, with the server artifact.
|
|
856
|
+
- **Package versions:** every `@walkeros/*` package releases at one version, and
|
|
857
|
+
this file pins all of them to the version it ships with. Exact is recommended.
|
|
858
|
+
|
|
859
|
+
| `version` | Resolves to |
|
|
860
|
+
| ---------------------- | -------------------------------------------------------------- |
|
|
861
|
+
| omitted | `latest`; `validate --strict` warns |
|
|
862
|
+
| `latest` or a dist-tag | the version the tag points to at build time, re-resolved daily |
|
|
863
|
+
| a range (`^4.6.0`) | the highest published version in the range, re-resolved daily |
|
|
864
|
+
| exact (`4.6.1`) | that version, cached for good |
|
|
865
|
+
| `path` instead | a local directory, copied from disk, no registry |
|
|
866
|
+
|
|
867
|
+
- `--release` stamps the id on every event as `source.release`; there is no
|
|
868
|
+
release variable in the file.
|
|
869
|
+
- Root `include` ships `shared/` (with `walker.js`) next to every artifact.
|
|
870
|
+
- **CLI:**
|
|
871
|
+
|
|
872
|
+
```bash
|
|
873
|
+
walkeros bundle packages/cli/examples/flow-complete.json -f web --stats
|
|
874
|
+
walkeros bundle packages/cli/examples/flow-complete.json --all -o dist/all
|
|
875
|
+
walkeros bundle packages/cli/examples/flow-complete.json -f server -o dist/server.mjs --release 2026-09-26
|
|
876
|
+
runneros start dist/server.mjs -p 8080 --env-file .env
|
|
877
|
+
walkeros cache info
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
- `--all` needs `-o`: it writes `dist/all/web/walker.js`,
|
|
881
|
+
`dist/all/server/flow.mjs` and `dist/all/warehouse/flow.mjs`.
|
|
882
|
+
- `runneros` (`@walkeros/runner`) runs a prebuilt artifact; the same artifact
|
|
883
|
+
runs in Docker.
|
|
884
|
+
- **MCP:** `flow_bundle`.
|
|
885
|
+
- **In GTM:** publishing a container version.
|
|
886
|
+
- **Docs:** `getting-started/flow/index`.
|
|
887
|
+
|
|
888
|
+
## operate: Observe, CI and deploy
|
|
889
|
+
|
|
890
|
+
- **Purpose:** watch live events, gate changes in CI, deploy through the app.
|
|
891
|
+
- **Features:**
|
|
892
|
+
- `observe-public` at `/flows/web/config/observe`: A web flow reports to
|
|
893
|
+
Observe through a public url and binding.
|
|
894
|
+
- `observe-level` at `/flows/server/config/observe/level`: level standard
|
|
895
|
+
reports step outcomes; trace adds payloads.
|
|
896
|
+
- `observe-sample` at `/flows/server/config/observe/sample`: sample reports
|
|
897
|
+
one event in ten.
|
|
898
|
+
- `collector-logger` at `/flows/server/collector/logger`: The log level comes
|
|
899
|
+
from the flow variable, so LOG_LEVEL changes it per environment.
|
|
900
|
+
- **CI:** `walkeros validate packages/cli/examples/flow-complete.json --strict`
|
|
901
|
+
plus the step examples as fixtures.
|
|
902
|
+
- **CLI (app-bound, need a login; listed, not run for this guide):**
|
|
903
|
+
|
|
904
|
+
```bash
|
|
905
|
+
walkeros flows create flow-complete -c packages/cli/examples/flow-complete.json
|
|
906
|
+
walkeros deploy create packages/cli/examples/flow-complete.json
|
|
907
|
+
walkeros deploy start <flowId>
|
|
908
|
+
walkeros observe start <flowId>
|
|
909
|
+
```
|
|
134
910
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
| Pre-collector chain | dataLayer | `"next": "enricher"` |
|
|
140
|
-
|
|
141
|
-
#### Destinations
|
|
142
|
-
|
|
143
|
-
| Feature | Location | Example |
|
|
144
|
-
| ----------------------- | ---------------- | ------------------------------------------- |
|
|
145
|
-
| Require (deferred init) | GA4 | `"require": ["consent", "user"]` |
|
|
146
|
-
| Destination consent | GA4 | `"consent": { "marketing": true }` |
|
|
147
|
-
| Destination mapping | All destinations | Entity/action to vendor events |
|
|
148
|
-
| Multiple destinations | Both flows | GA4 + API, Meta + Demo |
|
|
149
|
-
| Batch option | API | `"batch": 5` |
|
|
150
|
-
| Transform function | API | `"transform": "$code:(data) => ..."` |
|
|
151
|
-
| Empty consent (always) | debug | `"consent": {}` (fires without consent) |
|
|
152
|
-
| Inline `validate:` | GA4 | Step-level schema check on `order complete` |
|
|
153
|
-
|
|
154
|
-
#### Collector
|
|
155
|
-
|
|
156
|
-
| Feature | Location | Example |
|
|
157
|
-
| ----------------- | ---------------- | --------------------------------------- |
|
|
158
|
-
| Tagging | Both collectors | `"tagging": 1` |
|
|
159
|
-
| Consent defaults | Both collectors | `"consent": { "functional": true }` |
|
|
160
|
-
| Globals | Both collectors | `"environment": "demo"` |
|
|
161
|
-
| Custom properties | web collector | `"custom": { "campaign": "flow-demo" }` |
|
|
162
|
-
| User defaults | web collector | `"user": { "id": "anonymous" }` |
|
|
163
|
-
| Dynamic global | server collector | `"startedAt": "$code:Date.now()"` |
|
|
164
|
-
|
|
165
|
-
#### Server-Specific
|
|
166
|
-
|
|
167
|
-
| Feature | Location | Example |
|
|
168
|
-
| --------------------- | --------------- | ----------------------------------------------------------------------- |
|
|
169
|
-
| Ingest metadata | http source | `"context.ip": "ip"` |
|
|
170
|
-
| Language header | ingest | `"context.language": "headers.accept-language"` |
|
|
171
|
-
| Ingest computed field | http source | `"fn": "$code:(ip) => ip.replace(...)"` |
|
|
172
|
-
| Policy | Meta | Pre-processing field transformation |
|
|
173
|
-
| Policy consent-gated | Meta | `"user_data.em"` with consent |
|
|
174
|
-
| Policy nested map | Meta | `"custom_data.request_meta": { "map": {...} }` |
|
|
175
|
-
| Local package path | server packages | `"path": "../../core"` (resolve from filesystem) |
|
|
176
|
-
| Source cache | http source | `"cache": { "rules": [...] }` (built-in tier) |
|
|
177
|
-
| Cache match rule | http source | `"match": { "key": "ingest.method", "operator": "eq", "value": "GET" }` |
|
|
178
|
-
| Cache TTL | http source | `"ttl": 300` (seconds) |
|
|
179
|
-
| Cache response update | http source | `"update": { "headers.X-Cache": { "key": "cache.status" } }` |
|
|
180
|
-
|
|
181
|
-
#### Browser Source
|
|
182
|
-
|
|
183
|
-
| Feature | Location | Example |
|
|
184
|
-
| --------------- | -------- | ---------------------------------- |
|
|
185
|
-
| Prefix | browser | `"prefix": "data-elb"` |
|
|
186
|
-
| Auto pageview | browser | `"pageview": true` |
|
|
187
|
-
| Session consent | browser | `"session": { "consent": {...} }` |
|
|
188
|
-
| ELB binding | browser | `"elb": "elb"`, `"elbLayer": true` |
|
|
189
|
-
|
|
190
|
-
---
|
|
191
|
-
|
|
192
|
-
### Features NOT Used (6)
|
|
193
|
-
|
|
194
|
-
#### Now Available via $code: Prefix ✅
|
|
195
|
-
|
|
196
|
-
These features are now fully supported in JSON via `$code:` prefix (and ARE used
|
|
197
|
-
in this example):
|
|
198
|
-
|
|
199
|
-
| Feature | Status |
|
|
200
|
-
| ----------------------- | --------------------------------- |
|
|
201
|
-
| `fn:` function | ✅ Used via `$code:` in GA4 value |
|
|
202
|
-
| `condition:` | ✅ Used via `$code:` in variables |
|
|
203
|
-
| Custom transformer code | ✅ Used in enricher, filter |
|
|
204
|
-
| Custom destination code | ✅ Used in debug logger |
|
|
205
|
-
|
|
206
|
-
#### Omitted for Clarity (6)
|
|
207
|
-
|
|
208
|
-
These features could be added but were omitted to keep the example focused:
|
|
209
|
-
|
|
210
|
-
| Feature | Why Omitted |
|
|
211
|
-
| ------------------------- | ----------------------------- |
|
|
212
|
-
| Multiple named flows (3+) | Two flows sufficient for demo |
|
|
213
|
-
| Queue config | Advanced batching scenario |
|
|
214
|
-
| Retry config | Advanced error handling |
|
|
215
|
-
| Custom fetch options | API destination advanced |
|
|
216
|
-
| Custom headers in API | Would add complexity |
|
|
217
|
-
|
|
218
|
-
---
|
|
219
|
-
|
|
220
|
-
## Data Flow Examples
|
|
221
|
-
|
|
222
|
-
### Order Complete (Server Flow)
|
|
223
|
-
|
|
224
|
-
1. **Ingest**: Request metadata extracted (IP, user-agent, referer) to
|
|
225
|
-
`context.*`
|
|
226
|
-
2. **Policy**: Pre-processes event:
|
|
227
|
-
- `user_data.em` from `user.email` (only if marketing consent)
|
|
228
|
-
- `user_data.external_id` from `user.id`
|
|
229
|
-
- `custom_data.server_processed` = `true`
|
|
230
|
-
- `custom_data.request_meta` = `{ ip, ua }` from context
|
|
231
|
-
3. **Fingerprint**: Hashes context fields to `user.hash`
|
|
232
|
-
4. **Mapping**: Transforms to Meta format:
|
|
233
|
-
- `"name": "Purchase"`
|
|
234
|
-
- `value`, `currency`, `order_id` extracted
|
|
235
|
-
- `contents` via `$ref` to definition loop
|
|
236
|
-
|
|
237
|
-
### Product Add (Web Flow)
|
|
238
|
-
|
|
239
|
-
1. **DataLayer Source**: Captures `add_to_cart` event
|
|
240
|
-
2. **Source Mapping**: Transforms to `product add` with walkerOS structure
|
|
241
|
-
3. **Enricher**: Adds enrichedAt timestamp to context
|
|
242
|
-
4. **Collector**: Adds globals, consent, user data
|
|
243
|
-
5. **GA4 Mapping**: Transforms to `add_to_cart` with items array
|
|
244
|
-
6. **API Destination**: Batches and sends to server (if batch size reached)
|
|
245
|
-
|
|
246
|
-
---
|
|
247
|
-
|
|
248
|
-
## Environment Variables
|
|
249
|
-
|
|
250
|
-
| Variable | Default | Description |
|
|
251
|
-
| -------------------- | ------------------------------- | -------------------------- |
|
|
252
|
-
| `GA4_MEASUREMENT_ID` | `G-DEMO123456` | Google Analytics 4 ID |
|
|
253
|
-
| `API_URL` | `http://localhost:8080/collect` | Server collection endpoint |
|
|
254
|
-
| `META_PIXEL_ID` | `123456789012345` | Meta Pixel ID |
|
|
255
|
-
| `META_ACCESS_TOKEN` | `demo_token` | Meta Conversions API token |
|
|
911
|
+
- **MCP:** `flow_manage`, `deploy_manage`, `observe_session`, `flow_validate`
|
|
912
|
+
then `flow_simulate` then `flow_push` as the agent loop.
|
|
913
|
+
- **In GTM:** preview mode and container versions.
|
|
914
|
+
- **Docs:** `collector/logger`.
|