Skip to content

Conversation

@alexanderkirtzel
Copy link
Collaborator

@alexanderkirtzel alexanderkirtzel commented Jan 26, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Snowplow web destination with ecommerce tracking, consent handling, and activity tracking support.
    • Introduced dynamic pattern syntax ($def, $var, $env, $code:) for flexible configuration management and inline JavaScript.
    • CLI now auto-imports collector when sources or destinations are configured.
  • Bug Fixes

    • Fixed CLI commands hanging after completion.
    • Fixed destination lifecycle to queue events until initialization completes.
  • Documentation

    • Updated configuration examples and guides to reflect new pattern syntax.

✏️ Tip: You can customize this high-level summary in your review settings.

@alexanderkirtzel alexanderkirtzel linked an issue Jan 26, 2026 that may be closed by this pull request
@vercel
Copy link

vercel bot commented Jan 26, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
demo-tagging Ready Ready Preview, Comment Jan 26, 2026 8:58am
storybook Ready Ready Preview, Comment Jan 26, 2026 8:58am
website Ready Ready Preview, Comment Jan 26, 2026 8:58am

@coderabbitai
Copy link

coderabbitai bot commented Jan 26, 2026

📝 Walkthrough

Walkthrough

This PR introduces unified dynamic pattern syntax ($var, $env, $def, $code) for flow configuration, refactors collector queue lifecycle to queue on() events until init completes, adds complete Snowplow destination integration with dual tracker modes, enhances CLI bundling with implicit collector injection and inline code serialization, updates core pattern resolution, and bumps package versions across the monorepo.

Changes

Cohort / File(s) Summary
Package Versions & Changelogs
apps/*/CHANGELOG.md, apps/*/package.json, packages/*/CHANGELOG.md, packages/*/package.json, package.json
Version bumps (1.0.1, 1.0.2, 1.1.0, 5.0.0, 2.0.0) and changelog entries across 30+ packages; added "packages/config" to root workspaces
Core Pattern Resolution
packages/core/src/flow.ts, packages/core/src/__tests__/flow.test.ts, packages/core/src/schemas/flow.ts, packages/core/src/types/flow.ts, packages/core/README.md, packages/core/CHANGELOG.md
New unified resolver supporting $def.name, $var.name, $env.NAME:default syntax; replaced legacy interpolateVariables and $ref logic; added comprehensive pattern resolution tests; updated documentation
Collector Queue Refactor
packages/collector/src/destination.ts, packages/collector/src/handle.ts, packages/collector/src/on.ts, packages/collector/src/__tests__/on-queue.test.ts, packages/collector/CHANGELOG.md
Renamed queue → queuePush, added queueOn for lifecycle events; queues on() events until init() completes; new callDestinationOn helper; lifecycle event delivery order guarantees
CLI Bundler Enhancements
packages/cli/src/commands/bundle/bundler.ts, packages/cli/src/commands/bundle/index.ts, packages/cli/src/__tests__/bundle/bundler.test.ts, packages/cli/examples/flow-complete.json, packages/cli/CHANGELOG.md
Auto-adds collector when sources/destinations present; new serializeWithCode for $code: inline JS; default import generation; process.exit() to prevent hangs; 369 new test lines covering code/collector behaviors
CLI Command Updates
packages/cli/src/commands/push/index.ts, packages/cli/src/commands/simulate/index.ts
Added process.exit(0) for successful runs; unified simulate output formatting; single exit strategy to prevent hanging
Snowplow Destination (New)
packages/web/destinations/snowplow/*
7,000+ lines: complete Snowplow integration with dual tracker modes (sp.js + browser-tracker), comprehensive examples (events, mappings, walkerOS events, env), extensive types/schemas, adapter pattern, consent handling, user privacy controls, README with 1,200+ lines, full test suite
Destination Type Updates
packages/core/src/types/destination.ts, packages/server/core/src/__tests__/*.test.ts
Replaced queue with queuePush + queueOn structure; updated Push payload type; test fixtures updated to use queuePush
Web Destination Updates
packages/web/destinations/gtag/src/index.ts, packages/web/destinations/*{/package.json,/CHANGELOG.md}
GTtag init simplification (direct window.gtag access); version bumps and dependency updates across api, meta, piwikpro, plausible, browser, dataLayer packages
Jest Config Updates
packages/config/jest/index.mjs, apps/quickstart/jest.config.mjs
Added modulePathIgnorePatterns for Haste collisions; refactored quickstart config to separate displayName/moduleNameMapper; removed testEnvironment jsdom from quickstart
Server Packages
packages/server/{core,destinations/*,sources/*,transformers/*}/package.json, packages/server/core/src/__tests__/*.test.ts
Version bumps to 1.0.1 across api/aws/datamanager/gcp/meta destinations; 1.0.1 for aws/express/fetch/gcp sources; 5.0.0 (fingerprint), 2.0.0 (validator) transformers; queuePush property rename in tests
Documentation Updates
website/docs/{getting-started/,destinations/,comparisons/}*.mdx
New Flow/Dynamic Patterns section with $var/$env/$def/$code examples; Snowplow destination docs (816 lines); updated CLI docs removing explicit imports; environment variable syntax updates (${VAR} → $env.NAME) across examples; new Snowplow comparison section

Sequence Diagrams

sequenceDiagram
    participant CLI as CLI Bundler
    participant Flow as Flow Config
    participant Resolver as Pattern Resolver
    participant Serializer as Code Serializer
    participant Imports as Import Generator

    CLI->>Flow: Load flow.json
    Flow->>Resolver: Detect $var, $env, $def, $code patterns
    Resolver->>Resolver: Recursively resolve patterns<br/>(defs → vars → env vars)
    Resolver-->>Serializer: Resolved values
    Serializer->>Serializer: Serialize with $code support<br/>(emit raw JS for $code:)
    Serializer-->>Imports: Config + code references
    Imports->>Imports: Auto-detect default exports<br/>Generate named imports<br/>Inject collector if needed<br/>Add startFlow for collector
    Imports-->>CLI: Bundled entry point
Loading
sequenceDiagram
    participant App as Application
    participant Collector as Collector Init
    participant Dest as Destination
    participant Queue as Event Queue
    participant On as Lifecycle Handler

    App->>Collector: Start flow with destinations
    Collector->>Queue: Initialize queuePush (event queue)
    Collector->>Queue: Initialize queueOn (lifecycle queue)
    App->>On: Call destination.on('consent')
    On->>Queue: Queue event (queueOn)<br/>dest not initialized yet
    Collector->>Dest: Call destinationInit()
    Dest-->>Collector: init() completes
    Collector->>Queue: Flush queueOn events
    Collector->>On: callDestinationOn for each queued
    On-->>App: Lifecycle events delivered
    App->>Dest: Push regular events
    Dest->>Dest: process via adapter
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • 565 ai readiness #566: Modifies overlapping bundler, flow resolution, and collector destination/queue internals (packages/cli/src/commands/bundle/bundler.ts, packages/core/src/flow.ts, packages/collector/src/{destination,on}.ts), directly related at code level.
  • 547 default source tagging #548: Updates collector internals for destination queue properties and on() lifecycle logic (packages/collector/*.ts), foundational to queue refactoring in this PR.
  • 557 docker #558: Touches CLI bundler, config, simulate, and run command implementations (packages/cli), with overlapping bundler and command path changes.

Poem

🐰 Hop, hop—the patterns now align!
With $var, $env, $def so fine,
Queues that wait for init's embrace,
Snowplow trackers join the race.
Code flows bundled, collectors fly—
Walker hops through walkerOS sky!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title '554 snowplow web destination' directly references the main change—implementing a Snowplow web destination integration. It clearly identifies the primary feature added.
Docstring Coverage ✅ Passed Docstring coverage is 85.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Contributor

Preview deployed

https://walkeros-io.b-cdn.net/preview/pr-585/

@aws-amplify-eu-central-1
Copy link

This pull request is automatically being deployed by Amplify Hosting (learn more).

Access this pull request here: https://pr-585.d27uka2p9i8zm5.amplifyapp.com

@alexanderkirtzel alexanderkirtzel merged commit a21d8dd into main Jan 26, 2026
8 of 9 checks passed
@alexanderkirtzel alexanderkirtzel deleted the 554-snowplow-web-destination branch January 26, 2026 09:13
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
packages/cli/src/commands/push/index.ts (1)

135-161: process.exit(0) bypasses the finally block, leaving temp directories uncleaned.

Calling process.exit(0) on line 138 terminates the process immediately, preventing the finally block (lines 154-161) from executing. This means temp directories created during successful pushes will never be cleaned up, potentially causing disk space issues over time.

Consider cleaning up before exiting:

Proposed fix
     // Explicit exit on success to avoid hanging from open handles
     // (JSDOM instances, esbuild workers, HTTP connections, etc.)
+    if (tempDir) {
+      await fs.remove(tempDir).catch(() => {
+        // Ignore cleanup errors
+      });
+    }
     process.exit(0);
packages/cli/src/commands/bundle/bundler.ts (2)

79-137: Cache key is computed before implicit collector injection.

Because the collector is added after the cache lookup, implicit-collector flows won’t hit the cache and will store under a different key. Normalize packages before cache key generation (or include the auto-add in generateCacheKeyContent) so cache hits work.

🧩 Suggested reordering
-  // Check build cache if caching is enabled
-  if (buildOptions.cache !== false) {
-    const configContent = generateCacheKeyContent(flowConfig, buildOptions);
-    ...
-  }
-
-  try {
-    // Step 1: Ensure temporary directory exists
-    await fs.ensureDir(TEMP_DIR);
-
-    // Step 1.5: Auto-add collector if sources/destinations exist but collector not specified
-    const hasSourcesOrDests = ...
-    if (hasSourcesOrDests && !buildOptions.packages['@walkeros/collector']) {
-      buildOptions.packages['@walkeros/collector'] = {};
-    }
+  // Auto-add collector before cache key generation
+  const hasSourcesOrDests = ...
+  if (hasSourcesOrDests && !buildOptions.packages['@walkeros/collector']) {
+    buildOptions.packages['@walkeros/collector'] = {};
+  }
+
+  // Check build cache if caching is enabled
+  if (buildOptions.cache !== false) {
+    const configContent = generateCacheKeyContent(flowConfig, buildOptions);
+    ...
+  }
+
+  try {
+    // Step 1: Ensure temporary directory exists
+    await fs.ensureDir(TEMP_DIR);

557-625: Mixed explicit + implicit usage of a package can drop the default import.

When any explicit code exists for a package, generateImportStatements skips the default import entirely. If another source/destination omits code, buildConfigObject still references the default import variable, leading to a runtime ReferenceError. Track which packages need a default import per usage and emit it whenever any usage is implicit.

✅ One way to preserve default imports when needed
+function detectPackagesNeedingDefaultImport(flowConfig: Flow.Config): Set<string> {
+  const needsDefault = new Set<string>();
+  const sources =
+    (flowConfig as { sources?: Record<string, { package?: string; code?: string }> })
+      .sources || {};
+  const destinations =
+    (flowConfig as {
+      destinations?: Record<string, { package?: string; code?: string }>;
+    }).destinations || {};
+
+  for (const cfg of [...Object.values(sources), ...Object.values(destinations)]) {
+    if (cfg?.package && (!cfg.code || cfg.code.startsWith('_'))) {
+      needsDefault.add(cfg.package);
+    }
+  }
+  return needsDefault;
+}
@@
-  const { importStatements } = generateImportStatements(
+  const defaultImportPackages = detectPackagesNeedingDefaultImport(flowConfig);
+  const { importStatements } = generateImportStatements(
     buildOptions.packages,
     destinationPackages,
     sourcePackages,
     explicitCodeImports,
+    defaultImportPackages,
   );
@@
-function generateImportStatements(
+function generateImportStatements(
   packages: BuildOptions['packages'],
   destinationPackages: Set<string>,
   sourcePackages: Set<string>,
   explicitCodeImports: Map<string, Set<string>>,
+  defaultImportPackages: Set<string>,
 ): ImportGenerationResult {
@@
-    if (isUsedByDestOrSource && !hasExplicitCode) {
+    if (defaultImportPackages.has(packageName)) {
       const varName = packageNameToVariable(packageName);
       importStatements.push(`import ${varName} from '${packageName}';`);
     }
🤖 Fix all issues with AI agents
In `@apps/quickstart/CHANGELOG.md`:
- Around line 7-10: Remove the duplicate "Updated dependencies [b65b773]" entry
in the changelog so it only appears once; locate the repeated line string
"Updated dependencies [b65b773]" (the two identical entries surrounding the
"@walkeros/collector@1.0.1" bullet) and delete the redundant occurrence so the
changelog lists each update reference only once.

In `@packages/cli/CHANGELOG.md`:
- Around line 19-25: The example in the CHANGELOG shows a semicolon after the
arrow function inside the object literal (property "fn"), which is a syntax nit;
edit the example so the object property uses no trailing semicolon (remove the
semicolon after the arrow function in the "fn" property) to produce a valid
JavaScript object literal in the docs.

In `@packages/cli/src/commands/bundle/bundler.ts`:
- Around line 222-225: Remove the invalid call to esbuild.stop() in the finally
block of the bundler logic: delete the await esbuild.stop() line (esbuild.stop()
does not exist in the Node.js API) and leave no explicit cleanup for single-shot
esbuild.build() calls; if you later add watch/serve behavior use the proper APIs
(context.dispose() for context/watch or server.stop() for serve) instead of
esbuild.stop().

In `@packages/collector/src/destination.ts`:
- Line 146: The call to concat on destination.queuePush discards the result so
skippedEvents never get added; update the code that currently calls
destination.queuePush.concat(skippedEvents) to actually merge the arrays (e.g.,
replace with an assignment destination.queuePush =
destination.queuePush.concat(skippedEvents) or use
destination.queuePush.push(...skippedEvents)) so skippedEvents are appended to
destination.queuePush.

In `@packages/collector/src/on.ts`:
- Around line 38-59: callDestinationOn builds a Destination.Context but omits
the required setIngest field so on() handlers get an undefined or fail
type-checking; update callDestinationOn to include context.setIngest by wiring
the same setIngest function used in the destination push context (i.e., source
it from the collector or the shared context builder used for destination pushes)
so Destination.Context in callDestinationOn (used by callDestinationOn,
destination.on, mergeEnvironments, tryCatch) matches the type in packages/core
and provides the correct setIngest implementation to handlers.
- Around line 107-117: The current logic queues on-events whenever
destination.config.init is falsy, causing permanent queuing for destinations
that implement on() but have no init(); change the gate to check the actual
presence/state of an init step: use destination.init (or set
destination.config.init = true when destination.init is undefined) before
pushing to destination.queueOn so only destinations with a pending init get
queued; locate the logic in the loop over collector.destinations (symbols:
collector.destinations, destination.on, destination.config.init,
destination.init, destination.queueOn, callDestinationOn) and update it so
non-init destinations call callDestinationOn immediately while only destinations
with a defined/pending init have their on-events enqueued.

In `@packages/core/src/flow.ts`:
- Around line 69-78: Add an exact-match branch before the inline replacement to
preserve non-string types: test value against the full-match regex
/^\$var\.([a-zA-Z_][a-zA-Z0-9_]*)$/ and if it matches, return variables[name]
directly (or call throwError if undefined) instead of stringifying; otherwise
keep the existing value.replace(...) inline substitution logic that uses
String(variables[name]) and throwError for missing variables. Use the same
capture group/name logic as in the current /\$var\.([a-zA-Z_][a-zA-Z0-9_]*)/g
code and keep references to value, variables, throwError and the current
replacement behavior.
- Around line 58-67: The code in resolvePatterns currently follows $def
references (defMatch / defName) without cycle detection, so add a tracking set
or stack (e.g., pass a visitedDefs Set<string> or resolutionStack) into
resolvePatterns and check it before recursing: if defName is already in the
set/stack, throw a clear error like 'Cyclic definition reference: <defName>';
otherwise add defName, recurse to resolvePatterns(definitions[defName],
variables, definitions, visitedDefs), and remove defName after recursion
completes to prevent false positives. Ensure all call sites of resolvePatterns
are updated to propagate the visited set (initializing it empty at the top
level).

In `@packages/transformers/validator/package.json`:
- Line 4: The package.json in the validator package was bumped to "2.0.0"
without any breaking changes; change the "version" field in
packages/transformers/validator/package.json from "2.0.0" to "1.1.0" (or "1.0.1"
if you prefer patch) to reflect the dependency-only update, and adjust the
CHANGELOG entry to match the chosen semantic version so the release metadata
remains consistent with the actual changes.

In `@packages/web/destinations/snowplow/package.json`:
- Around line 40-45: Replace the two devDependencies that use the floating
"latest" tag—@walkeros/collector and `@walkeros/config`—in package.json with
explicit version ranges (e.g., ^x.y.z) to ensure reproducible installs; update
the entries for "@walkeros/collector" and "@walkeros/config" to the chosen
pinned versions, run npm install to refresh package-lock.json/workspaces, and
ensure the same pinned versions are used consistently across other destination
packages.

In `@packages/web/destinations/snowplow/README.md`:
- Around line 369-405: Replace the bold inline "headings" like "**Option 1:
Auto-track on init**", "**Option 2: Track via walkerOS event**", and "**Option
3: Custom event name**" with proper Markdown headings (e.g., "### Option 1:
Auto-track on init") in the README content to satisfy MD036; update the three
heading lines so they use actual heading syntax and keep the surrounding code
blocks and examples unchanged (look for the exact text "Option 1: Auto-track on
init", "Option 2: Track via walkerOS event", and "Option 3: Custom event name"
to locate the replacements).
- Around line 1076-1080: Replace the bare URL in the README step "4. **Inspect
events**" by wrapping it as a Markdown link to satisfy MD034: update the line
under "- **Web UI**" that currently reads "Open http://localhost:9090/micro/ui
in your browser" to use the link form, e.g. "Open
[http://localhost:9090/micro/ui](http://localhost:9090/micro/ui) in your
browser".

In `@packages/web/destinations/snowplow/src/examples/mapping.ts`:
- Around line 93-102: The is_guest mapping under SCHEMAS.USER currently returns
true when user.id exists (inverted); update the fn for is_guest to return false
when (event as { user?: { id?: string } }).user?.id is present and true (or
undefined when unknown), and return true when no user.id (anonymous); modify the
anonymous arrow function inside the is_guest mapping to invert its boolean logic
accordingly.

In `@packages/web/destinations/snowplow/src/examples/walkeros-events.ts`:
- Around line 125-182: The transaction example functions transaction and
transactionMultipleProducts use data.revenue and data.tax but the Snowplow
mapping expects data.total and data.taxes; update those event payloads in
getEvent calls (inside transaction and transactionMultipleProducts) to rename
revenue -> total and tax -> taxes (and corresponding comments/examples) so the
example fields match the Snowplow mapping rules.

In `@packages/web/destinations/snowplow/src/index.ts`:
- Around line 191-193: The check for the required collectorUrl calls
logger.throw('Config settings collectorUrl missing') but doesn't stop execution;
update the code handling for the collectorUrl validation so the function returns
immediately after logger.throw (or replace logger.throw with an actual thrown
Error) to prevent proceeding with an undefined collectorUrl; locate the
collectorUrl validation and ensure either a return statement follows the
logger.throw call or throw a new Error there so subsequent uses of collectorUrl
(e.g., where non-null assertions are used) are safe.

In `@packages/web/destinations/snowplow/src/setup.ts`:
- Around line 20-34: The dedupe check in addScript currently uses collectorUrl
but should use the script source URL; update the function (addScript) to check
loadedScripts.has(src) (or script.src after resolving DEFAULT_SCRIPT_URL) and to
add src (or script.src) to loadedScripts after appending the element, so
duplicate script src values are prevented regardless of collectorUrl; keep the
rest of the logic (getEnv/document manipulation, async/script insertion)
unchanged.

In `@website/docs/destinations/server/meta-capi.mdx`:
- Around line 103-105: The value for "test_event_code" is using the $env pattern
but has an unintended trailing colon; update the "test_event_code" entry (the
"test_event_code" key referencing $env.META_TEST_CODE) to either remove the
trailing ':' so it matches other entries or provide a sensible default after the
colon (e.g., $env.META_TEST_CODE:defaultValue) so the env fallback pattern is
consistent.
🧹 Nitpick comments (12)
package.json (1)

48-48: Consider updating the version constraint for consistency.

The devDependency specifies ^1.0.1, but packages/config/package.json is being bumped to 1.0.2. While semver caret will accept 1.0.2, updating to ^1.0.2 would provide clearer alignment with the actual version being released.

Suggested change
-    "@walkeros/config": "^1.0.1",
+    "@walkeros/config": "^1.0.2",
packages/web/destinations/plausible/CHANGELOG.md (1)

3-7: Minor formatting inconsistency in changelog entry.

The dependency update entry on line 7 uses a simpler format compared to other changelogs in this PR (e.g., packages/server/sources/fetch/CHANGELOG.md uses "Updated dependencies [commit_hash]" format). Consider using the same format for consistency across the monorepo:

 ### Patch Changes

-- `@walkeros/web-core`@1.0.1
+- Updated dependencies [commit_hash]
+  - `@walkeros/web-core`@1.0.1
packages/web/destinations/meta/CHANGELOG.md (1)

3-7: Minor formatting inconsistency.

This changelog entry differs from other changelogs in this PR (e.g., packages/server/sources/aws/CHANGELOG.md) which use the format:

- Updated dependencies [commit_hash]
  - `@walkeros/package`@version

Consider aligning with the consistent format used elsewhere for easier tracking of changes.

packages/cli/src/commands/simulate/index.ts (1)

99-101: Type string | unknown simplifies to unknown.

The union string | unknown is redundant since unknown is a supertype of all types, making the string | part meaningless. If the intent is to accept either a string path or an object config, consider using a more specific union type.

Suggested fix
 export async function simulate(
-  configOrPath: string | unknown,
+  configOrPath: string | Record<string, unknown>,
   event: unknown,

Or if only string paths are currently supported (as the implementation suggests):

 export async function simulate(
-  configOrPath: string | unknown,
+  configOrPath: string,
   event: unknown,
packages/collector/src/__tests__/on-queue.test.ts (1)

69-108: Assert replay order explicitly to match the test name.
Right now it only checks contains, not ordering.

Proposed assertion tightening
-    // Queued events come after init
-    expect(callOrder.slice(1)).toContain('on:consent');
+    // Queued events come after init and keep order
+    const runIdx = callOrder.indexOf('on:run');
+    const consentIdx = callOrder.indexOf('on:consent');
+    expect(runIdx).toBeGreaterThan(0);
+    expect(consentIdx).toBeGreaterThan(runIdx);
packages/web/destinations/snowplow/src/schemas/mapping.ts (1)

8-13: Consider tightening the data schema.

The data field uses z.record(z.string(), z.unknown()) which is very permissive. The TypeScript interface shows data: WalkerOSMapping.Map which may have more specific structure. If unknown is intentional for flexibility, this is fine.

packages/web/destinations/snowplow/src/adapter.ts (1)

170-173: Silent no-op may surprise users.

The call() method silently does nothing in browser-tracker mode. Consider logging a warning when called, so users understand why their URL-based plugin enable calls aren't working.

💡 Suggested improvement
     call(method: string, ...args: unknown[]) {
       // In browser-tracker mode, arbitrary method calls are not supported
       // URL-based plugins that need enable methods won't work here
+      console.warn(
+        `[snowplow] call('${method}') not supported in browser-tracker mode`,
+      );
     },
packages/web/destinations/snowplow/src/setup.ts (2)

36-40: Duplicate interface definition.

SnowplowFunction is already exported from ./types/index.ts (lines 57-60). Consider importing it to avoid definition drift.

♻️ Proposed refactor
-// Snowplow function interface
-interface SnowplowFunction {
-  (...args: unknown[]): void;
-  q?: unknown[];
-}
+import type { SnowplowFunction } from './types';

10-11: Add scriptUrl to SettingsSchema to allow users to override the tracker script URL.

The code includes a helpful warning comment about using @latest, but the override mechanism isn't exposed to users through the configuration interface. The addScript function accepts a custom src parameter, but users would need direct function access to use it. Consider adding a scriptUrl field to SettingsSchema so users can pin a specific version without modifying code.

packages/web/destinations/snowplow/src/push.ts (1)

170-191: Consider adding type validation for loop expansion values.

The casts on lines 174 and 181 assume scope is a string and itemMapping is valid mapping data, but isLoopContextData only validates the structure has a loop array with 2 elements. If malformed data is provided, this could fail silently or cause unexpected behavior in getMappingValue.

💡 Optional: Add defensive type checks
     if (isLoopContextData(contextDef.data)) {
       const [scope, itemMapping] = contextDef.data.loop;
 
+      // Validate scope is a string path
+      if (!isString(scope)) {
+        continue;
+      }
+
       // Get the source array using getMappingValue with the scope
-      const sourceArray = await getMappingValue(event, scope as string);
+      const sourceArray = await getMappingValue(event, scope);
packages/web/destinations/snowplow/src/schemas/settings.ts (1)

144-158: Usage of z.any() violates coding guidelines.

Per the coding guidelines, any type should not be used in production code. Lines 146 and 155 use z.any() which defeats runtime validation and type safety.

Consider defining more specific schemas for these complex types:

♻️ Suggested improvement for plugins schema
   // Plugins - validated loosely since BrowserPlugin is complex
   plugins: z
-    .array(z.union([UrlBasedPluginSchema, z.any()]))
+    .array(z.union([
+      UrlBasedPluginSchema,
+      z.object({ code: z.unknown(), config: z.record(z.string(), z.unknown()).optional() }),
+      z.unknown() // BrowserPlugin instances - can't validate at runtime
+    ]))
     .optional()
     .describe('Snowplow plugins'),

Note: While z.unknown() is also permissive, it's safer than z.any() as it requires explicit type narrowing when used. Based on coding guidelines.

packages/web/destinations/snowplow/src/types/index.ts (1)

403-412: Documentation says "Required" but type is optional.

The collectorUrl property comment states "Required" but the type is string | undefined. This is intentional to allow runtime validation, but could be confusing. Consider using a clearer pattern.

💡 Optional: Clarify required vs optional distinction
   /**
    * Snowplow collector endpoint URL
    *
-   * Required. The URL of your Snowplow collector.
+   * The URL of your Snowplow collector.
+   * This is required at runtime but optional in the type to enable
+   * partial configuration and runtime validation.
    *
    * `@example` "https://collector.example.com"
    */
   collectorUrl?: string;

Comment on lines +7 to +10
- Updated dependencies [b65b773]
- Updated dependencies [20eca6e]
- Updated dependencies [b65b773]
- @walkeros/collector@1.0.1
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove duplicate “Updated dependencies” entry.
Line 7 and Line 9 repeat the same commit reference, which reads like an accidental duplication.

✅ Suggested fix
- - Updated dependencies [b65b773]
- - Updated dependencies [20eca6e]
- - Updated dependencies [b65b773]
+ - Updated dependencies [b65b773]
+ - Updated dependencies [20eca6e]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- Updated dependencies [b65b773]
- Updated dependencies [20eca6e]
- Updated dependencies [b65b773]
- @walkeros/collector@1.0.1
- Updated dependencies [b65b773]
- Updated dependencies [20eca6e]
- `@walkeros/collector`@1.0.1
🤖 Prompt for AI Agents
In `@apps/quickstart/CHANGELOG.md` around lines 7 - 10, Remove the duplicate
"Updated dependencies [b65b773]" entry in the changelog so it only appears once;
locate the repeated line string "Updated dependencies [b65b773]" (the two
identical entries surrounding the "@walkeros/collector@1.0.1" bullet) and delete
the redundant occurrence so the changelog lists each update reference only once.

Comment on lines +19 to +25
Outputs:

```javascript
{
fn: (value) => value.toUpperCase();
}
```
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Minor syntax error in example output.

The example shows a semicolon after the function inside the object literal, but object properties should be separated by commas. This is a documentation nit.

Suggested fix
   Outputs:

   ```javascript
   {
-    fn: (value) => value.toUpperCase();
+    fn: (value) => value.toUpperCase()
   }
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
Outputs:
🤖 Prompt for AI Agents
In `@packages/cli/CHANGELOG.md` around lines 19 - 25, The example in the CHANGELOG
shows a semicolon after the arrow function inside the object literal (property
"fn"), which is a syntax nit; edit the example so the object property uses no
trailing semicolon (remove the semicolon after the arrow function in the "fn"
property) to produce a valid JavaScript object literal in the docs.

Comment on lines +222 to 225
} finally {
// Clean up esbuild worker threads to allow process to exit
await esbuild.stop();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check esbuild version in manifests/lockfiles
rg -n -C2 '"esbuild"' --glob 'package.json' --glob '*lock*'

Repository: elbwalker/walkerOS

Length of output: 9597


🌐 Web query:

esbuild stop() API documentation version support

💡 Result:

stop() is not a general esbuild JS API method for the Node/npm package; in Node you typically end long‑running activity by disposing a build context (e.g. ctx.dispose() for watch/serve) instead. [1]

Where stop() is documented/supported:

  • Deno wrapper (deno.land/x/esbuild): The Deno package adds await esbuild.stop() because Deno won’t exit while esbuild’s internal child process is still running; the docs explicitly say you should call it when you’re done. [2]
  • Version note (Deno stop()): By esbuild v0.11.19, calling stop() no longer permanently prevents further use; esbuild can be used again after stop() (it will restart internally, and you’ll need to call stop() again before exit). [3]

Common confusion:

  • server.stop() exists on the object returned by the serve API (to stop the dev server), but that’s different from esbuild.stop(). [4]

Sources: esbuild API docs [1], esbuild “Getting Started” (Deno section) [2], esbuild changelog entry for 0.11.19 [3], serve API example showing server.stop() [4].


🏁 Script executed:

# Check how esbuild is used in bundler.ts
cat packages/cli/src/commands/bundle/bundler.ts

Repository: elbwalker/walkerOS

Length of output: 28863


Remove the await esbuild.stop() call—it does not exist in the esbuild Node.js API.

The esbuild npm package (v0.27.1) does not provide a stop() method. The stop() API exists only in the Deno wrapper. For Node.js, esbuild.build() requires no explicit cleanup; the promise resolves after the build completes. If you later add watch mode or serve mode, use context.dispose() or server.stop() respectively, but for single-shot builds, simply remove this line to prevent a runtime error.

🤖 Prompt for AI Agents
In `@packages/cli/src/commands/bundle/bundler.ts` around lines 222 - 225, Remove
the invalid call to esbuild.stop() in the finally block of the bundler logic:
delete the await esbuild.stop() line (esbuild.stop() does not exist in the
Node.js API) and leave no explicit cleanup for single-shot esbuild.build()
calls; if you later add watch/serve behavior use the proper APIs
(context.dispose() for context/watch or server.stop() for serve) instead of
esbuild.stop().


// Add skipped events back to the queue
destination.queue.concat(skippedEvents);
destination.queuePush.concat(skippedEvents);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bug: concat() result is discarded.

concat() returns a new array and does not mutate the original. The skipped events are never actually added back to the queue.

🐛 Proposed fix
       // Add skipped events back to the queue
-      destination.queuePush.concat(skippedEvents);
+      destination.queuePush.push(...skippedEvents);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
destination.queuePush.concat(skippedEvents);
destination.queuePush.push(...skippedEvents);
🤖 Prompt for AI Agents
In `@packages/collector/src/destination.ts` at line 146, The call to concat on
destination.queuePush discards the result so skippedEvents never get added;
update the code that currently calls destination.queuePush.concat(skippedEvents)
to actually merge the arrays (e.g., replace with an assignment
destination.queuePush = destination.queuePush.concat(skippedEvents) or use
destination.queuePush.push(...skippedEvents)) so skippedEvents are appended to
destination.queuePush.

Comment on lines +38 to +59
export function callDestinationOn(
collector: Collector.Instance,
destination: Destination.Instance,
destId: string,
type: On.Types,
data: unknown,
) {
if (!destination.on) return;

const destType = destination.type || 'unknown';
const destLogger = collector.logger.scope(destType).scope('on').scope(type);

const context: Destination.Context = {
collector,
logger: destLogger,
id: destId,
config: destination.config,
data: data as Destination.Data,
env: mergeEnvironments(destination.env, destination.config.env),
};

tryCatch(destination.on)(type, context);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Destination.Context is missing setIngest.
Destination.Context includes setIngest (see packages/core/src/types/destination.ts), but the new helper doesn’t supply it. This will either fail type-checking or leave context.setIngest undefined for on() handlers that rely on it. Please wire it in (ideally via the same context builder used for destination pushes).

🤖 Prompt for AI Agents
In `@packages/collector/src/on.ts` around lines 38 - 59, callDestinationOn builds
a Destination.Context but omits the required setIngest field so on() handlers
get an undefined or fail type-checking; update callDestinationOn to include
context.setIngest by wiring the same setIngest function used in the destination
push context (i.e., source it from the collector or the shared context builder
used for destination pushes) so Destination.Context in callDestinationOn (used
by callDestinationOn, destination.on, mergeEnvironments, tryCatch) matches the
type in packages/core and provides the correct setIngest implementation to
handlers.

Comment on lines +93 to +102
// User entity - is_guest: true when user.id exists
{
schema: SCHEMAS.USER,
data: {
id: 'user.id',
email: 'user.email',
is_guest: {
fn: (event) =>
(event as { user?: { id?: string } }).user?.id ? true : undefined,
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Snowplow ecommerce user context is_guest field definition

💡 Result:

In Snowplow’s Ecommerce User context/entity (iglu:com.snowplowanalytics.snowplow.ecommerce/user/jsonschema/1-0-0), the field:

  • is_guest (boolean, optional): Whether or not the user is a guest (i.e., a guest/anonymous checkout rather than a logged-in/registered user). [1][2]

Sources: Snowplow “Ecommerce events and entities” docs [1] and the web tracker ecommerce plugin docs for setEcommerceUser({ id, is_guest, email }) [2].

[1] (docs.snowplow.io)
[2] (docs.snowplow.io)

Citations:


Fix inverted is_guest calculation.

The logic is inverted: when user.id exists (indicating a logged-in user), the code returns true, marking them as a guest. According to Snowplow's ecommerce user context schema, is_guest should be true for anonymous users and false for authenticated users. Invert the logic so false is returned when user.id is present.

Suggested fix
         data: {
           id: 'user.id',
           email: 'user.email',
           is_guest: {
             fn: (event) =>
-              (event as { user?: { id?: string } }).user?.id ? true : undefined,
+              (event as { user?: { id?: string } }).user?.id ? false : true,
           },
         },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// User entity - is_guest: true when user.id exists
{
schema: SCHEMAS.USER,
data: {
id: 'user.id',
email: 'user.email',
is_guest: {
fn: (event) =>
(event as { user?: { id?: string } }).user?.id ? true : undefined,
},
// User entity - is_guest: true when user.id exists
{
schema: SCHEMAS.USER,
data: {
id: 'user.id',
email: 'user.email',
is_guest: {
fn: (event) =>
(event as { user?: { id?: string } }).user?.id ? false : true,
},
🤖 Prompt for AI Agents
In `@packages/web/destinations/snowplow/src/examples/mapping.ts` around lines 93 -
102, The is_guest mapping under SCHEMAS.USER currently returns true when user.id
exists (inverted); update the fn for is_guest to return false when (event as {
user?: { id?: string } }).user?.id is present and true (or undefined when
unknown), and return true when no user.id (anonymous); modify the anonymous
arrow function inside the is_guest mapping to invert its boolean logic
accordingly.

Comment on lines +125 to +182
export function transaction(): WalkerOS.Event {
return getEvent('order complete', {
data: {
id: 'ORD-123',
revenue: 999,
currency: 'USD',
payment_method: 'credit_card',
total_quantity: 1,
tax: 80,
shipping: 10,
},
nested: [
{
entity: 'product',
data: {
id: 'P123',
name: 'Laptop',
category: 'Electronics',
price: 999,
currency: 'USD',
quantity: 1,
},
nested: [],
},
],
context: { shopping: ['complete', 0] },
globals: { pagegroup: 'shop' },
});
}

/**
* Transaction with Multiple Products
* walkerOS: elb('order complete', {
* id: 'ORD-456',
* revenue: 1097,
* tax: 120,
* shipping: 15,
* currency: 'USD',
* payment_method: 'paypal'
* }, {
* nested: [
* { type: 'product', data: { id: 'P123', name: 'Laptop', price: 999, quantity: 1 } },
* { type: 'product', data: { id: 'P456', name: 'Mouse', price: 49, quantity: 2 } }
* ]
* })
* Generated when order with multiple products is completed
*/
export function transactionMultipleProducts(): WalkerOS.Event {
return getEvent('order complete', {
data: {
id: 'ORD-456',
revenue: 1097,
currency: 'USD',
payment_method: 'paypal',
total_quantity: 3,
tax: 120,
shipping: 15,
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Align transaction example fields with mapping expectations.

Line 129-135 and Line 175-181 set revenue/tax, but the Snowplow mapping rules resolve data.total/data.taxes, so these examples won’t reflect the values you set. Consider renaming the fields (or update the mapping if revenue/tax is the canonical shape).

✅ Suggested fix
 export function transaction(): WalkerOS.Event {
   return getEvent('order complete', {
     data: {
       id: 'ORD-123',
-      revenue: 999,
+      total: 999,
       currency: 'USD',
       payment_method: 'credit_card',
       total_quantity: 1,
-      tax: 80,
+      taxes: 80,
       shipping: 10,
     },
 export function transactionMultipleProducts(): WalkerOS.Event {
   return getEvent('order complete', {
     data: {
       id: 'ORD-456',
-      revenue: 1097,
+      total: 1097,
       currency: 'USD',
       payment_method: 'paypal',
       total_quantity: 3,
-      tax: 120,
+      taxes: 120,
       shipping: 15,
     },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function transaction(): WalkerOS.Event {
return getEvent('order complete', {
data: {
id: 'ORD-123',
revenue: 999,
currency: 'USD',
payment_method: 'credit_card',
total_quantity: 1,
tax: 80,
shipping: 10,
},
nested: [
{
entity: 'product',
data: {
id: 'P123',
name: 'Laptop',
category: 'Electronics',
price: 999,
currency: 'USD',
quantity: 1,
},
nested: [],
},
],
context: { shopping: ['complete', 0] },
globals: { pagegroup: 'shop' },
});
}
/**
* Transaction with Multiple Products
* walkerOS: elb('order complete', {
* id: 'ORD-456',
* revenue: 1097,
* tax: 120,
* shipping: 15,
* currency: 'USD',
* payment_method: 'paypal'
* }, {
* nested: [
* { type: 'product', data: { id: 'P123', name: 'Laptop', price: 999, quantity: 1 } },
* { type: 'product', data: { id: 'P456', name: 'Mouse', price: 49, quantity: 2 } }
* ]
* })
* Generated when order with multiple products is completed
*/
export function transactionMultipleProducts(): WalkerOS.Event {
return getEvent('order complete', {
data: {
id: 'ORD-456',
revenue: 1097,
currency: 'USD',
payment_method: 'paypal',
total_quantity: 3,
tax: 120,
shipping: 15,
},
export function transaction(): WalkerOS.Event {
return getEvent('order complete', {
data: {
id: 'ORD-123',
total: 999,
currency: 'USD',
payment_method: 'credit_card',
total_quantity: 1,
taxes: 80,
shipping: 10,
},
nested: [
{
entity: 'product',
data: {
id: 'P123',
name: 'Laptop',
category: 'Electronics',
price: 999,
currency: 'USD',
quantity: 1,
},
nested: [],
},
],
context: { shopping: ['complete', 0] },
globals: { pagegroup: 'shop' },
});
}
/**
* Transaction with Multiple Products
* walkerOS: elb('order complete', {
* id: 'ORD-456',
* total: 1097,
* taxes: 120,
* shipping: 15,
* currency: 'USD',
* payment_method: 'paypal'
* }, {
* nested: [
* { type: 'product', data: { id: 'P123', name: 'Laptop', price: 999, quantity: 1 } },
* { type: 'product', data: { id: 'P456', name: 'Mouse', price: 49, quantity: 2 } }
* ]
* })
* Generated when order with multiple products is completed
*/
export function transactionMultipleProducts(): WalkerOS.Event {
return getEvent('order complete', {
data: {
id: 'ORD-456',
total: 1097,
currency: 'USD',
payment_method: 'paypal',
total_quantity: 3,
taxes: 120,
shipping: 15,
},
🤖 Prompt for AI Agents
In `@packages/web/destinations/snowplow/src/examples/walkeros-events.ts` around
lines 125 - 182, The transaction example functions transaction and
transactionMultipleProducts use data.revenue and data.tax but the Snowplow
mapping expects data.total and data.taxes; update those event payloads in
getEvent calls (inside transaction and transactionMultipleProducts) to rename
revenue -> total and tax -> taxes (and corresponding comments/examples) so the
example fields match the Snowplow mapping rules.

Comment on lines +191 to +193
// Required collector URL
if (!collectorUrl) logger.throw('Config settings collectorUrl missing');

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing early return after logger.throw for required collectorUrl.

After calling logger.throw on line 192, the function continues execution. If logger.throw doesn't actually throw an exception, collectorUrl could still be undefined, leading to the non-null assertions on lines 210, 225, and 232 being invalid.

🐛 Proposed fix
     // Required collector URL
-    if (!collectorUrl) logger.throw('Config settings collectorUrl missing');
+    if (!collectorUrl) {
+      logger.throw('Config settings collectorUrl missing');
+      return false;
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Required collector URL
if (!collectorUrl) logger.throw('Config settings collectorUrl missing');
// Required collector URL
if (!collectorUrl) {
logger.throw('Config settings collectorUrl missing');
return false;
}
🤖 Prompt for AI Agents
In `@packages/web/destinations/snowplow/src/index.ts` around lines 191 - 193, The
check for the required collectorUrl calls logger.throw('Config settings
collectorUrl missing') but doesn't stop execution; update the code handling for
the collectorUrl validation so the function returns immediately after
logger.throw (or replace logger.throw with an actual thrown Error) to prevent
proceeding with an undefined collectorUrl; locate the collectorUrl validation
and ensure either a return statement follows the logger.throw call or throw a
new Error there so subsequent uses of collectorUrl (e.g., where non-null
assertions are used) are safe.

Comment on lines +20 to +34
export function addScript(
collectorUrl: string,
env?: DestinationWeb.Env,
src = DEFAULT_SCRIPT_URL,
): void {
// Prevent loading the same script multiple times
if (loadedScripts.has(collectorUrl)) return;

const { document } = getEnv(env);
const script = (document as Document).createElement('script');
script.src = src;
script.async = true;
(document as Document).head.appendChild(script);
loadedScripts.add(collectorUrl);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Deduplication key uses collectorUrl instead of script src.

The loadedScripts set uses collectorUrl as the key, but this doesn't prevent loading the same script URL multiple times if called with different collector URLs. If the intent is to deduplicate by script source, use src instead.

🐛 Proposed fix
 export function addScript(
   collectorUrl: string,
   env?: DestinationWeb.Env,
   src = DEFAULT_SCRIPT_URL,
 ): void {
   // Prevent loading the same script multiple times
-  if (loadedScripts.has(collectorUrl)) return;
+  if (loadedScripts.has(src)) return;

   const { document } = getEnv(env);
   const script = (document as Document).createElement('script');
   script.src = src;
   script.async = true;
   (document as Document).head.appendChild(script);
-  loadedScripts.add(collectorUrl);
+  loadedScripts.add(src);
 }
🤖 Prompt for AI Agents
In `@packages/web/destinations/snowplow/src/setup.ts` around lines 20 - 34, The
dedupe check in addScript currently uses collectorUrl but should use the script
source URL; update the function (addScript) to check loadedScripts.has(src) (or
script.src after resolving DEFAULT_SCRIPT_URL) and to add src (or script.src) to
loadedScripts after appending the element, so duplicate script src values are
prevented regardless of collectorUrl; keep the rest of the logic
(getEnv/document manipulation, async/script insertion) unchanged.

Comment on lines +103 to +105
"accessToken": "$env.META_ACCESS_TOKEN",
"pixelId": "$env.META_PIXEL_ID",
"test_event_code": "$env.META_TEST_CODE:"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Search for $env patterns with trailing colons to see if this is consistent usage
rg '\$env\.[A-Z_]+:' --multiline

# Also search for any documentation about the $env syntax
rg '\$env\.' docs/ -A 2 -B 2 | head -100

Repository: elbwalker/walkerOS

Length of output: 1665


Remove or complete the trailing colon in test_event_code.

The trailing colon in "test_event_code": "$env.META_TEST_CODE:" appears to be incomplete. While the $env.NAME:default syntax is intentional throughout the codebase (allowing environment variables with optional fallback defaults), this line has a colon with no default value, making it inconsistent with all other uses of the pattern (e.g., $env.AWS_REGION:eu-central-1, $env.BQ_DATASET:walkeros). Either remove the colon to match the pattern on lines 103-104, or add an appropriate default value after it.

🤖 Prompt for AI Agents
In `@website/docs/destinations/server/meta-capi.mdx` around lines 103 - 105, The
value for "test_event_code" is using the $env pattern but has an unintended
trailing colon; update the "test_event_code" entry (the "test_event_code" key
referencing $env.META_TEST_CODE) to either remove the trailing ':' so it matches
other entries or provide a sensible default after the colon (e.g.,
$env.META_TEST_CODE:defaultValue) so the env fallback pattern is consistent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Snowplow web destination

2 participants