diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 04d151db8..383a282ad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -119,7 +119,7 @@ jobs: - name: Publish to npm run: npx changeset publish env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Commit version changes (stable only) if: inputs.release_type == 'stable release' diff --git a/.npmrc b/.npmrc deleted file mode 100644 index f0a91ac69..000000000 --- a/.npmrc +++ /dev/null @@ -1,2 +0,0 @@ -//registry.npmjs.org/:_authToken=${NPM_TOKEN} -loglevel=error diff --git a/apps/cli/CHANGELOG.md b/apps/cli/CHANGELOG.md index 1a4dd07db..11c971d67 100644 --- a/apps/cli/CHANGELOG.md +++ b/apps/cli/CHANGELOG.md @@ -1,5 +1,13 @@ # walkeros +## 1.0.3 + +### Patch Changes + +- Updated dependencies [888bbdf] +- Updated dependencies [fdf6e7b] + - @walkeros/cli@1.1.0 + ## 1.0.2 ### Patch Changes diff --git a/apps/cli/package.json b/apps/cli/package.json index a3b126ca1..81848f509 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,6 +1,6 @@ { "name": "walkeros", - "version": "1.0.2", + "version": "1.0.3", "description": "walkerOS CLI - Bundle and deploy walkerOS components", "license": "MIT", "type": "module", @@ -18,7 +18,7 @@ "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist" }, "dependencies": { - "@walkeros/cli": "1.0.2" + "@walkeros/cli": "1.1.0" }, "devDependencies": { "tsup": "^8.5.1", diff --git a/apps/demos/destination/CHANGELOG.md b/apps/demos/destination/CHANGELOG.md index 3d4cfa8d3..8949239aa 100644 --- a/apps/demos/destination/CHANGELOG.md +++ b/apps/demos/destination/CHANGELOG.md @@ -1,5 +1,13 @@ # @walkeros/destination-demo +## 1.0.2 + +### Patch Changes + +- Updated dependencies [f39d9fb] +- Updated dependencies [888bbdf] + - @walkeros/core@1.2.0 + ## 1.0.1 ### Patch Changes diff --git a/apps/demos/destination/package.json b/apps/demos/destination/package.json index 758a47e9e..4ae1ce8f4 100644 --- a/apps/demos/destination/package.json +++ b/apps/demos/destination/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/destination-demo", "description": "Demo destination for walkerOS - logs events to console", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -24,7 +24,7 @@ "test": "jest" }, "dependencies": { - "@walkeros/core": "1.1.0" + "@walkeros/core": "1.2.0" }, "repository": { "url": "git+https://github.com/elbwalker/walkerOS.git", diff --git a/apps/demos/react/CHANGELOG.md b/apps/demos/react/CHANGELOG.md index 55e5b8852..bb3ae886a 100644 --- a/apps/demos/react/CHANGELOG.md +++ b/apps/demos/react/CHANGELOG.md @@ -1,5 +1,19 @@ # walkeros-demo-react +## 1.0.3 + +### Patch Changes + +- Updated dependencies [f39d9fb] +- Updated dependencies [888bbdf] +- Updated dependencies [a38d791] + - @walkeros/collector@1.1.0 + - @walkeros/core@1.2.0 + - @walkeros/web-source-browser@1.1.0 + - @walkeros/web-core@1.0.2 + - @walkeros/web-destination-api@1.1.2 + - @walkeros/web-destination-gtag@1.0.2 + ## 1.0.2 ### Patch Changes diff --git a/apps/demos/react/package.json b/apps/demos/react/package.json index cbe8aadbe..b495004ab 100644 --- a/apps/demos/react/package.json +++ b/apps/demos/react/package.json @@ -1,6 +1,6 @@ { "name": "walkeros-demo-react", - "version": "1.0.2", + "version": "1.0.3", "private": true, "type": "module", "scripts": { @@ -16,12 +16,12 @@ }, "dependencies": { "@remix-run/router": "^1.23.0", - "@walkeros/collector": "1.0.1", - "@walkeros/core": "1.1.0", - "@walkeros/web-core": "1.0.1", - "@walkeros/web-destination-api": "1.1.1", - "@walkeros/web-destination-gtag": "1.0.1", - "@walkeros/web-source-browser": "1.0.1", + "@walkeros/collector": "1.1.0", + "@walkeros/core": "1.2.0", + "@walkeros/web-core": "1.0.2", + "@walkeros/web-destination-api": "1.1.2", + "@walkeros/web-destination-gtag": "1.0.2", + "@walkeros/web-source-browser": "1.1.0", "react": "^19.2.3", "react-dom": "^19.2.3", "react-router-dom": "^7.10.1" diff --git a/apps/demos/source/CHANGELOG.md b/apps/demos/source/CHANGELOG.md index 6546770b7..a7a417554 100644 --- a/apps/demos/source/CHANGELOG.md +++ b/apps/demos/source/CHANGELOG.md @@ -1,5 +1,13 @@ # @walkeros/source-demo +## 1.0.2 + +### Patch Changes + +- Updated dependencies [f39d9fb] +- Updated dependencies [888bbdf] + - @walkeros/core@1.2.0 + ## 1.0.1 ### Patch Changes diff --git a/apps/demos/source/package.json b/apps/demos/source/package.json index 19e1c2bd5..45766ddd1 100644 --- a/apps/demos/source/package.json +++ b/apps/demos/source/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/source-demo", "description": "Demo source for walkerOS - generates events from config", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -24,7 +24,7 @@ "test": "jest" }, "dependencies": { - "@walkeros/core": "1.1.0" + "@walkeros/core": "1.2.0" }, "repository": { "url": "git+https://github.com/elbwalker/walkerOS.git", diff --git a/apps/demos/storybook/CHANGELOG.md b/apps/demos/storybook/CHANGELOG.md index 59bd33820..95da00762 100644 --- a/apps/demos/storybook/CHANGELOG.md +++ b/apps/demos/storybook/CHANGELOG.md @@ -1,5 +1,12 @@ # @walkeros/storybook-demo +## 1.0.2 + +### Patch Changes + +- Updated dependencies [a38d791] + - @walkeros/web-source-browser@1.1.0 + ## 1.0.1 ### Patch Changes diff --git a/apps/demos/storybook/package.json b/apps/demos/storybook/package.json index 621c88065..8884a31a2 100644 --- a/apps/demos/storybook/package.json +++ b/apps/demos/storybook/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/storybook-demo", "private": true, - "version": "1.0.1", + "version": "1.0.2", "type": "module", "scripts": { "dev": "vite", @@ -12,7 +12,7 @@ "build-storybook": "storybook build" }, "dependencies": { - "@walkeros/web-source-browser": "1.0.1", + "@walkeros/web-source-browser": "1.1.0", "react": "^19.2.3", "react-dom": "^19.2.3" }, @@ -24,7 +24,7 @@ "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.2", - "@walkeros/storybook-addon": "1.0.1", + "@walkeros/storybook-addon": "1.0.2", "autoprefixer": "^10.4.23", "eslint": "^9.39.2", "eslint-plugin-react-hooks": "^7.0.1", diff --git a/apps/demos/transformer/CHANGELOG.md b/apps/demos/transformer/CHANGELOG.md index 564e85656..073d2a980 100644 --- a/apps/demos/transformer/CHANGELOG.md +++ b/apps/demos/transformer/CHANGELOG.md @@ -1,5 +1,13 @@ # @walkeros/transformer-demo +## 1.0.2 + +### Patch Changes + +- Updated dependencies [f39d9fb] +- Updated dependencies [888bbdf] + - @walkeros/core@1.2.0 + ## 1.0.1 ### Patch Changes diff --git a/apps/demos/transformer/package.json b/apps/demos/transformer/package.json index ae4297d1f..e688ffa9e 100644 --- a/apps/demos/transformer/package.json +++ b/apps/demos/transformer/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/transformer-demo", "description": "Demo transformer for walkerOS - logs and passes through events", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -24,7 +24,7 @@ "test": "jest" }, "dependencies": { - "@walkeros/core": "1.1.0" + "@walkeros/core": "1.2.0" }, "repository": { "url": "git+https://github.com/elbwalker/walkerOS.git", diff --git a/apps/quickstart/CHANGELOG.md b/apps/quickstart/CHANGELOG.md index 8ea15bd54..cbb2b527f 100644 --- a/apps/quickstart/CHANGELOG.md +++ b/apps/quickstart/CHANGELOG.md @@ -1,5 +1,19 @@ # @walkeros/quickstart +## 1.0.3 + +### Patch Changes + +- Updated dependencies [f39d9fb] +- Updated dependencies [888bbdf] +- Updated dependencies [a38d791] + - @walkeros/collector@1.1.0 + - @walkeros/core@1.2.0 + - @walkeros/web-source-browser@1.1.0 + - @walkeros/web-core@1.0.2 + - @walkeros/web-destination-api@1.1.2 + - @walkeros/web-destination-gtag@1.0.2 + ## 1.0.2 ### Patch Changes diff --git a/apps/quickstart/package.json b/apps/quickstart/package.json index 8df48a102..2f742a941 100644 --- a/apps/quickstart/package.json +++ b/apps/quickstart/package.json @@ -1,6 +1,6 @@ { "name": "@walkeros/quickstart", - "version": "1.0.2", + "version": "1.0.3", "private": true, "description": "Verified code examples for walkerOS documentation", "license": "MIT", @@ -13,12 +13,12 @@ "update": "npx npm-check-updates -u && npm update" }, "dependencies": { - "@walkeros/core": "1.1.0", - "@walkeros/collector": "1.0.1", - "@walkeros/web-core": "1.0.1", - "@walkeros/web-source-browser": "1.0.1", - "@walkeros/web-destination-gtag": "1.0.1", - "@walkeros/web-destination-api": "1.1.1" + "@walkeros/core": "1.2.0", + "@walkeros/collector": "1.1.0", + "@walkeros/web-core": "1.0.2", + "@walkeros/web-source-browser": "1.1.0", + "@walkeros/web-destination-gtag": "1.0.2", + "@walkeros/web-destination-api": "1.1.2" }, "repository": { "url": "git+https://github.com/elbwalker/walkerOS.git", diff --git a/apps/storybook-addon/CHANGELOG.md b/apps/storybook-addon/CHANGELOG.md index f9a675643..9b21388cb 100644 --- a/apps/storybook-addon/CHANGELOG.md +++ b/apps/storybook-addon/CHANGELOG.md @@ -1,5 +1,17 @@ # @walkeros/storybook-addon +## 1.0.2 + +### Patch Changes + +- Updated dependencies [f39d9fb] +- Updated dependencies [888bbdf] +- Updated dependencies [a38d791] + - @walkeros/collector@1.1.0 + - @walkeros/core@1.2.0 + - @walkeros/web-source-browser@1.1.0 + - @walkeros/web-core@1.0.2 + ## 1.0.1 ### Patch Changes diff --git a/apps/storybook-addon/package.json b/apps/storybook-addon/package.json index e1f90592a..af89ea6cc 100644 --- a/apps/storybook-addon/package.json +++ b/apps/storybook-addon/package.json @@ -1,6 +1,6 @@ { "name": "@walkeros/storybook-addon", - "version": "1.0.1", + "version": "1.0.2", "description": "Visualize, debug, and validate walkerOS event tracking in your Storybook stories. Real-time event capture with visual DOM highlighting for data-attribute based tagging.", "keywords": [ "storybook-addons", @@ -59,10 +59,10 @@ "build-storybook": "storybook build" }, "dependencies": { - "@walkeros/core": "1.1.0", - "@walkeros/collector": "1.0.1", - "@walkeros/web-core": "1.0.1", - "@walkeros/web-source-browser": "1.0.1", + "@walkeros/core": "1.2.0", + "@walkeros/collector": "1.1.0", + "@walkeros/web-core": "1.0.2", + "@walkeros/web-source-browser": "1.1.0", "@storybook/icons": "^2.0.1" }, "devDependencies": { diff --git a/apps/walkerjs/CHANGELOG.md b/apps/walkerjs/CHANGELOG.md index fa6eb937d..8814282cc 100644 --- a/apps/walkerjs/CHANGELOG.md +++ b/apps/walkerjs/CHANGELOG.md @@ -1,5 +1,19 @@ # @walkeros/walker.js +## 1.0.2 + +### Patch Changes + +- Updated dependencies [f39d9fb] +- Updated dependencies [888bbdf] +- Updated dependencies [a38d791] + - @walkeros/collector@1.1.0 + - @walkeros/core@1.2.0 + - @walkeros/web-source-browser@1.1.0 + - @walkeros/web-source-session@1.1.0 + - @walkeros/web-source-datalayer@1.0.2 + - @walkeros/web-core@1.0.2 + ## 1.0.1 ### Patch Changes diff --git a/apps/walkerjs/package.json b/apps/walkerjs/package.json index b60f79d9e..54ec2bcfd 100644 --- a/apps/walkerjs/package.json +++ b/apps/walkerjs/package.json @@ -1,6 +1,6 @@ { "name": "@walkeros/walker.js", - "version": "1.0.1", + "version": "1.0.2", "description": "Ready-to-use walkerOS bundle with browser source, collector, and dataLayer support", "license": "MIT", "main": "./dist/index.js", @@ -38,11 +38,12 @@ "preview": "npm run build && npx serve -l 3333 examples" }, "dependencies": { - "@walkeros/core": "1.1.0", - "@walkeros/collector": "1.0.1", - "@walkeros/web-core": "1.0.1", - "@walkeros/web-source-browser": "1.0.1", - "@walkeros/web-source-datalayer": "1.0.1" + "@walkeros/core": "1.2.0", + "@walkeros/collector": "1.1.0", + "@walkeros/web-core": "1.0.2", + "@walkeros/web-source-browser": "1.1.0", + "@walkeros/web-source-datalayer": "1.0.2", + "@walkeros/web-source-session": "1.1.0" }, "devDependencies": { "@swc/jest": "^0.2.39", diff --git a/apps/walkerjs/src/index.ts b/apps/walkerjs/src/index.ts index e4bbbc25e..9a4d649d8 100644 --- a/apps/walkerjs/src/index.ts +++ b/apps/walkerjs/src/index.ts @@ -9,6 +9,7 @@ import { getGlobals, SourceBrowser, } from '@walkeros/web-source-browser'; +import { sourceSession } from '@walkeros/web-source-session'; import { sourceDataLayer } from '@walkeros/web-source-datalayer'; import { dataLayerDestination } from './destination'; @@ -26,9 +27,8 @@ export async function createWalkerjs(config: Config = {}): Promise { dataLayer: { code: dataLayerDestination() }, }, }, - browser: { - session: true, - }, + browser: {}, + session: true, dataLayer: false, elb: 'elb', run: true, @@ -53,6 +53,22 @@ export async function createWalkerjs(config: Config = {}): Promise { }, }; + // Add session source if configured + if (fullConfig.session !== false) { + const sessionSettings = isObject(fullConfig.session) + ? fullConfig.session + : {}; + + if (collectorConfig.sources) { + collectorConfig.sources.session = { + code: sourceSession, + config: { + settings: sessionSettings, + }, + }; + } + } + // Add dataLayer source if configured if (fullConfig.dataLayer) { const dataLayerSettings = isObject(fullConfig.dataLayer) diff --git a/apps/walkerjs/src/types/index.ts b/apps/walkerjs/src/types/index.ts index 1aa9a4f14..fb9037247 100644 --- a/apps/walkerjs/src/types/index.ts +++ b/apps/walkerjs/src/types/index.ts @@ -1,6 +1,7 @@ import type { Collector, Source, WalkerOS } from '@walkeros/core'; import type { SourceBrowser } from '@walkeros/web-source-browser'; import type { SourceDataLayer } from '@walkeros/web-source-datalayer'; +import type { SourceSession } from '@walkeros/web-source-session'; declare global { interface Window { @@ -22,6 +23,9 @@ export interface Config { // Browser source configuration browser?: Partial; + // Session source configuration + session?: boolean | Partial; + // DataLayer configuration dataLayer?: boolean | Partial; diff --git a/package-lock.json b/package-lock.json index ecf361c59..776b5e71b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,10 +44,10 @@ }, "apps/cli": { "name": "walkeros", - "version": "1.0.2", + "version": "1.0.3", "license": "MIT", "dependencies": { - "@walkeros/cli": "1.0.2" + "@walkeros/cli": "1.1.0" }, "bin": { "walkeros": "dist/index.js" @@ -59,23 +59,23 @@ }, "apps/demos/destination": { "name": "@walkeros/destination-demo", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "dependencies": { - "@walkeros/core": "1.1.0" + "@walkeros/core": "1.2.0" } }, "apps/demos/react": { "name": "walkeros-demo-react", - "version": "1.0.2", + "version": "1.0.3", "dependencies": { "@remix-run/router": "^1.23.0", - "@walkeros/collector": "1.0.1", - "@walkeros/core": "1.1.0", - "@walkeros/web-core": "1.0.1", - "@walkeros/web-destination-api": "1.1.1", - "@walkeros/web-destination-gtag": "1.0.1", - "@walkeros/web-source-browser": "1.0.1", + "@walkeros/collector": "1.1.0", + "@walkeros/core": "1.2.0", + "@walkeros/web-core": "1.0.2", + "@walkeros/web-destination-api": "1.1.2", + "@walkeros/web-destination-gtag": "1.0.2", + "@walkeros/web-source-browser": "1.1.0", "react": "^19.2.3", "react-dom": "^19.2.3", "react-router-dom": "^7.10.1" @@ -122,17 +122,17 @@ }, "apps/demos/source": { "name": "@walkeros/source-demo", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "dependencies": { - "@walkeros/core": "1.1.0" + "@walkeros/core": "1.2.0" } }, "apps/demos/storybook": { "name": "@walkeros/storybook-demo", - "version": "1.0.1", + "version": "1.0.2", "dependencies": { - "@walkeros/web-source-browser": "1.0.1", + "@walkeros/web-source-browser": "1.1.0", "react": "^19.2.3", "react-dom": "^19.2.3" }, @@ -145,7 +145,7 @@ "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.2", - "@walkeros/storybook-addon": "1.0.1", + "@walkeros/storybook-addon": "1.0.2", "autoprefixer": "^10.4.23", "eslint": "^9.39.2", "eslint-plugin-react-hooks": "^7.0.1", @@ -183,35 +183,35 @@ }, "apps/demos/transformer": { "name": "@walkeros/transformer-demo", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "dependencies": { - "@walkeros/core": "1.1.0" + "@walkeros/core": "1.2.0" } }, "apps/quickstart": { "name": "@walkeros/quickstart", - "version": "1.0.2", + "version": "1.0.3", "license": "MIT", "dependencies": { - "@walkeros/collector": "1.0.1", - "@walkeros/core": "1.1.0", - "@walkeros/web-core": "1.0.1", - "@walkeros/web-destination-api": "1.1.1", - "@walkeros/web-destination-gtag": "1.0.1", - "@walkeros/web-source-browser": "1.0.1" + "@walkeros/collector": "1.1.0", + "@walkeros/core": "1.2.0", + "@walkeros/web-core": "1.0.2", + "@walkeros/web-destination-api": "1.1.2", + "@walkeros/web-destination-gtag": "1.0.2", + "@walkeros/web-source-browser": "1.1.0" } }, "apps/storybook-addon": { "name": "@walkeros/storybook-addon", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "dependencies": { "@storybook/icons": "^2.0.1", - "@walkeros/collector": "1.0.1", - "@walkeros/core": "1.1.0", - "@walkeros/web-core": "1.0.1", - "@walkeros/web-source-browser": "1.0.1" + "@walkeros/collector": "1.1.0", + "@walkeros/core": "1.2.0", + "@walkeros/web-core": "1.0.2", + "@walkeros/web-source-browser": "1.1.0" }, "devDependencies": { "@storybook/addon-docs": "^10.1.9", @@ -261,14 +261,15 @@ }, "apps/walkerjs": { "name": "@walkeros/walker.js", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "dependencies": { - "@walkeros/collector": "1.0.1", - "@walkeros/core": "1.1.0", - "@walkeros/web-core": "1.0.1", - "@walkeros/web-source-browser": "1.0.1", - "@walkeros/web-source-datalayer": "1.0.1" + "@walkeros/collector": "1.1.0", + "@walkeros/core": "1.2.0", + "@walkeros/web-core": "1.0.2", + "@walkeros/web-source-browser": "1.1.0", + "@walkeros/web-source-datalayer": "1.0.2", + "@walkeros/web-source-session": "1.1.0" }, "devDependencies": { "@swc/jest": "^0.2.39", @@ -5925,6 +5926,69 @@ "react-dom": "*" } }, + "node_modules/@docusaurus/plugin-client-redirects": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-client-redirects/-/plugin-client-redirects-3.9.2.tgz", + "integrity": "sha512-lUgMArI9vyOYMzLRBUILcg9vcPTCyyI2aiuXq/4npcMVqOr6GfmwtmBYWSbNMlIUM0147smm4WhpXD0KFboffw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/logger": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-common": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "eta": "^2.2.0", + "fs-extra": "^11.1.1", + "lodash": "^4.17.21", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-client-redirects/node_modules/fs-extra": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", + "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@docusaurus/plugin-client-redirects/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@docusaurus/plugin-client-redirects/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/@docusaurus/plugin-content-blog": { "version": "3.9.2", "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.9.2.tgz", @@ -11196,7 +11260,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11210,7 +11273,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11224,7 +11286,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11238,7 +11299,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11252,7 +11312,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11266,7 +11325,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11280,7 +11338,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -11291,7 +11348,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", "integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -11307,7 +11363,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11321,7 +11376,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11335,7 +11389,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12863,7 +12916,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -12880,7 +12932,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -12897,7 +12948,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -12914,7 +12964,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -12931,7 +12980,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -12948,7 +12996,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -12965,7 +13012,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -12982,7 +13028,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -12999,7 +13044,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -13016,7 +13060,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -15223,6 +15266,10 @@ "resolved": "packages/web/sources/dataLayer", "link": true }, + "node_modules/@walkeros/web-source-session": { + "resolved": "packages/web/sources/session", + "link": true + }, "node_modules/@walkeros/website": { "resolved": "website", "link": true @@ -40925,11 +40972,11 @@ }, "packages/cli": { "name": "@walkeros/cli", - "version": "1.0.2", + "version": "1.1.0", "license": "MIT", "dependencies": { - "@walkeros/core": "1.1.0", - "@walkeros/server-core": "1.0.1", + "@walkeros/core": "1.2.0", + "@walkeros/server-core": "1.0.2", "chalk": "^5.6.2", "commander": "^14.0.2", "cors": "^2.8.5", @@ -40989,7 +41036,7 @@ }, "packages/collector": { "name": "@walkeros/collector", - "version": "1.0.1", + "version": "1.1.0", "funding": [ { "type": "GitHub Sponsors", @@ -40998,7 +41045,7 @@ ], "license": "MIT", "dependencies": { - "@walkeros/core": "1.1.0" + "@walkeros/core": "1.2.0" }, "devDependencies": {} }, @@ -41037,7 +41084,7 @@ }, "packages/core": { "name": "@walkeros/core", - "version": "1.1.0", + "version": "1.2.0", "funding": [ { "type": "GitHub Sponsors", @@ -41051,7 +41098,7 @@ }, "packages/server/core": { "name": "@walkeros/server-core", - "version": "1.0.1", + "version": "1.0.2", "funding": [ { "type": "GitHub Sponsors", @@ -41060,12 +41107,12 @@ ], "license": "MIT", "dependencies": { - "@walkeros/core": "1.1.0" + "@walkeros/core": "1.2.0" } }, "packages/server/destinations/api": { "name": "@walkeros/server-destination-api", - "version": "1.0.1", + "version": "1.0.2", "funding": [ { "type": "GitHub Sponsors", @@ -41074,14 +41121,14 @@ ], "license": "MIT", "dependencies": { - "@walkeros/core": "1.1.0", - "@walkeros/server-core": "1.0.1" + "@walkeros/core": "1.2.0", + "@walkeros/server-core": "1.0.2" }, "devDependencies": {} }, "packages/server/destinations/aws": { "name": "@walkeros/server-destination-aws", - "version": "1.0.1", + "version": "1.0.2", "funding": [ { "type": "GitHub Sponsors", @@ -41091,13 +41138,13 @@ "license": "MIT", "dependencies": { "@aws-sdk/client-firehose": "^3.952.0", - "@walkeros/server-core": "1.0.1" + "@walkeros/server-core": "1.0.2" }, "devDependencies": {} }, "packages/server/destinations/datamanager": { "name": "@walkeros/server-destination-datamanager", - "version": "1.0.1", + "version": "1.0.2", "funding": [ { "type": "GitHub Sponsors", @@ -41106,15 +41153,15 @@ ], "license": "MIT", "dependencies": { - "@walkeros/core": "1.1.0", - "@walkeros/server-core": "1.0.1", + "@walkeros/core": "1.2.0", + "@walkeros/server-core": "1.0.2", "google-auth-library": "^10.5.0" }, "devDependencies": {} }, "packages/server/destinations/gcp": { "name": "@walkeros/server-destination-gcp", - "version": "1.0.1", + "version": "1.0.2", "funding": [ { "type": "GitHub Sponsors", @@ -41124,13 +41171,13 @@ "license": "MIT", "dependencies": { "@google-cloud/bigquery": "^8.1.1", - "@walkeros/server-core": "1.0.1" + "@walkeros/server-core": "1.0.2" }, "devDependencies": {} }, "packages/server/destinations/meta": { "name": "@walkeros/server-destination-meta", - "version": "1.0.1", + "version": "1.0.2", "funding": [ { "type": "GitHub Sponsors", @@ -41139,14 +41186,14 @@ ], "license": "MIT", "dependencies": { - "@walkeros/core": "1.1.0", - "@walkeros/server-core": "1.0.1" + "@walkeros/core": "1.2.0", + "@walkeros/server-core": "1.0.2" }, "devDependencies": {} }, "packages/server/sources/aws": { "name": "@walkeros/server-source-aws", - "version": "1.0.1", + "version": "1.0.2", "funding": [ { "type": "GitHub Sponsors", @@ -41155,7 +41202,7 @@ ], "license": "MIT", "dependencies": { - "@walkeros/core": "1.1.0" + "@walkeros/core": "1.2.0" }, "devDependencies": { "@types/aws-lambda": "^8.10.159" @@ -41166,7 +41213,7 @@ }, "packages/server/sources/express": { "name": "@walkeros/server-source-express", - "version": "1.0.1", + "version": "1.0.2", "funding": [ { "type": "GitHub Sponsors", @@ -41175,7 +41222,7 @@ ], "license": "MIT", "dependencies": { - "@walkeros/core": "1.1.0", + "@walkeros/core": "1.2.0", "cors": "^2.8.5", "express": "^5.2.1" }, @@ -41447,16 +41494,16 @@ }, "packages/server/sources/fetch": { "name": "@walkeros/server-source-fetch", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "dependencies": { - "@walkeros/core": "1.1.0" + "@walkeros/core": "1.2.0" }, "devDependencies": {} }, "packages/server/sources/gcp": { "name": "@walkeros/server-source-gcp", - "version": "1.0.1", + "version": "1.0.2", "funding": [ { "type": "GitHub Sponsors", @@ -41465,7 +41512,7 @@ ], "license": "MIT", "dependencies": { - "@walkeros/core": "1.1.0" + "@walkeros/core": "1.2.0" }, "devDependencies": {}, "peerDependencies": { @@ -41474,7 +41521,7 @@ }, "packages/server/transformers/fingerprint": { "name": "@walkeros/server-transformer-fingerprint", - "version": "5.0.0", + "version": "6.0.0", "funding": [ { "type": "GitHub Sponsors", @@ -41483,17 +41530,17 @@ ], "license": "MIT", "devDependencies": { - "@walkeros/core": "1.1.0", - "@walkeros/server-core": "1.0.1" + "@walkeros/core": "1.2.0", + "@walkeros/server-core": "1.0.2" }, "peerDependencies": { - "@walkeros/core": "^1.1.0", - "@walkeros/server-core": "^1.0.1" + "@walkeros/core": "^1.2.0", + "@walkeros/server-core": "^1.0.2" } }, "packages/transformers/validator": { "name": "@walkeros/transformer-validator", - "version": "2.0.0", + "version": "3.0.0", "funding": [ { "type": "GitHub Sponsors", @@ -41505,10 +41552,10 @@ "ajv": "^8.17.1" }, "devDependencies": { - "@walkeros/core": "1.1.0" + "@walkeros/core": "1.2.0" }, "peerDependencies": { - "@walkeros/core": "^1.1.0" + "@walkeros/core": "^1.2.0" } }, "packages/transformers/validator/node_modules/ajv": { @@ -41535,7 +41582,7 @@ }, "packages/web/core": { "name": "@walkeros/web-core", - "version": "1.0.1", + "version": "1.0.2", "funding": [ { "type": "GitHub Sponsors", @@ -41544,12 +41591,12 @@ ], "license": "MIT", "dependencies": { - "@walkeros/core": "1.1.0" + "@walkeros/core": "1.2.0" } }, "packages/web/destinations/api": { "name": "@walkeros/web-destination-api", - "version": "1.1.1", + "version": "1.1.2", "funding": [ { "type": "GitHub Sponsors", @@ -41558,13 +41605,13 @@ ], "license": "MIT", "dependencies": { - "@walkeros/web-core": "1.0.1" + "@walkeros/web-core": "1.0.2" }, "devDependencies": {} }, "packages/web/destinations/gtag": { "name": "@walkeros/web-destination-gtag", - "version": "1.0.1", + "version": "1.0.2", "funding": [ { "type": "GitHub Sponsors", @@ -41573,12 +41620,12 @@ ], "license": "MIT", "dependencies": { - "@walkeros/web-core": "1.0.1" + "@walkeros/web-core": "1.0.2" } }, "packages/web/destinations/meta": { "name": "@walkeros/web-destination-meta", - "version": "1.0.1", + "version": "1.0.2", "funding": [ { "type": "GitHub Sponsors", @@ -41587,7 +41634,7 @@ ], "license": "MIT", "dependencies": { - "@walkeros/web-core": "1.0.1" + "@walkeros/web-core": "1.0.2" }, "devDependencies": { "@types/facebook-pixel": "^0.0.31" @@ -41595,7 +41642,7 @@ }, "packages/web/destinations/piwikpro": { "name": "@walkeros/web-destination-piwikpro", - "version": "1.0.1", + "version": "1.0.2", "funding": [ { "type": "GitHub Sponsors", @@ -41604,14 +41651,14 @@ ], "license": "MIT", "dependencies": { - "@walkeros/core": "1.1.0", - "@walkeros/web-core": "1.0.1" + "@walkeros/core": "1.2.0", + "@walkeros/web-core": "1.0.2" }, "devDependencies": {} }, "packages/web/destinations/plausible": { "name": "@walkeros/web-destination-plausible", - "version": "1.0.1", + "version": "1.0.2", "funding": [ { "type": "GitHub Sponsors", @@ -41620,13 +41667,13 @@ ], "license": "MIT", "dependencies": { - "@walkeros/web-core": "1.0.1" + "@walkeros/web-core": "1.0.2" }, "devDependencies": {} }, "packages/web/destinations/snowplow": { "name": "@walkeros/web-destination-snowplow", - "version": "0.0.8", + "version": "0.0.9", "funding": [ { "type": "GitHub Sponsors", @@ -41635,7 +41682,7 @@ ], "license": "MIT", "dependencies": { - "@walkeros/web-core": "1.0.1" + "@walkeros/web-core": "1.0.2" }, "devDependencies": { "@snowplow/browser-plugin-snowplow-ecommerce": "^4.6.8", @@ -41699,6 +41746,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/@walkeros/core/-/core-1.0.0.tgz", "integrity": "sha512-SI7ktgBx/yQ+1vDfe1zyWLV74wYiv98eDQEgHzsyZhaqoviXjMC09X7JBhVN6VK2dL9TGM59C4Aks8uccrCpKQ==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -41712,7 +41760,7 @@ }, "packages/web/sources/browser": { "name": "@walkeros/web-source-browser", - "version": "1.0.1", + "version": "1.1.0", "funding": [ { "type": "GitHub Sponsors", @@ -41721,13 +41769,13 @@ ], "license": "MIT", "dependencies": { - "@walkeros/collector": "1.0.1", - "@walkeros/web-core": "1.0.1" + "@walkeros/collector": "1.1.0", + "@walkeros/web-core": "1.0.2" } }, "packages/web/sources/dataLayer": { "name": "@walkeros/web-source-datalayer", - "version": "1.0.1", + "version": "1.0.2", "funding": [ { "type": "GitHub Sponsors", @@ -41736,13 +41784,23 @@ ], "license": "MIT", "dependencies": { - "@walkeros/collector": "1.0.1", - "@walkeros/core": "1.1.0" + "@walkeros/collector": "1.1.0", + "@walkeros/core": "1.2.0" }, "devDependencies": { "@types/gtag.js": "^0.0.20" } }, + "packages/web/sources/session": { + "name": "@walkeros/web-source-session", + "version": "1.1.0", + "license": "MIT", + "dependencies": { + "@walkeros/core": "1.2.0", + "@walkeros/web-core": "1.0.2" + }, + "devDependencies": {} + }, "website": { "name": "@walkeros/website", "version": "0.6.2", @@ -41775,6 +41833,7 @@ "devDependencies": { "@docusaurus/faster": "^3.9.2", "@docusaurus/module-type-aliases": "^3.9.2", + "@docusaurus/plugin-client-redirects": "^3.9.2", "@docusaurus/tsconfig": "^3.9.2", "@docusaurus/types": "^3.9.2", "@heroicons/react": "^2.2.0", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index c5d6658df..cb2c06110 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,54 @@ # @walkeros/cli +## 1.1.0 + +### Minor Changes + +- 888bbdf: Add inline code syntax for sources, transformers, and destinations + + Enables defining custom logic directly in flow.json using `code` objects + instead of requiring external packages. This is ideal for simple one-liner + transformations. + + **Example:** + + ```json + { + "transformers": { + "enrich": { + "code": { + "push": "$code:(event) => ({ ...event, data: { ...event.data, enriched: true } })" + }, + "config": {} + } + } + } + ``` + + **Code object properties:** + - `push` - The push function with `$code:` prefix (required) + - `type` - Optional instance type identifier + - `init` - Optional init function with `$code:` prefix + + **Rules:** + - Use `package` OR `code`, never both (CLI validates this) + - `config` stays separate from `code` + - `$code:` prefix outputs raw JavaScript at bundle time + +### Patch Changes + +- fdf6e7b: Add transformer support to CLI bundler + - Detect and bundle transformer packages from flow.json configuration + - Support transformer chaining via `next` field + - Handle `$code:` prefix for inline JavaScript in transformer config + - Generate proper import statements and config objects for transformers + - Document transformer configuration in flow.json + +- Updated dependencies [f39d9fb] +- Updated dependencies [888bbdf] + - @walkeros/core@1.2.0 + - @walkeros/server-core@1.0.2 + ## 1.0.2 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 86964c780..98b0e83a2 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@walkeros/cli", - "version": "1.0.2", + "version": "1.1.0", "description": "walkerOS CLI - Bundle and deploy walkerOS components", "license": "MIT", "type": "module", @@ -33,8 +33,8 @@ "docker:publish": "bash scripts/publish-docker.sh" }, "dependencies": { - "@walkeros/core": "1.1.0", - "@walkeros/server-core": "1.0.1", + "@walkeros/core": "1.2.0", + "@walkeros/server-core": "1.0.2", "chalk": "^5.6.2", "commander": "^14.0.2", "cors": "^2.8.5", diff --git a/packages/cli/src/__tests__/bundle/bundler.test.ts b/packages/cli/src/__tests__/bundle/bundler.test.ts index d7df3b19b..aeebc3caa 100644 --- a/packages/cli/src/__tests__/bundle/bundler.test.ts +++ b/packages/cli/src/__tests__/bundle/bundler.test.ts @@ -5,6 +5,8 @@ import { buildConfigObject, generatePlatformWrapper, createEntryPoint, + detectTransformerPackages, + detectExplicitCodeImports, } from '../../commands/bundle/bundler.js'; import { loadBundleConfig } from '../../config/index.js'; import { createLogger, type Logger } from '../../core/index.js'; @@ -813,4 +815,234 @@ describe('Bundler', () => { expect(result).toContain('createCollector'); }); }); + + describe('detectTransformerPackages', () => { + it('detects transformer packages from flow config', () => { + const flowConfig: Flow.Config = { + server: {}, + sources: {}, + destinations: {}, + transformers: { + fingerprint: { + package: '@walkeros/server-transformer-fingerprint', + code: 'transformerFingerprint', + config: { settings: { output: 'user.hash' } }, + }, + validate: { + package: '@walkeros/transformer-validator', + }, + }, + }; + + const result = detectTransformerPackages(flowConfig); + + expect(result).toEqual( + new Set([ + '@walkeros/server-transformer-fingerprint', + '@walkeros/transformer-validator', + ]), + ); + }); + + it('returns empty set when no transformers', () => { + const flowConfig: Flow.Config = { + server: {}, + sources: {}, + destinations: {}, + }; + + const result = detectTransformerPackages(flowConfig); + + expect(result).toEqual(new Set()); + }); + }); + + describe('detectExplicitCodeImports', () => { + it('detects explicit code imports from transformers', () => { + const flowConfig: Flow.Config = { + server: {}, + sources: {}, + destinations: {}, + transformers: { + fingerprint: { + package: '@walkeros/server-transformer-fingerprint', + code: 'transformerFingerprint', + config: {}, + }, + }, + }; + + const result = detectExplicitCodeImports(flowConfig); + + expect(result.get('@walkeros/server-transformer-fingerprint')).toEqual( + new Set(['transformerFingerprint']), + ); + }); + }); + + describe('transformer support', () => { + it('includes transformers in config object', () => { + const flowConfig: Flow.Config = { + server: {}, + sources: {}, + destinations: {}, + transformers: { + fingerprint: { + package: '@walkeros/server-transformer-fingerprint', + code: 'transformerFingerprint', + config: { + settings: { + fields: ['ingest.ip', 'ingest.userAgent'], + output: 'user.hash', + }, + }, + }, + }, + }; + + const explicitCodeImports = new Map([ + [ + '@walkeros/server-transformer-fingerprint', + new Set(['transformerFingerprint']), + ], + ]); + + const result = buildConfigObject(flowConfig, explicitCodeImports); + + expect(result).toContain('transformers:'); + expect(result).toContain('fingerprint:'); + expect(result).toContain('code: transformerFingerprint'); + expect(result).toContain('"output": "user.hash"'); + }); + + it('handles transformer next field in config', () => { + const flowConfig: Flow.Config = { + server: {}, + sources: {}, + destinations: {}, + transformers: { + enrich: { + package: '@walkeros/transformer-enricher', + code: 'transformerEnrich', + config: { apiUrl: 'https://api.example.com' }, + next: 'validate', + }, + validate: { + package: '@walkeros/transformer-validator', + code: 'transformerValidator', + config: {}, + }, + }, + }; + + const explicitCodeImports = new Map([ + ['@walkeros/transformer-enricher', new Set(['transformerEnrich'])], + ['@walkeros/transformer-validator', new Set(['transformerValidator'])], + ]); + + const result = buildConfigObject(flowConfig, explicitCodeImports); + + // next should be inside config for runtime + expect(result).toContain('"next": "validate"'); + }); + + it('handles $code: prefix in transformer config', () => { + const flowConfig: Flow.Config = { + server: {}, + sources: {}, + destinations: {}, + transformers: { + fingerprint: { + package: '@walkeros/server-transformer-fingerprint', + code: 'transformerFingerprint', + config: { + settings: { + fields: [ + { fn: '$code:() => new Date().getDate()' }, + 'ingest.ip', + ], + }, + }, + }, + }, + }; + + const explicitCodeImports = new Map([ + [ + '@walkeros/server-transformer-fingerprint', + new Set(['transformerFingerprint']), + ], + ]); + + const result = buildConfigObject(flowConfig, explicitCodeImports); + + expect(result).toContain('"fn": () => new Date().getDate()'); + expect(result).not.toContain('$code:'); + }); + }); + + describe('full flow with transformers', () => { + it('generates complete config with source -> transformer -> destination chain', () => { + const flowConfig: Flow.Config = { + server: {}, + sources: { + express: { + package: '@walkeros/server-source-express', + code: 'sourceExpress', + config: { settings: { port: 8080 } }, + next: 'fingerprint', + }, + }, + transformers: { + fingerprint: { + package: '@walkeros/server-transformer-fingerprint', + code: 'transformerFingerprint', + config: { + settings: { + fields: [ + { fn: '$code:() => new Date().getDate()' }, + 'ingest.ip', + ], + output: 'user.hash', + }, + }, + }, + }, + destinations: { + bigquery: { + package: '@walkeros/server-destination-bigquery', + code: 'destinationBigQuery', + config: { settings: { projectId: 'my-project' } }, + }, + }, + }; + + const explicitCodeImports = new Map([ + ['@walkeros/server-source-express', new Set(['sourceExpress'])], + [ + '@walkeros/server-transformer-fingerprint', + new Set(['transformerFingerprint']), + ], + [ + '@walkeros/server-destination-bigquery', + new Set(['destinationBigQuery']), + ], + ]); + + const result = buildConfigObject(flowConfig, explicitCodeImports); + + // Sources + expect(result).toContain('sources:'); + expect(result).toContain('code: sourceExpress'); + + // Transformers + expect(result).toContain('transformers:'); + expect(result).toContain('code: transformerFingerprint'); + expect(result).toContain('"fn": () => new Date().getDate()'); + + // Destinations + expect(result).toContain('destinations:'); + expect(result).toContain('code: destinationBigQuery'); + }); + }); }); diff --git a/packages/cli/src/commands/bundle/__tests__/inline-code.test.ts b/packages/cli/src/commands/bundle/__tests__/inline-code.test.ts new file mode 100644 index 000000000..742a2182e --- /dev/null +++ b/packages/cli/src/commands/bundle/__tests__/inline-code.test.ts @@ -0,0 +1,206 @@ +import { describe, it, expect } from '@jest/globals'; +import { buildConfigObject } from '../bundler.js'; +import type { Flow } from '@walkeros/core'; + +describe('Validation', () => { + it('should error when both package and code are specified', () => { + const flowConfig: Flow.Config = { + server: {}, + transformers: { + invalid: { + package: '@walkeros/transformer-validator', + code: { + type: 'inline', + push: '$code:(e) => e', + }, + }, + }, + }; + + const explicitCodeImports = new Map>(); + expect(() => buildConfigObject(flowConfig, explicitCodeImports)).toThrow( + /both package and code/i, + ); + }); + + it('should error when neither package nor code are specified', () => { + const flowConfig: Flow.Config = { + server: {}, + transformers: { + invalid: { + config: {}, + }, + }, + }; + + const explicitCodeImports = new Map>(); + expect(() => buildConfigObject(flowConfig, explicitCodeImports)).toThrow( + /package or code/i, + ); + }); +}); + +describe('Inline Code Bundling', () => { + describe('Transformer with code object', () => { + it('should generate inline transformer from code object', () => { + const flowConfig: Flow.Config = { + server: {}, + transformers: { + enrich: { + code: { + type: 'enricher', + push: '$code:(event) => ({ ...event, data: { ...event.data, enriched: true } })', + }, + config: {}, + }, + }, + }; + + // buildConfigObject takes flowConfig and explicitCodeImports map + const explicitCodeImports = new Map>(); + const result = buildConfigObject(flowConfig, explicitCodeImports); + + // Should contain the inline transformer + expect(result).toContain('enrich'); + expect(result).toContain('enricher'); + expect(result).toContain('enriched: true'); + // Should NOT have package import reference (since there's no package) + expect(result).not.toContain('@walkeros/transformer'); + }); + + it('should handle inline transformer with init function', () => { + const flowConfig: Flow.Config = { + server: {}, + transformers: { + validator: { + code: { + type: 'validator', + push: '$code:(event) => event.data?.valid ? event : null', + init: '$code:() => console.log("Validator initialized")', + }, + config: { strict: true }, + }, + }, + }; + + const explicitCodeImports = new Map>(); + const result = buildConfigObject(flowConfig, explicitCodeImports); + + // Should contain the inline transformer with init + expect(result).toContain('validator'); + expect(result).toContain('init'); + expect(result).toContain('push'); + }); + }); + + describe('Source with code object', () => { + it('should generate inline source from code object', () => { + const flowConfig: Flow.Config = { + server: {}, + sources: { + customSource: { + code: { + type: 'logger', + push: '$code:(event) => console.log("Event:", event.name)', + }, + config: {}, + }, + }, + destinations: {}, + }; + + const explicitCodeImports = new Map>(); + const result = buildConfigObject(flowConfig, explicitCodeImports); + + // Should contain the inline source + expect(result).toContain('customSource'); + expect(result).toContain('logger'); + }); + }); + + describe('Destination with code object', () => { + it('should generate inline destination from code object', () => { + const flowConfig: Flow.Config = { + server: {}, + sources: {}, + destinations: { + customDest: { + code: { + type: 'logger', + push: '$code:(event) => console.log("Sending:", event)', + }, + config: {}, + }, + }, + }; + + const explicitCodeImports = new Map>(); + const result = buildConfigObject(flowConfig, explicitCodeImports); + + // Should contain the inline destination + expect(result).toContain('customDest'); + expect(result).toContain('logger'); + }); + }); +}); + +describe('Integration', () => { + it('should bundle mixed package and inline definitions', () => { + const flowConfig: Flow.Config = { + server: {}, + packages: { + '@walkeros/collector': { imports: ['startFlow'] }, + '@walkeros/transformer-validator': { + imports: ['transformerValidator'], + }, + }, + sources: { + manual: { + code: { + type: 'manual', + push: '$code:context.env.elb', + }, + config: {}, + }, + }, + transformers: { + validate: { + package: '@walkeros/transformer-validator', + }, + enrich: { + code: { + type: 'enricher', + push: '$code:(event) => ({ ...event, data: { ...event.data, enriched: true } })', + }, + next: 'validate', + config: {}, + }, + }, + destinations: { + logger: { + code: { + type: 'console', + push: '$code:(event, ctx) => ctx.logger.info(event)', + }, + config: {}, + }, + }, + }; + + const explicitCodeImports = new Map>(); + const result = buildConfigObject(flowConfig, explicitCodeImports); + + // Package-based transformer should reference the import variable + expect(result).toContain('validate'); + expect(result).toContain('_walkerosTransformerValidator'); + + // Inline code should be present + expect(result).toContain('manual'); + expect(result).toContain('enricher'); + expect(result).toContain('console'); + expect(result).toContain('enriched: true'); + + // Transformer chaining should be preserved + expect(result).toContain('next'); + }); +}); diff --git a/packages/cli/src/commands/bundle/bundler.ts b/packages/cli/src/commands/bundle/bundler.ts index 3048ee8fd..ae58383b9 100644 --- a/packages/cli/src/commands/bundle/bundler.ts +++ b/packages/cli/src/commands/bundle/bundler.ts @@ -3,6 +3,91 @@ import path from 'path'; import fs from 'fs-extra'; import type { Flow } from '@walkeros/core'; import { packageNameToVariable } from '@walkeros/core'; + +/** + * Type guard to check if a code value is an InlineCode object. + * InlineCode has { push: string, type?: string, init?: string } + */ +function isInlineCode(code: unknown): code is Flow.InlineCode { + return ( + code !== null && + typeof code === 'object' && + !Array.isArray(code) && + 'push' in code + ); +} + +/** + * Validates that a reference has either package XOR code, not both or neither. + * Throws descriptive error for invalid configurations. + */ +function validateReference( + type: string, + name: string, + ref: { package?: string; code?: unknown }, +): void { + const hasPackage = !!ref.package; + const hasCode = isInlineCode(ref.code); + + if (hasPackage && hasCode) { + throw new Error( + `${type} "${name}": Cannot specify both package and code. Use one or the other.`, + ); + } + if (!hasPackage && !hasCode) { + throw new Error(`${type} "${name}": Must specify either package or code.`); + } +} + +/** + * Generate inline code for sources and transformers. + * Creates a factory function that returns the instance at runtime. + */ +function generateInlineCode( + inline: Flow.InlineCode, + config: object, + env?: object, +): string { + const pushFn = inline.push.replace('$code:', ''); + const initFn = inline.init ? inline.init.replace('$code:', '') : undefined; + const typeLine = inline.type ? `type: '${inline.type}',` : ''; + + return `{ + code: async (context) => ({ + ${typeLine} + config: context.config, + ${initFn ? `init: ${initFn},` : ''} + push: ${pushFn} + }), + config: ${JSON.stringify(config || {})}, + env: ${JSON.stringify(env || {})} + }`; +} + +/** + * Generate inline code for destinations. + * Destinations have a different structure - code is the instance directly. + */ +function generateInlineDestinationCode( + inline: Flow.InlineCode, + config: object, + env?: object, +): string { + const pushFn = inline.push.replace('$code:', ''); + const initFn = inline.init ? inline.init.replace('$code:', '') : undefined; + const typeLine = inline.type ? `type: '${inline.type}',` : ''; + + return `{ + code: { + ${typeLine} + config: ${JSON.stringify(config || {})}, + ${initFn ? `init: ${initFn},` : ''} + push: ${pushFn} + }, + config: ${JSON.stringify(config || {})}, + env: ${JSON.stringify(env || {})} + }`; +} import type { BuildOptions } from '../../types/bundle.js'; import { downloadPackages } from './package-manager.js'; import type { Logger } from '../../core/index.js'; @@ -66,6 +151,69 @@ function generateCacheKeyContent( return JSON.stringify(configForCache); } +/** + * Validates flow config and warns about deprecated features. + * Returns true if there are any issues that should stop the build. + * + * Note: We use (code as unknown) === true to check for deprecated code: true + * because the type no longer includes true, but runtime values may still have it. + */ +function validateFlowConfig(flowConfig: Flow.Config, logger: Logger): boolean { + let hasDeprecatedCodeTrue = false; + + // Check sources for code: true (deprecated, removed from types) + const sources = flowConfig.sources || {}; + for (const [sourceId, source] of Object.entries(sources)) { + if ( + source && + typeof source === 'object' && + (source.code as unknown) === true + ) { + logger.warning( + `DEPRECATED: Source "${sourceId}" uses code: true which is no longer supported. ` + + `Use $code: prefix in config values or create a source package instead.`, + ); + hasDeprecatedCodeTrue = true; + } + } + + // Check destinations for code: true (deprecated, removed from types) + const destinations = flowConfig.destinations || {}; + for (const [destId, dest] of Object.entries(destinations)) { + if (dest && typeof dest === 'object' && (dest.code as unknown) === true) { + logger.warning( + `DEPRECATED: Destination "${destId}" uses code: true which is no longer supported. ` + + `Use $code: prefix in config values or create a destination package instead.`, + ); + hasDeprecatedCodeTrue = true; + } + } + + // Check transformers for code: true (deprecated, removed from types) + const transformers = flowConfig.transformers || {}; + for (const [transformerId, transformer] of Object.entries(transformers)) { + if ( + transformer && + typeof transformer === 'object' && + (transformer.code as unknown) === true + ) { + logger.warning( + `DEPRECATED: Transformer "${transformerId}" uses code: true which is no longer supported. ` + + `Use $code: prefix in config values or create a transformer package instead.`, + ); + hasDeprecatedCodeTrue = true; + } + } + + if (hasDeprecatedCodeTrue) { + logger.warning( + `See https://www.elbwalker.com/docs/walkeros/getting-started/flow for migration guide.`, + ); + } + + return hasDeprecatedCodeTrue; +} + export async function bundleCore( flowConfig: Flow.Config, buildOptions: BuildOptions, @@ -73,6 +221,13 @@ export async function bundleCore( showStats = false, ): Promise { const bundleStartTime = Date.now(); + + // Validate flow config and warn about deprecated features + const hasDeprecatedFeatures = validateFlowConfig(flowConfig, logger); + if (hasDeprecatedFeatures) { + logger.warning('Skipping deprecated code: true entries from bundle.'); + } + // Use provided temp dir or default .tmp/ const TEMP_DIR = buildOptions.tempDir || getTmpPath(); @@ -420,6 +575,15 @@ function detectDestinationPackages(flowConfig: Flow.Config): Set { if (destinations) { for (const [destKey, destConfig] of Object.entries(destinations)) { + // Skip if code: true (uses built-in inline code destination) + if ( + typeof destConfig === 'object' && + destConfig !== null && + 'code' in destConfig && + destConfig.code === true + ) { + continue; + } // Require explicit package field - no inference for any packages if ( typeof destConfig === 'object' && @@ -448,6 +612,15 @@ function detectSourcePackages(flowConfig: Flow.Config): Set { if (sources) { for (const [sourceKey, sourceConfig] of Object.entries(sources)) { + // Skip if code: true (uses built-in inline code source) + if ( + typeof sourceConfig === 'object' && + sourceConfig !== null && + 'code' in sourceConfig && + sourceConfig.code === true + ) { + continue; + } // Require explicit package field - no inference for any packages if ( typeof sourceConfig === 'object' && @@ -464,10 +637,50 @@ function detectSourcePackages(flowConfig: Flow.Config): Set { } /** - * Detects explicit code imports from destinations and sources. + * Detects transformer packages from flow configuration. + * Extracts package names from transformers that have explicit 'package' field. + */ +export function detectTransformerPackages( + flowConfig: Flow.Config, +): Set { + const transformerPackages = new Set(); + const transformers = ( + flowConfig as unknown as { transformers?: Record } + ).transformers; + + if (transformers) { + for (const [transformerKey, transformerConfig] of Object.entries( + transformers, + )) { + // Skip if code: true (uses built-in inline code transformer) + if ( + typeof transformerConfig === 'object' && + transformerConfig !== null && + 'code' in transformerConfig && + transformerConfig.code === true + ) { + continue; + } + // Require explicit package field + if ( + typeof transformerConfig === 'object' && + transformerConfig !== null && + 'package' in transformerConfig && + typeof transformerConfig.package === 'string' + ) { + transformerPackages.add(transformerConfig.package); + } + } + } + + return transformerPackages; +} + +/** + * Detects explicit code imports from destinations, sources, and transformers. * Returns a map of package names to sets of export names. */ -function detectExplicitCodeImports( +export function detectExplicitCodeImports( flowConfig: Flow.Config, ): Map> { const explicitCodeImports = new Map>(); @@ -479,6 +692,15 @@ function detectExplicitCodeImports( if (destinations) { for (const [destKey, destConfig] of Object.entries(destinations)) { + // Skip code: true (built-in inline code) + if ( + typeof destConfig === 'object' && + destConfig !== null && + 'code' in destConfig && + destConfig.code === true + ) { + continue; + } if ( typeof destConfig === 'object' && destConfig !== null && @@ -507,6 +729,15 @@ function detectExplicitCodeImports( if (sources) { for (const [sourceKey, sourceConfig] of Object.entries(sources)) { + // Skip code: true (built-in inline code) + if ( + typeof sourceConfig === 'object' && + sourceConfig !== null && + 'code' in sourceConfig && + sourceConfig.code === true + ) { + continue; + } if ( typeof sourceConfig === 'object' && sourceConfig !== null && @@ -528,6 +759,46 @@ function detectExplicitCodeImports( } } + // Check transformers + const transformers = ( + flowConfig as unknown as { transformers?: Record } + ).transformers; + + if (transformers) { + for (const [transformerKey, transformerConfig] of Object.entries( + transformers, + )) { + // Skip code: true (built-in inline code) + if ( + typeof transformerConfig === 'object' && + transformerConfig !== null && + 'code' in transformerConfig && + transformerConfig.code === true + ) { + continue; + } + if ( + typeof transformerConfig === 'object' && + transformerConfig !== null && + 'package' in transformerConfig && + typeof transformerConfig.package === 'string' && + 'code' in transformerConfig && + typeof transformerConfig.code === 'string' + ) { + // Only treat as explicit if code doesn't match auto-generated pattern + const isAutoGenerated = transformerConfig.code.startsWith('_'); + if (!isAutoGenerated) { + if (!explicitCodeImports.has(transformerConfig.package)) { + explicitCodeImports.set(transformerConfig.package, new Set()); + } + explicitCodeImports + .get(transformerConfig.package)! + .add(transformerConfig.code); + } + } + } + } + return explicitCodeImports; } @@ -544,11 +815,16 @@ function generateImportStatements( packages: BuildOptions['packages'], destinationPackages: Set, sourcePackages: Set, + transformerPackages: Set, explicitCodeImports: Map>, ): ImportGenerationResult { const importStatements: string[] = []; const examplesMappings: string[] = []; - const usedPackages = new Set([...destinationPackages, ...sourcePackages]); + const usedPackages = new Set([ + ...destinationPackages, + ...sourcePackages, + ...transformerPackages, + ]); for (const [packageName, packageConfig] of Object.entries(packages)) { const isUsedByDestOrSource = usedPackages.has(packageName); @@ -639,9 +915,10 @@ export async function createEntryPoint( buildOptions: BuildOptions, packagePaths: Map, ): Promise { - // Detect packages used by destinations and sources + // Detect packages used by destinations, sources, and transformers const destinationPackages = detectDestinationPackages(flowConfig); const sourcePackages = detectSourcePackages(flowConfig); + const transformerPackages = detectTransformerPackages(flowConfig); const explicitCodeImports = detectExplicitCodeImports(flowConfig); // Generate import statements @@ -649,11 +926,18 @@ export async function createEntryPoint( buildOptions.packages, destinationPackages, sourcePackages, + transformerPackages, explicitCodeImports, ); const importsCode = importStatements.join('\n'); - const hasFlow = destinationPackages.size > 0 || sourcePackages.size > 0; + const hasFlow = + Object.values(flowConfig.sources || {}).some( + (s) => s.package || isInlineCode(s.code), + ) || + Object.values(flowConfig.destinations || {}).some( + (d) => d.package || isInlineCode(d.code), + ); // If no sources/destinations, just return user code with imports (no flow wrapper) if (!hasFlow) { @@ -734,42 +1018,117 @@ export function buildConfigObject( const flowWithProps = flowConfig as unknown as { sources?: Record< string, - { package: string; code?: string; config?: unknown; env?: unknown } + { + package?: string; + code?: string | true; + config?: unknown; + env?: unknown; + } >; destinations?: Record< string, - { package: string; code?: string; config?: unknown; env?: unknown } + { + package?: string; + code?: string | true; + config?: unknown; + env?: unknown; + } + >; + transformers?: Record< + string, + { + package?: string; + code?: string | true; + config?: unknown; + env?: unknown; + next?: string; + } >; collector?: unknown; }; const sources = flowWithProps.sources || {}; const destinations = flowWithProps.destinations || {}; + const transformers = flowWithProps.transformers || {}; - // Build sources - const sourcesEntries = Object.entries(sources).map(([key, source]) => { - const hasExplicitCode = - source.code && explicitCodeImports.has(source.package); - const codeVar = hasExplicitCode - ? source.code - : packageNameToVariable(source.package); - - const configStr = source.config ? processConfigValue(source.config) : '{}'; - const envStr = source.env - ? `,\n env: ${processConfigValue(source.env)}` - : ''; + // Validate references before processing (skip deprecated code: true entries) + Object.entries(sources).forEach(([name, source]) => { + if ((source.code as unknown) !== true) { + validateReference('Source', name, source); + } + }); - return ` ${key}: {\n code: ${codeVar},\n config: ${configStr}${envStr}\n }`; + Object.entries(destinations).forEach(([name, dest]) => { + if ((dest.code as unknown) !== true) { + validateReference('Destination', name, dest); + } }); - // Build destinations - const destinationsEntries = Object.entries(destinations).map( - ([key, dest]) => { - const hasExplicitCode = - dest.code && explicitCodeImports.has(dest.package); - const codeVar = hasExplicitCode - ? dest.code - : packageNameToVariable(dest.package); + Object.entries(transformers).forEach(([name, transformer]) => { + if ((transformer.code as unknown) !== true) { + validateReference('Transformer', name, transformer); + } + }); + + // Build sources (skip deprecated code: true entries) + const sourcesEntries = Object.entries(sources) + .filter( + ([, source]) => + (source.code as unknown) !== true && + (source.package || isInlineCode(source.code)), + ) + .map(([key, source]) => { + // Handle inline code object + if (isInlineCode(source.code)) { + return ` ${key}: ${generateInlineCode(source.code, (source.config as object) || {}, source.env as object)}`; + } + + // Handle package-based source + let codeVar: string; + if ( + source.code && + typeof source.code === 'string' && + explicitCodeImports.has(source.package!) + ) { + codeVar = source.code; + } else { + codeVar = packageNameToVariable(source.package!); + } + + const configStr = source.config + ? processConfigValue(source.config) + : '{}'; + const envStr = source.env + ? `,\n env: ${processConfigValue(source.env)}` + : ''; + + return ` ${key}: {\n code: ${codeVar},\n config: ${configStr}${envStr}\n }`; + }); + + // Build destinations (skip deprecated code: true entries) + const destinationsEntries = Object.entries(destinations) + .filter( + ([, dest]) => + (dest.code as unknown) !== true && + (dest.package || isInlineCode(dest.code)), + ) + .map(([key, dest]) => { + // Handle inline code object + if (isInlineCode(dest.code)) { + return ` ${key}: ${generateInlineDestinationCode(dest.code, (dest.config as object) || {}, dest.env as object)}`; + } + + // Handle package-based destination + let codeVar: string; + if ( + dest.code && + typeof dest.code === 'string' && + explicitCodeImports.has(dest.package!) + ) { + codeVar = dest.code; + } else { + codeVar = packageNameToVariable(dest.package!); + } const configStr = dest.config ? processConfigValue(dest.config) : '{}'; const envStr = dest.env @@ -777,21 +1136,73 @@ export function buildConfigObject( : ''; return ` ${key}: {\n code: ${codeVar},\n config: ${configStr}${envStr}\n }`; - }, - ); + }); + + // Build transformers (skip deprecated code: true entries) + const transformersEntries = Object.entries(transformers) + .filter( + ([, transformer]) => + (transformer.code as unknown) !== true && + (transformer.package || isInlineCode(transformer.code)), + ) + .map(([key, transformer]) => { + // Handle inline code object + if (isInlineCode(transformer.code)) { + // Merge next into config for runtime if present + const configWithNext = transformer.next + ? { + ...((transformer.config as object) || {}), + next: transformer.next, + } + : (transformer.config as object) || {}; + return ` ${key}: ${generateInlineCode(transformer.code, configWithNext, transformer.env as object)}`; + } + + // Handle package-based transformer + let codeVar: string; + if ( + transformer.code && + typeof transformer.code === 'string' && + explicitCodeImports.has(transformer.package!) + ) { + codeVar = transformer.code; + } else { + codeVar = packageNameToVariable(transformer.package!); + } + + // Merge next into config for runtime (transformer.config.next) + const configWithNext = transformer.next + ? { ...((transformer.config as object) || {}), next: transformer.next } + : transformer.config; + + const configStr = configWithNext + ? processConfigValue(configWithNext) + : '{}'; + const envStr = transformer.env + ? `,\n env: ${processConfigValue(transformer.env)}` + : ''; + + return ` ${key}: {\n code: ${codeVar},\n config: ${configStr}${envStr}\n }`; + }); // Build collector const collectorStr = flowWithProps.collector ? `,\n ...${processConfigValue(flowWithProps.collector)}` : ''; + // Build transformers section (only if transformers exist) + const transformersStr = + transformersEntries.length > 0 + ? `,\n transformers: {\n${transformersEntries.join(',\n')}\n }` + : ''; + return `{ sources: { ${sourcesEntries.join(',\n')} }, destinations: { ${destinationsEntries.join(',\n')} - }${collectorStr} + }${transformersStr}${collectorStr} }`; } diff --git a/packages/collector/CHANGELOG.md b/packages/collector/CHANGELOG.md index a11671102..6d77d9ede 100644 --- a/packages/collector/CHANGELOG.md +++ b/packages/collector/CHANGELOG.md @@ -1,5 +1,80 @@ # @walkeros/collector +## 1.1.0 + +### Minor Changes + +- f39d9fb: Add array support for transformer chain configuration + + Enables explicit control over transformer chain order by accepting arrays for + `next` and `before` properties, bypassing automatic chain resolution. + + **Array chain behavior:** + + | Syntax | Behavior | + | -------------------------------- | ------------------------------------------------------ | + | `"next": "validate"` | Walks chain via each transformer's `next` property | + | `"next": ["validate", "enrich"]` | Uses exact order specified, ignores transformer `next` | + + **Example:** + + ```json + { + "sources": { + "http": { + "package": "@walkeros/server-source-express", + "next": ["validate", "enrich", "redact"] + } + }, + "destinations": { + "analytics": { + "package": "@walkeros/server-destination-gcp", + "before": ["format", "anonymize"] + } + } + } + ``` + + When walking a chain encounters an array `next`, it appends all items and + stops (does not recursively resolve those transformers' `next` properties). + +- 888bbdf: Add inline code syntax for sources, transformers, and destinations + + Enables defining custom logic directly in flow.json using `code` objects + instead of requiring external packages. This is ideal for simple one-liner + transformations. + + **Example:** + + ```json + { + "transformers": { + "enrich": { + "code": { + "push": "$code:(event) => ({ ...event, data: { ...event.data, enriched: true } })" + }, + "config": {} + } + } + } + ``` + + **Code object properties:** + - `push` - The push function with `$code:` prefix (required) + - `type` - Optional instance type identifier + - `init` - Optional init function with `$code:` prefix + + **Rules:** + - Use `package` OR `code`, never both (CLI validates this) + - `config` stays separate from `code` + - `$code:` prefix outputs raw JavaScript at bundle time + +### Patch Changes + +- Updated dependencies [f39d9fb] +- Updated dependencies [888bbdf] + - @walkeros/core@1.2.0 + ## 1.0.1 ### Patch Changes diff --git a/packages/collector/package.json b/packages/collector/package.json index a0946dd80..f5863fd7b 100644 --- a/packages/collector/package.json +++ b/packages/collector/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/collector", "description": "Unified platform-agnostic collector for walkerOS", - "version": "1.0.1", + "version": "1.1.0", "main": "./dist/index.js", "module": "./dist/index.mjs", "types": "./dist/index.d.ts", @@ -30,7 +30,7 @@ "update": "npx npm-check-updates -u && npm update" }, "dependencies": { - "@walkeros/core": "1.1.0" + "@walkeros/core": "1.2.0" }, "devDependencies": {}, "repository": { diff --git a/packages/collector/src/__tests__/destination-code.test.ts b/packages/collector/src/__tests__/destination-code.test.ts deleted file mode 100644 index 25b8e4eb1..000000000 --- a/packages/collector/src/__tests__/destination-code.test.ts +++ /dev/null @@ -1,499 +0,0 @@ -import type { Collector, Destination } from '@walkeros/core'; -import { createEvent, createMockLogger } from '@walkeros/core'; -import { destinationCode } from '../destination-code'; -import { initDestinations } from '../destination'; -import type { - Settings, - CodeMapping, - PushContext, - PushBatchContext, - Context, -} from '../types/code'; - -describe('destinationCode', () => { - const createMockCollector = (): Collector.Instance => - ({ - consent: {}, - destinations: {}, - sources: {}, - queue: [], - hooks: {}, - on: {}, - globals: {}, - user: {}, - allowed: true, - config: {}, - count: 0, - logger: createMockLogger(), - push: jest.fn(), - }) as unknown as Collector.Instance; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('basic properties', () => { - it('should have correct type', () => { - expect(destinationCode.type).toBe('code'); - }); - - it('should have empty default config', () => { - expect(destinationCode.config).toEqual({}); - }); - }); - - describe('init', () => { - it('accepts scripts array in settings', () => { - const settings: Settings = { - scripts: [ - 'https://example.com/analytics.js', - 'https://example.com/pixel.js', - ], - init: "console.log('ready')", - }; - expect(settings.scripts).toHaveLength(2); - }); - - it('injects script tags for each URL in scripts array', () => { - const initialScriptCount = - document.head.querySelectorAll('script').length; - - const context: Context = { - collector: createMockCollector(), - config: { - settings: { - scripts: ['https://example.com/a.js', 'https://example.com/b.js'], - }, - }, - env: {}, - logger: createMockLogger(), - id: 'test', - }; - - destinationCode.init!(context); - - const scripts = document.head.querySelectorAll('script'); - expect(scripts.length).toBe(initialScriptCount + 2); - - const addedScripts = Array.from(scripts).slice(-2); - expect(addedScripts[0].src).toBe('https://example.com/a.js'); - expect(addedScripts[0].async).toBe(true); - expect(addedScripts[1].src).toBe('https://example.com/b.js'); - expect(addedScripts[1].async).toBe(true); - }); - - it('injects scripts before running init code', () => { - const initialScriptCount = - document.head.querySelectorAll('script').length; - const mockLogger = createMockLogger(); - - const context: Context = { - collector: createMockCollector(), - config: { - settings: { - scripts: ['https://example.com/lib.js'], - init: "context.logger.info('init ran')", - }, - }, - env: {}, - logger: mockLogger, - id: 'test', - }; - - destinationCode.init!(context); - - // Scripts should be injected - const scripts = document.head.querySelectorAll('script'); - expect(scripts.length).toBe(initialScriptCount + 1); - expect(Array.from(scripts).pop()?.src).toBe('https://example.com/lib.js'); - - // Init code should also run - expect(mockLogger.info).toHaveBeenCalledWith('init ran'); - }); - - it('executes init code string', () => { - const mockLogger = createMockLogger(); - const context: Context = { - collector: createMockCollector(), - config: { - settings: { - init: "context.logger.info('initialized')", - }, - }, - env: {}, - logger: mockLogger, - id: 'test', - }; - - destinationCode.init!(context); - - expect(mockLogger.info).toHaveBeenCalledWith('initialized'); - }); - - it('handles empty scripts array gracefully', () => { - const initialScriptCount = - document.head.querySelectorAll('script').length; - const mockLogger = createMockLogger(); - const context: Context = { - collector: createMockCollector(), - config: { - settings: { - scripts: [], - init: "context.logger.info('init ran')", - }, - }, - env: {}, - logger: mockLogger, - id: 'test', - }; - - expect(() => destinationCode.init!(context)).not.toThrow(); - - // No scripts should be added - expect(document.head.querySelectorAll('script').length).toBe( - initialScriptCount, - ); - - // Init code should still run - expect(mockLogger.info).toHaveBeenCalledWith('init ran'); - }); - - it('handles missing init code gracefully', () => { - const context: Context = { - collector: createMockCollector(), - config: { settings: {} }, - env: {}, - logger: createMockLogger(), - id: 'test', - }; - - expect(() => destinationCode.init!(context)).not.toThrow(); - }); - - it('catches and logs errors in init code', () => { - const mockLogger = createMockLogger(); - const context: Context = { - collector: createMockCollector(), - config: { - settings: { - init: "throw new Error('test error')", - }, - }, - env: {}, - logger: mockLogger, - id: 'test', - }; - - destinationCode.init!(context); - - expect(mockLogger.error).toHaveBeenCalled(); - }); - }); - - describe('push', () => { - it('executes push code from mapping', () => { - const mockLogger = createMockLogger(); - const context: PushContext = { - collector: createMockCollector(), - config: {}, - data: { transformed: true }, - env: {}, - logger: mockLogger, - id: 'test', - rule: { - push: 'context.logger.info(event.name, context.data)', - } as CodeMapping, - }; - - destinationCode.push(createEvent({ name: 'product view' }), context); - - expect(mockLogger.info).toHaveBeenCalledWith('product view', { - transformed: true, - }); - }); - - it('falls back to settings.push when mapping.push is missing', () => { - const mockLogger = createMockLogger(); - const context: PushContext = { - collector: createMockCollector(), - config: { - settings: { - push: "context.logger.info('settings fallback')", - } as Settings, - }, - data: {}, - env: {}, - logger: mockLogger, - id: 'test', - rule: {}, - }; - - destinationCode.push(createEvent({ name: 'product view' }), context); - - expect(mockLogger.info).toHaveBeenCalledWith('settings fallback'); - }); - - it('prefers mapping.push over settings.push', () => { - const mockLogger = createMockLogger(); - const context: PushContext = { - collector: createMockCollector(), - config: { - settings: { - push: "context.logger.info('from settings')", - } as Settings, - }, - data: {}, - env: {}, - logger: mockLogger, - id: 'test', - rule: { - push: "context.logger.info('from mapping')", - } as CodeMapping, - }; - - destinationCode.push(createEvent({ name: 'product view' }), context); - - expect(mockLogger.info).toHaveBeenCalledWith('from mapping'); - expect(mockLogger.info).not.toHaveBeenCalledWith('from settings'); - }); - - it('handles missing push code gracefully', () => { - const context: PushContext = { - collector: createMockCollector(), - config: {}, - env: {}, - logger: createMockLogger(), - id: 'test', - rule: {}, - data: {}, - }; - - expect(() => - destinationCode.push(createEvent({ name: 'product view' }), context), - ).not.toThrow(); - }); - - it('catches and logs errors in push code', () => { - const mockLogger = createMockLogger(); - const context: PushContext = { - collector: createMockCollector(), - config: {}, - env: {}, - logger: mockLogger, - id: 'test', - rule: { - push: "throw new Error('test error')", - } as CodeMapping, - data: {}, - }; - - destinationCode.push(createEvent({ name: 'product view' }), context); - - expect(mockLogger.error).toHaveBeenCalled(); - }); - }); - - describe('pushBatch', () => { - it('executes pushBatch code from mapping', () => { - const mockLogger = createMockLogger(); - const batch: Destination.Batch = { - key: 'product view', - events: [ - createEvent({ name: 'product view', id: '1' }), - createEvent({ name: 'product view', id: '2' }), - ], - data: [{ id: '1' }, { id: '2' }], - }; - - const context: PushBatchContext = { - collector: createMockCollector(), - config: {}, - env: {}, - logger: mockLogger, - id: 'test', - rule: { - pushBatch: "context.logger.info('batch size:', batch.events.length)", - } as CodeMapping, - }; - - destinationCode.pushBatch!(batch, context); - - expect(mockLogger.info).toHaveBeenCalledWith('batch size:', 2); - }); - - it('falls back to settings.pushBatch when mapping.pushBatch is missing', () => { - const mockLogger = createMockLogger(); - const batch: Destination.Batch = { - key: 'test', - events: [], - data: [], - }; - - const context: PushBatchContext = { - collector: createMockCollector(), - config: { - settings: { - pushBatch: "context.logger.info('batch settings fallback')", - } as Settings, - }, - env: {}, - logger: mockLogger, - id: 'test', - rule: {}, - }; - - destinationCode.pushBatch!(batch, context); - - expect(mockLogger.info).toHaveBeenCalledWith('batch settings fallback'); - }); - - it('handles missing pushBatch code gracefully', () => { - const batch: Destination.Batch = { - key: 'test', - events: [], - data: [], - }; - - const context: PushBatchContext = { - collector: createMockCollector(), - config: {}, - env: {}, - logger: createMockLogger(), - id: 'test', - rule: {}, - }; - - expect(() => destinationCode.pushBatch!(batch, context)).not.toThrow(); - }); - - it('catches and logs errors in pushBatch code', () => { - const mockLogger = createMockLogger(); - const batch: Destination.Batch = { - key: 'test', - events: [], - data: [], - }; - - const context: PushBatchContext = { - collector: createMockCollector(), - config: {}, - env: {}, - logger: mockLogger, - id: 'test', - rule: { - pushBatch: "throw new Error('test error')", - } as CodeMapping, - }; - - destinationCode.pushBatch!(batch, context); - - expect(mockLogger.error).toHaveBeenCalled(); - }); - }); - - describe('on', () => { - it('executes on code string', () => { - const mockLogger = createMockLogger(); - const context: Context = { - collector: createMockCollector(), - config: { - settings: { - on: "if (type === 'consent') context.logger.info('consent:', context.data)", - } as Settings, - }, - data: { marketing: true }, - env: {}, - logger: mockLogger, - id: 'test', - }; - - destinationCode.on!('consent', context); - - expect(mockLogger.info).toHaveBeenCalledWith('consent:', { - marketing: true, - }); - }); - - it('handles missing on code gracefully', () => { - const context: Context = { - collector: createMockCollector(), - config: { settings: {} }, - env: {}, - logger: createMockLogger(), - id: 'test', - }; - - expect(() => destinationCode.on!('consent', context)).not.toThrow(); - }); - - it('catches and logs errors in on code', () => { - const mockLogger = createMockLogger(); - const context: Context = { - collector: createMockCollector(), - config: { - settings: { - on: "throw new Error('test error')", - } as Settings, - }, - env: {}, - logger: mockLogger, - id: 'test', - }; - - destinationCode.on!('consent', context); - - expect(mockLogger.error).toHaveBeenCalled(); - }); - }); -}); - -describe('code: true initialization', () => { - it('uses built-in destinationCode when code is true', async () => { - const collector = { - logger: createMockLogger(), - } as unknown as Collector.Instance; - - const destinations = await initDestinations(collector, { - myCodeDest: { - code: true as unknown as Destination.Instance, - config: { - settings: { - init: "context.logger.info('ready')", - }, - }, - }, - }); - - expect(destinations.myCodeDest).toBeDefined(); - expect(destinations.myCodeDest.type).toBe('code'); - expect(destinations.myCodeDest.init).toBeDefined(); - expect(destinations.myCodeDest.push).toBeDefined(); - }); - - it('preserves provided config with code: true', async () => { - const collector = { - logger: createMockLogger(), - } as unknown as Collector.Instance; - - const destinations = await initDestinations(collector, { - myCodeDest: { - code: true as unknown as Destination.Instance, - config: { - settings: { - init: "context.logger.info('custom init')", - push: "context.logger.info('custom push')", - }, - consent: { functional: true }, - }, - }, - }); - - expect(destinations.myCodeDest.config.settings).toEqual({ - init: "context.logger.info('custom init')", - push: "context.logger.info('custom push')", - }); - expect(destinations.myCodeDest.config.consent).toEqual({ - functional: true, - }); - }); -}); diff --git a/packages/collector/src/__tests__/inline-code.test.ts b/packages/collector/src/__tests__/inline-code.test.ts new file mode 100644 index 000000000..cc0867538 --- /dev/null +++ b/packages/collector/src/__tests__/inline-code.test.ts @@ -0,0 +1,440 @@ +import type { + Collector, + Destination, + Source, + Transformer, + WalkerOS, + Elb, +} from '@walkeros/core'; +import { createEvent, createMockLogger } from '@walkeros/core'; +import { startFlow } from '../flow'; +import { initSources } from '../source'; +import { initDestinations, pushToDestinations } from '../destination'; +import { initTransformers, runTransformerChain } from '../transformer'; + +/** + * Tests for inline function code support. + * + * These tests verify that inline functions work correctly for sources, + * destinations, and transformers. This is the mechanism that the $code: + * prefix uses at bundle time - it outputs raw JavaScript functions instead + * of strings, which are then used directly as the code parameter. + * + * The old `code: true` approach used `new Function()` at runtime, which + * is now deprecated. Users should use $code: prefix in flow.json instead. + */ +describe('Inline Code Support ($code: prefix equivalent)', () => { + // Helper to create a minimal collector for testing + function createTestCollector( + overrides: Partial = {}, + ): Collector.Instance { + const mockLogger = createMockLogger(); + return { + allowed: true, + config: { tagging: 1, globalsStatic: {}, sessionStatic: {} }, + consent: {}, + count: 0, + custom: {}, + destinations: {}, + transformers: {}, + transformerChain: { pre: [], post: {} }, + globals: {}, + group: '', + hooks: {}, + logger: mockLogger, + on: {}, + queue: [], + round: 0, + session: undefined, + timing: Date.now(), + user: {}, + version: '1.0.0', + sources: {}, + push: jest.fn(), + command: jest.fn(), + ...overrides, + } as unknown as Collector.Instance; + } + + describe('Source with inline code', () => { + let collector: Collector.Instance; + + beforeEach(async () => { + const result = await startFlow(); + collector = result.collector; + }); + + it('should initialize source with inline function code', async () => { + // This is what $code: produces at bundle time - a raw function + const inlineSourceCode: Source.Init = async (context) => { + const { config, env } = context; + return { + type: 'inline-source', + config: { + ...config, + settings: { initialized: true }, + } as Source.Config, + push: env.push as Elb.Fn, + }; + }; + + const sources = await initSources(collector, { + mySource: { + code: inlineSourceCode, + config: { settings: {} } as Source.Config, + env: {}, + }, + }); + + expect(sources).toHaveProperty('mySource'); + expect(sources.mySource.type).toBe('inline-source'); + expect(sources.mySource.config.settings).toEqual({ initialized: true }); + }); + + it('should provide push function to inline source', async () => { + let receivedPush: unknown; + + const inlineSourceCode: Source.Init = async (context) => { + const { config, env } = context; + receivedPush = env.push; + + return { + type: 'inline-push', + config: config as Source.Config, + push: env.push as Elb.Fn, + }; + }; + + await initSources(collector, { + pushSource: { + code: inlineSourceCode, + config: { settings: {} } as Source.Config, + env: {}, + }, + }); + + // The source should have received a push function + expect(receivedPush).toBeDefined(); + expect(typeof receivedPush).toBe('function'); + }); + }); + + describe('Destination with inline code', () => { + it('should initialize destination with inline function code', async () => { + const collector = createTestCollector(); + + // This is what $code: produces at bundle time + const inlineDestination: Destination.Instance = { + type: 'inline-dest', + config: {}, + push: jest.fn(), + }; + + const destinations = await initDestinations(collector, { + myDest: { + code: inlineDestination, + config: {}, + env: {}, + }, + }); + + expect(destinations).toHaveProperty('myDest'); + expect(destinations.myDest.type).toBe('inline-dest'); + }); + + it('should push events to inline destination', async () => { + const mockPush = jest.fn(); + const collector = createTestCollector(); + + const inlineDestination: Destination.Instance = { + type: 'inline-receiver', + config: {}, + push: mockPush, + }; + + const destinations = { + receiver: inlineDestination, + }; + + const event = createEvent({ name: 'test event', data: { foo: 'bar' } }); + + await pushToDestinations(collector, event, {}, destinations); + + expect(mockPush).toHaveBeenCalledTimes(1); + expect(mockPush).toHaveBeenCalledWith( + expect.objectContaining({ name: 'test event' }), + expect.any(Object), + ); + }); + + it('should support init function in inline destination', async () => { + const mockInit = jest.fn().mockResolvedValue({ init: true }); + const mockPush = jest.fn(); + const collector = createTestCollector(); + + const inlineDestinationWithInit: Destination.Instance = { + type: 'inline-with-init', + config: {}, + init: mockInit, + push: mockPush, + }; + + const destinations = { + initDest: inlineDestinationWithInit, + }; + + const event = createEvent({ name: 'trigger init', data: {} }); + + await pushToDestinations(collector, event, {}, destinations); + + expect(mockInit).toHaveBeenCalledTimes(1); + expect(mockPush).toHaveBeenCalledTimes(1); + }); + }); + + describe('Transformer with inline code', () => { + let collector: Collector.Instance; + + beforeEach(async () => { + const result = await startFlow(); + collector = result.collector; + }); + + it('should initialize transformer with inline function code', async () => { + // This is what $code: produces at bundle time + const inlineTransformerCode: Transformer.Init = async (context) => { + return { + type: 'inline-transformer', + config: context.config, + push: async (event) => { + // Transform the event + return { + ...event, + data: { ...event.data, transformed: true }, + }; + }, + }; + }; + + const transformers = await initTransformers(collector, { + myTransformer: { + code: inlineTransformerCode, + config: {}, + env: {}, + }, + }); + + expect(transformers).toHaveProperty('myTransformer'); + expect(transformers.myTransformer.type).toBe('inline-transformer'); + }); + + it('should transform events through inline transformer', async () => { + const inlineTransformerCode: Transformer.Init = async () => { + return { + type: 'enricher', + config: {}, + push: async (event) => { + return { + ...event, + data: { + ...event.data, + enriched: true, + timestamp: 12345, + }, + }; + }, + }; + }; + + const transformers = await initTransformers(collector, { + enricher: { + code: inlineTransformerCode, + config: {}, + env: {}, + }, + }); + + collector.transformers = transformers; + + const inputEvent: WalkerOS.DeepPartialEvent = { + name: 'original event', + data: { original: true }, + }; + + const result = await runTransformerChain( + collector, + transformers, + ['enricher'], + inputEvent, + ); + + expect(result).not.toBeNull(); + expect(result!.data).toEqual({ + original: true, + enriched: true, + timestamp: 12345, + }); + }); + + it('should support transformer chaining with inline code', async () => { + const validatorCode: Transformer.Init = async () => ({ + type: 'validator', + config: { next: 'enricher' }, + push: async (event) => { + if (!event.name) return false; // Stop chain if no name + return { ...event, data: { ...event.data, validated: true } }; + }, + }); + + const enricherCode: Transformer.Init = async () => ({ + type: 'enricher', + config: {}, + push: async (event) => { + return { ...event, data: { ...event.data, enriched: true } }; + }, + }); + + const transformers = await initTransformers(collector, { + validator: { + code: validatorCode, + config: { next: 'enricher' }, + env: {}, + }, + enricher: { code: enricherCode, config: {}, env: {} }, + }); + + collector.transformers = transformers; + + const inputEvent: WalkerOS.DeepPartialEvent = { + name: 'chained event', + data: { original: true }, + }; + + const result = await runTransformerChain( + collector, + transformers, + ['validator', 'enricher'], + inputEvent, + ); + + expect(result).not.toBeNull(); + expect(result!.data).toEqual({ + original: true, + validated: true, + enriched: true, + }); + }); + + it('should stop chain when transformer returns false', async () => { + const blockerCode: Transformer.Init = async () => ({ + type: 'blocker', + config: {}, + push: async () => false as const, // Always block + }); + + const neverCalledCode: Transformer.Init = async () => ({ + type: 'never-called', + config: {}, + push: jest.fn(), + }); + + const transformers = await initTransformers(collector, { + blocker: { code: blockerCode, config: {}, env: {} }, + neverCalled: { code: neverCalledCode, config: {}, env: {} }, + }); + + collector.transformers = transformers; + + const result = await runTransformerChain( + collector, + transformers, + ['blocker', 'neverCalled'], + { name: 'blocked event', data: {} }, + ); + + expect(result).toBeNull(); + }); + }); + + describe('Full flow with inline code', () => { + it('should work with inline source, transformer, and destination together', async () => { + const mockDestPush = jest.fn(); + + // Use startFlow to get a properly initialized collector + const { collector } = await startFlow(); + + // Initialize inline transformer + const inlineTransformer: Transformer.Init = async () => ({ + type: 'flow-transformer', + config: {}, + push: async (event) => ({ + ...event, + data: { ...event.data, transformed: true }, + }), + }); + + const transformers = await initTransformers(collector, { + myTransformer: { + code: inlineTransformer, + config: {}, + env: {}, + }, + }); + + collector.transformers = transformers; + + // Initialize inline destination + const inlineDestination: Destination.Instance = { + type: 'flow-destination', + config: {}, + push: mockDestPush, + }; + + const destinations = { + myDestination: inlineDestination, + }; + + // Initialize inline source + const inlineSource: Source.Init = async (context) => { + return { + type: 'flow-source', + config: context.config as Source.Config, + push: context.env.push as Elb.Fn, + }; + }; + + const sources = await initSources(collector, { + mySource: { + code: inlineSource, + config: { settings: {} } as Source.Config, + env: {}, + }, + }); + + // Verify all components initialized + expect(transformers.myTransformer).toBeDefined(); + expect(transformers.myTransformer.type).toBe('flow-transformer'); + expect(destinations.myDestination.type).toBe('flow-destination'); + expect(sources.mySource.type).toBe('flow-source'); + + // Test transformer transforms event + const event: WalkerOS.DeepPartialEvent = { + name: 'flow event', + data: { source: 'test' }, + }; + + const transformedEvent = await runTransformerChain( + collector, + transformers, + ['myTransformer'], + event, + ); + + expect(transformedEvent).not.toBeNull(); + expect(transformedEvent!.data).toEqual({ + source: 'test', + transformed: true, + }); + }); + }); +}); diff --git a/packages/collector/src/__tests__/transformer.test.ts b/packages/collector/src/__tests__/transformer.test.ts index 3da102f76..792a89531 100644 --- a/packages/collector/src/__tests__/transformer.test.ts +++ b/packages/collector/src/__tests__/transformer.test.ts @@ -97,6 +97,37 @@ describe('Transformer', () => { const result = walkChain('a', transformers); expect(result).toEqual(['a']); }); + + test('returns array directly when provided', () => { + const chain = walkChain(['a', 'b', 'c'], {}); + expect(chain).toEqual(['a', 'b', 'c']); + }); + + test('ignores transformer.next when array provided at start', () => { + const chain = walkChain(['a'], { a: { next: 'b' }, b: {} }); + expect(chain).toEqual(['a']); + }); + + test('still walks chain for string input', () => { + const chain = walkChain('a', { a: { next: 'b' }, b: {} }); + expect(chain).toEqual(['a', 'b']); + }); + + test('appends array next and stops when encountered during walk', () => { + const chain = walkChain('a', { + a: { next: 'b' }, + b: { next: ['c', 'd'] }, + c: { next: 'e' }, + d: {}, + e: {}, + }); + expect(chain).toEqual(['a', 'b', 'c', 'd']); + }); + + test('handles empty array at start', () => { + const chain = walkChain([], { a: { next: 'b' } }); + expect(chain).toEqual([]); + }); }); describe('resolveTransformerGraph', () => { diff --git a/packages/collector/src/destination-code.ts b/packages/collector/src/destination-code.ts deleted file mode 100644 index 6282d3565..000000000 --- a/packages/collector/src/destination-code.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { Destination } from '@walkeros/core'; -import type { CodeMapping, Settings } from './types/code'; - -export const destinationCode: Destination.Instance = { - type: 'code', - config: {}, - - init(context) { - const { config, logger } = context; - const settings = config.settings as Settings | undefined; - - // Inject scripts (fire and forget) - const scripts = settings?.scripts; - if (scripts && typeof document !== 'undefined') { - for (const src of scripts) { - const script = document.createElement('script'); - script.src = src; - script.async = true; - document.head.appendChild(script); - } - } - - // Execute init code - const initCode = settings?.init; - if (!initCode) return; - try { - const fn = new Function('context', initCode); - fn(context); - } catch (e) { - logger.error('Code destination init error:', e); - } - }, - - push(event, context) { - const { rule, config, logger } = context; - const pushCode = - (rule as CodeMapping | undefined)?.push ?? - (config.settings as Settings | undefined)?.push; - if (!pushCode) return; - try { - const fn = new Function('event', 'context', pushCode); - fn(event, context); - } catch (e) { - logger.error('Code destination push error:', e); - } - }, - - pushBatch(batch, context) { - const { rule, config, logger } = context; - const pushBatchCode = - (rule as CodeMapping | undefined)?.pushBatch ?? - (config.settings as Settings | undefined)?.pushBatch; - if (!pushBatchCode) return; - try { - const fn = new Function('batch', 'context', pushBatchCode); - fn(batch, context); - } catch (e) { - logger.error('Code destination pushBatch error:', e); - } - }, - - on(type, context) { - const { config, logger } = context; - const onCode = (config.settings as Settings | undefined)?.on; - if (!onCode) return; - try { - const fn = new Function('type', 'context', onCode); - fn(type, context); - } catch (e) { - logger.error('Code destination on error:', e); - } - }, -}; - -export default destinationCode; diff --git a/packages/collector/src/destination.ts b/packages/collector/src/destination.ts index 9f720d99d..75a74213b 100644 --- a/packages/collector/src/destination.ts +++ b/packages/collector/src/destination.ts @@ -11,14 +11,9 @@ import { tryCatchAsync, useHooks, } from '@walkeros/core'; -import { destinationCode } from './destination-code'; import { callDestinationOn } from './on'; import { runTransformerChain } from './transformer'; -function resolveCode(code: Destination.Instance | true): Destination.Instance { - return code === true ? destinationCode : code; -} - /** * Adds a new destination to the collector. * @@ -35,11 +30,10 @@ export async function addDestination( const { code, config: dataConfig = {}, env = {} } = data; const config = options || dataConfig || { init: false }; - const resolved = resolveCode(code); const destination: Destination.Instance = { - ...resolved, + ...code, config, - env: mergeEnvironments(resolved.env, env), + env: mergeEnvironments(code.env, env), }; let id = destination.config.id; // Use given id @@ -468,17 +462,16 @@ export async function initDestinations( for (const [name, destinationDef] of Object.entries(destinations)) { const { code, config = {}, env = {} } = destinationDef; - const resolved = resolveCode(code); const mergedConfig = { - ...resolved.config, + ...code.config, ...config, }; - const mergedEnv = mergeEnvironments(resolved.env, env); + const mergedEnv = mergeEnvironments(code.env, env); result[name] = { - ...resolved, + ...code, config: mergedConfig, env: mergedEnv, }; diff --git a/packages/collector/src/index.ts b/packages/collector/src/index.ts index 51fe9c9e9..fba86d3ca 100644 --- a/packages/collector/src/index.ts +++ b/packages/collector/src/index.ts @@ -6,7 +6,6 @@ export * from './consent'; export * from './flow'; export * from './push'; export * from './destination'; -export * from './destination-code'; export * from './handle'; export * from './on'; export * from './source'; diff --git a/packages/collector/src/transformer.ts b/packages/collector/src/transformer.ts index 54d11d019..74a1bac0b 100644 --- a/packages/collector/src/transformer.ts +++ b/packages/collector/src/transformer.ts @@ -23,16 +23,22 @@ export interface CollectorWithTransformers extends Collector.Instance { * Walks a transformer chain starting from a given transformer ID. * Returns ordered array of transformer IDs in the chain. * - * @param startId - First transformer in chain + * @param startId - First transformer in chain, or explicit array of transformer IDs * @param transformers - Available transformer configs with optional `next` field * @returns Ordered array of transformer IDs */ export function walkChain( - startId: string | undefined, - transformers: Record = {}, + startId: string | string[] | undefined, + transformers: Record = {}, ): string[] { if (!startId) return []; + // If array provided, use it directly (explicit chain) + if (Array.isArray(startId)) { + return startId; + } + + // Walk the chain via transformer.next links const chain: string[] = []; const visited = new Set(); let current: string | undefined = startId; @@ -44,7 +50,16 @@ export function walkChain( } visited.add(current); chain.push(current); - current = transformers[current].next; + + const next: string | string[] | undefined = transformers[current].next; + + // If transformer has array next, append it and stop walking + if (Array.isArray(next)) { + chain.push(...next); + break; + } + + current = next; } return chain; @@ -61,9 +76,9 @@ export function walkChain( * @returns Resolved transformer chains */ export function resolveTransformerGraph( - _sources: Record = {}, - destinations: Record = {}, - transformers: Record = {}, + _sources: Record = {}, + destinations: Record = {}, + transformers: Record = {}, ): TransformerChain { const post: Record = {}; diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 4a42a5a81..ced73088a 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,74 @@ # @walkeros/core +## 1.2.0 + +### Minor Changes + +- f39d9fb: Add array support for transformer chain configuration + + Enables explicit control over transformer chain order by accepting arrays for + `next` and `before` properties, bypassing automatic chain resolution. + + **Array chain behavior:** + + | Syntax | Behavior | + | -------------------------------- | ------------------------------------------------------ | + | `"next": "validate"` | Walks chain via each transformer's `next` property | + | `"next": ["validate", "enrich"]` | Uses exact order specified, ignores transformer `next` | + + **Example:** + + ```json + { + "sources": { + "http": { + "package": "@walkeros/server-source-express", + "next": ["validate", "enrich", "redact"] + } + }, + "destinations": { + "analytics": { + "package": "@walkeros/server-destination-gcp", + "before": ["format", "anonymize"] + } + } + } + ``` + + When walking a chain encounters an array `next`, it appends all items and + stops (does not recursively resolve those transformers' `next` properties). + +- 888bbdf: Add inline code syntax for sources, transformers, and destinations + + Enables defining custom logic directly in flow.json using `code` objects + instead of requiring external packages. This is ideal for simple one-liner + transformations. + + **Example:** + + ```json + { + "transformers": { + "enrich": { + "code": { + "push": "$code:(event) => ({ ...event, data: { ...event.data, enriched: true } })" + }, + "config": {} + } + } + } + ``` + + **Code object properties:** + - `push` - The push function with `$code:` prefix (required) + - `type` - Optional instance type identifier + - `init` - Optional init function with `$code:` prefix + + **Rules:** + - Use `package` OR `code`, never both (CLI validates this) + - `config` stays separate from `code` + - `$code:` prefix outputs raw JavaScript at bundle time + ## 1.1.0 ### Minor Changes diff --git a/packages/core/package.json b/packages/core/package.json index f719484a9..8ffb498d2 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/core", "description": "Core types and platform-agnostic utilities for walkerOS", - "version": "1.1.0", + "version": "1.2.0", "main": "./dist/index.js", "module": "./dist/index.mjs", "types": "./dist/index.d.ts", diff --git a/packages/core/src/flow.ts b/packages/core/src/flow.ts index 93ac342e7..daffdd6ef 100644 --- a/packages/core/src/flow.ts +++ b/packages/core/src/flow.ts @@ -142,10 +142,10 @@ export function packageNameToVariable(packageName: string): string { */ function resolveCodeFromPackage( packageName: string | undefined, - existingCode: string | undefined, + existingCode: string | Flow.InlineCode | undefined, packages: Flow.Packages | undefined, -): string | undefined { - // Preserve explicit code first +): string | Flow.InlineCode | undefined { + // Preserve explicit code first (including InlineCode objects) if (existingCode) return existingCode; // Auto-generate code from package name if package exists @@ -229,11 +229,22 @@ export function getFlowConfig( result.packages, ); + // Exclude deprecated code: true, only keep valid string or InlineCode + const validCode = + typeof source.code === 'string' || typeof source.code === 'object' + ? source.code + : undefined; + const finalCode = resolvedCode || validCode; result.sources[name] = { - ...source, + package: source.package, config: processedConfig, - ...(resolvedCode && { code: resolvedCode }), - }; + env: source.env, + primary: source.primary, + variables: source.variables, + definitions: source.definitions, + next: source.next, + code: finalCode, + } as Flow.SourceReference; } } @@ -260,11 +271,21 @@ export function getFlowConfig( result.packages, ); + // Exclude deprecated code: true, only keep valid string or InlineCode + const validCode = + typeof dest.code === 'string' || typeof dest.code === 'object' + ? dest.code + : undefined; + const finalCode = resolvedCode || validCode; result.destinations[name] = { - ...dest, + package: dest.package, config: processedConfig, - ...(resolvedCode && { code: resolvedCode }), - }; + env: dest.env, + variables: dest.variables, + definitions: dest.definitions, + before: dest.before, + code: finalCode, + } as Flow.DestinationReference; } } diff --git a/packages/core/src/types/destination.ts b/packages/core/src/types/destination.ts index 6a760046b..28cdee456 100644 --- a/packages/core/src/types/destination.ts +++ b/packages/core/src/types/destination.ts @@ -97,7 +97,7 @@ export interface Policy { [key: string]: WalkerOSMapping.Value; } -export type Code = Instance | true; +export type Code = Instance; export type Init = { code: Code; diff --git a/packages/core/src/types/flow.ts b/packages/core/src/types/flow.ts index d8e5ce80b..f03d0cf7e 100644 --- a/packages/core/src/types/flow.ts +++ b/packages/core/src/types/flow.ts @@ -31,6 +31,16 @@ export type Variables = Record; */ export type Definitions = Record; +/** + * Inline code definition for sources/destinations/transformers. + * Used instead of package when defining inline functions. + */ +export interface InlineCode { + push: string; // "$code:..." function (required) + type?: string; // Optional instance type identifier + init?: string; // Optional "$code:..." init function +} + /** * Packages configuration for build. */ @@ -321,6 +331,7 @@ export interface Config { * @remarks * References a source package and provides configuration. * The package is automatically downloaded and imported during build. + * Alternatively, use `code: true` for inline code execution. */ export interface SourceReference { /** @@ -338,19 +349,31 @@ export interface SourceReference { * 3. Auto-detect default or named export * 4. Generate import statement * + * Optional when `code: true` is used for inline code execution. + * * @example * "package": "@walkeros/web-source-browser@latest" */ - package: string; + package?: string; /** - * Resolved import variable name. + * Resolved import variable name or built-in code source. * * @remarks - * Auto-resolved from packages[package].imports[0] during getFlowConfig(). - * Can also be provided explicitly for advanced use cases. + * - String: Auto-resolved from packages[package].imports[0] during getFlowConfig(), + * or provided explicitly for advanced use cases. + * - InlineCode: Object with type, push, and optional init for inline code definition. + * + * @example + * // Using inline code object + * { + * "code": { + * "type": "logger", + * "push": "$code:(event) => console.log(event)" + * } + * } */ - code?: string; + code?: string | InlineCode; // string for package import, InlineCode for inline /** * Source-specific configuration. @@ -413,8 +436,9 @@ export interface SourceReference { * @remarks * Name of the transformer to execute after this source captures an event. * If omitted, events route directly to the collector. + * Can be an array for explicit chain control (bypasses transformer.next resolution). */ - next?: string; + next?: string | string[]; } /** @@ -423,6 +447,7 @@ export interface SourceReference { * @remarks * References a transformer package and provides configuration. * Transformers transform events in the pipeline between sources and destinations. + * Alternatively, use `code: true` for inline code execution. */ export interface TransformerReference { /** @@ -430,20 +455,31 @@ export interface TransformerReference { * * @remarks * Same format as SourceReference.package + * Optional when `code: true` is used for inline code execution. * * @example * "package": "@walkeros/transformer-enricher@1.0.0" */ - package: string; + package?: string; /** - * Resolved import variable name. + * Resolved import variable name or built-in code transformer. * * @remarks - * Auto-resolved from packages[package].imports[0] during getFlowConfig(). - * Can also be provided explicitly for advanced use cases. + * - String: Auto-resolved from packages[package].imports[0] during getFlowConfig(), + * or provided explicitly for advanced use cases. + * - InlineCode: Object with type, push, and optional init for inline code definition. + * + * @example + * // Using inline code object + * { + * "code": { + * "type": "enricher", + * "push": "$code:(event) => ({ ...event, data: { enriched: true } })" + * } + * } */ - code?: string; + code?: string | InlineCode; // string for package import, InlineCode for inline /** * Transformer-specific configuration. @@ -471,8 +507,9 @@ export interface TransformerReference { * If omitted: * - Pre-collector: routes to collector * - Post-collector: routes to destination + * Can be an array for explicit chain control (terminates chain walking). */ - next?: string; + next?: string | string[]; /** * Transformer-level variables (highest priority in cascade). @@ -500,20 +537,31 @@ export interface DestinationReference { * * @remarks * Same format as SourceReference.package + * Optional when `code: true` is used for inline code execution. * * @example * "package": "@walkeros/web-destination-gtag@2.0.0" */ - package: string; + package?: string; /** - * Resolved import variable name. + * Resolved import variable name or built-in code destination. * * @remarks - * Auto-resolved from packages[package].imports[0] during getFlowConfig(). - * Can also be provided explicitly for advanced use cases. + * - String: Auto-resolved from packages[package].imports[0] during getFlowConfig(), + * or provided explicitly for advanced use cases. + * - InlineCode: Object with type, push, and optional init for inline code definition. + * + * @example + * // Using inline code object + * { + * "code": { + * "type": "logger", + * "push": "$code:(event) => console.log('Event:', event.name)" + * } + * } */ - code?: string; + code?: string | InlineCode; // string for package import, InlineCode for inline /** * Destination-specific configuration. @@ -576,6 +624,7 @@ export interface DestinationReference { * @remarks * Name of the transformer to execute before sending events to this destination. * If omitted, events are sent directly from the collector. + * Can be an array for explicit chain control (bypasses transformer.next resolution). */ - before?: string; + before?: string | string[]; } diff --git a/packages/server/core/CHANGELOG.md b/packages/server/core/CHANGELOG.md index 50b6e94af..f40bdf640 100644 --- a/packages/server/core/CHANGELOG.md +++ b/packages/server/core/CHANGELOG.md @@ -1,5 +1,13 @@ # @walkeros/server-core +## 1.0.2 + +### Patch Changes + +- Updated dependencies [f39d9fb] +- Updated dependencies [888bbdf] + - @walkeros/core@1.2.0 + ## 1.0.1 ### Patch Changes diff --git a/packages/server/core/package.json b/packages/server/core/package.json index 588d3eb0a..39c92cc85 100644 --- a/packages/server/core/package.json +++ b/packages/server/core/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/server-core", "description": "Server-specific utilities for walkerOS", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -25,7 +25,7 @@ "update": "npx npm-check-updates -u && npm update" }, "dependencies": { - "@walkeros/core": "1.1.0" + "@walkeros/core": "1.2.0" }, "repository": { "url": "git+https://github.com/elbwalker/walkerOS.git", diff --git a/packages/server/destinations/api/CHANGELOG.md b/packages/server/destinations/api/CHANGELOG.md index 97e442175..ff2331606 100644 --- a/packages/server/destinations/api/CHANGELOG.md +++ b/packages/server/destinations/api/CHANGELOG.md @@ -1,5 +1,14 @@ # @walkeros/server-destination-api +## 1.0.2 + +### Patch Changes + +- Updated dependencies [f39d9fb] +- Updated dependencies [888bbdf] + - @walkeros/core@1.2.0 + - @walkeros/server-core@1.0.2 + ## 1.0.1 ### Patch Changes diff --git a/packages/server/destinations/api/package.json b/packages/server/destinations/api/package.json index ffe7fe0f8..b5a03a35a 100644 --- a/packages/server/destinations/api/package.json +++ b/packages/server/destinations/api/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/server-destination-api", "description": "API server destination for walkerOS", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -35,8 +35,8 @@ "update": "npx npm-check-updates -u && npm update" }, "dependencies": { - "@walkeros/core": "1.1.0", - "@walkeros/server-core": "1.0.1" + "@walkeros/core": "1.2.0", + "@walkeros/server-core": "1.0.2" }, "devDependencies": {}, "repository": { diff --git a/packages/server/destinations/aws/CHANGELOG.md b/packages/server/destinations/aws/CHANGELOG.md index 389d53c42..31ecb8280 100644 --- a/packages/server/destinations/aws/CHANGELOG.md +++ b/packages/server/destinations/aws/CHANGELOG.md @@ -1,5 +1,12 @@ # @walkeros/server-destination-aws +## 1.0.2 + +### Patch Changes + +- 6778ab2: Add default exports for simpler CLI flow.json configuration + - @walkeros/server-core@1.0.2 + ## 1.0.1 ### Patch Changes diff --git a/packages/server/destinations/aws/package.json b/packages/server/destinations/aws/package.json index eac914b7f..f3eb27ca3 100644 --- a/packages/server/destinations/aws/package.json +++ b/packages/server/destinations/aws/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/server-destination-aws", "description": "AWS server destination for walkerOS", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "exports": { ".": { @@ -33,7 +33,7 @@ }, "dependencies": { "@aws-sdk/client-firehose": "^3.952.0", - "@walkeros/server-core": "1.0.1" + "@walkeros/server-core": "1.0.2" }, "devDependencies": {}, "repository": { diff --git a/packages/server/destinations/aws/src/index.ts b/packages/server/destinations/aws/src/index.ts index 320bcbabc..e5f92b4c7 100644 --- a/packages/server/destinations/aws/src/index.ts +++ b/packages/server/destinations/aws/src/index.ts @@ -1,3 +1,5 @@ // AWS Firehose export { destinationFirehose } from './firehose'; export * as DestinationFirehose from './firehose/types'; + +export { destinationFirehose as default } from './firehose'; diff --git a/packages/server/destinations/datamanager/CHANGELOG.md b/packages/server/destinations/datamanager/CHANGELOG.md index 733636904..693578743 100644 --- a/packages/server/destinations/datamanager/CHANGELOG.md +++ b/packages/server/destinations/datamanager/CHANGELOG.md @@ -1,5 +1,14 @@ # @walkeros/server-destination-datamanager +## 1.0.2 + +### Patch Changes + +- Updated dependencies [f39d9fb] +- Updated dependencies [888bbdf] + - @walkeros/core@1.2.0 + - @walkeros/server-core@1.0.2 + ## 1.0.1 ### Patch Changes diff --git a/packages/server/destinations/datamanager/package.json b/packages/server/destinations/datamanager/package.json index f3710812b..62432b41a 100644 --- a/packages/server/destinations/datamanager/package.json +++ b/packages/server/destinations/datamanager/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/server-destination-datamanager", "description": "Google Data Manager server destination for walkerOS", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "exports": { ".": { @@ -32,8 +32,8 @@ "update": "npx npm-check-updates -u && npm update" }, "dependencies": { - "@walkeros/core": "1.1.0", - "@walkeros/server-core": "1.0.1", + "@walkeros/core": "1.2.0", + "@walkeros/server-core": "1.0.2", "google-auth-library": "^10.5.0" }, "devDependencies": {}, diff --git a/packages/server/destinations/gcp/CHANGELOG.md b/packages/server/destinations/gcp/CHANGELOG.md index 1e90eabf8..30b8397d5 100644 --- a/packages/server/destinations/gcp/CHANGELOG.md +++ b/packages/server/destinations/gcp/CHANGELOG.md @@ -1,5 +1,12 @@ # @walkeros/server-destination-gcp +## 1.0.2 + +### Patch Changes + +- 6778ab2: Add default exports for simpler CLI flow.json configuration + - @walkeros/server-core@1.0.2 + ## 1.0.1 ### Patch Changes diff --git a/packages/server/destinations/gcp/package.json b/packages/server/destinations/gcp/package.json index d7d1da6b8..b2e6e1eae 100644 --- a/packages/server/destinations/gcp/package.json +++ b/packages/server/destinations/gcp/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/server-destination-gcp", "description": "Google Cloud Platform server destination for walkerOS (BigQuery)", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "exports": { ".": { @@ -28,7 +28,7 @@ }, "dependencies": { "@google-cloud/bigquery": "^8.1.1", - "@walkeros/server-core": "1.0.1" + "@walkeros/server-core": "1.0.2" }, "devDependencies": {}, "repository": { diff --git a/packages/server/destinations/gcp/src/index.ts b/packages/server/destinations/gcp/src/index.ts index 4bc528f7e..f5e5b2deb 100644 --- a/packages/server/destinations/gcp/src/index.ts +++ b/packages/server/destinations/gcp/src/index.ts @@ -1,3 +1,5 @@ // Google BigQuery export { destinationBigQuery } from './bigquery'; export * as DestinationBigQuery from './bigquery/types'; + +export { destinationBigQuery as default } from './bigquery'; diff --git a/packages/server/destinations/meta/CHANGELOG.md b/packages/server/destinations/meta/CHANGELOG.md index 36347bb3e..910fb7717 100644 --- a/packages/server/destinations/meta/CHANGELOG.md +++ b/packages/server/destinations/meta/CHANGELOG.md @@ -1,5 +1,14 @@ # @walkeros/server-destination-meta +## 1.0.2 + +### Patch Changes + +- Updated dependencies [f39d9fb] +- Updated dependencies [888bbdf] + - @walkeros/core@1.2.0 + - @walkeros/server-core@1.0.2 + ## 1.0.1 ### Patch Changes diff --git a/packages/server/destinations/meta/package.json b/packages/server/destinations/meta/package.json index 3befbc4f3..d7a4c60ee 100644 --- a/packages/server/destinations/meta/package.json +++ b/packages/server/destinations/meta/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/server-destination-meta", "description": "Meta server destination for walkerOS", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "exports": { ".": { @@ -32,8 +32,8 @@ "update": "npx npm-check-updates -u && npm update" }, "dependencies": { - "@walkeros/core": "1.1.0", - "@walkeros/server-core": "1.0.1" + "@walkeros/core": "1.2.0", + "@walkeros/server-core": "1.0.2" }, "devDependencies": {}, "repository": { diff --git a/packages/server/sources/aws/CHANGELOG.md b/packages/server/sources/aws/CHANGELOG.md index 885ea6583..982993683 100644 --- a/packages/server/sources/aws/CHANGELOG.md +++ b/packages/server/sources/aws/CHANGELOG.md @@ -1,5 +1,13 @@ # @walkeros/server-source-aws +## 1.0.2 + +### Patch Changes + +- Updated dependencies [f39d9fb] +- Updated dependencies [888bbdf] + - @walkeros/core@1.2.0 + ## 1.0.1 ### Patch Changes diff --git a/packages/server/sources/aws/package.json b/packages/server/sources/aws/package.json index 875bca5a4..78f1f6f92 100644 --- a/packages/server/sources/aws/package.json +++ b/packages/server/sources/aws/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/server-source-aws", "description": "AWS server sources for walkerOS (Lambda, API Gateway, Function URLs)", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -20,7 +20,7 @@ "update": "npx npm-check-updates -u && npm update" }, "dependencies": { - "@walkeros/core": "1.1.0" + "@walkeros/core": "1.2.0" }, "peerDependencies": { "@types/aws-lambda": "^8.10.0" diff --git a/packages/server/sources/express/CHANGELOG.md b/packages/server/sources/express/CHANGELOG.md index 9193d7e02..523fcdafb 100644 --- a/packages/server/sources/express/CHANGELOG.md +++ b/packages/server/sources/express/CHANGELOG.md @@ -1,5 +1,14 @@ # @walkeros/server-source-express +## 1.0.2 + +### Patch Changes + +- 6778ab2: Add default exports for simpler CLI flow.json configuration +- Updated dependencies [f39d9fb] +- Updated dependencies [888bbdf] + - @walkeros/core@1.2.0 + ## 1.0.1 ### Patch Changes diff --git a/packages/server/sources/express/package.json b/packages/server/sources/express/package.json index d494d2521..07cb84213 100644 --- a/packages/server/sources/express/package.json +++ b/packages/server/sources/express/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/server-source-express", "description": "Express server source for walkerOS", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -18,7 +18,7 @@ "update": "npx npm-check-updates -u && npm update" }, "dependencies": { - "@walkeros/core": "1.1.0", + "@walkeros/core": "1.2.0", "express": "^5.2.1", "cors": "^2.8.5" }, diff --git a/packages/server/sources/express/src/index.ts b/packages/server/sources/express/src/index.ts index 7649420d4..faa14b7f6 100644 --- a/packages/server/sources/express/src/index.ts +++ b/packages/server/sources/express/src/index.ts @@ -191,3 +191,5 @@ export type { // Export utils export { setCorsHeaders, TRANSPARENT_GIF } from './utils'; + +export default sourceExpress; diff --git a/packages/server/sources/fetch/CHANGELOG.md b/packages/server/sources/fetch/CHANGELOG.md index fa90288c9..402800885 100644 --- a/packages/server/sources/fetch/CHANGELOG.md +++ b/packages/server/sources/fetch/CHANGELOG.md @@ -1,5 +1,14 @@ # @walkeros/server-source-fetch +## 1.0.2 + +### Patch Changes + +- 6778ab2: Add default exports for simpler CLI flow.json configuration +- Updated dependencies [f39d9fb] +- Updated dependencies [888bbdf] + - @walkeros/core@1.2.0 + ## 1.0.1 ### Patch Changes diff --git a/packages/server/sources/fetch/package.json b/packages/server/sources/fetch/package.json index cc6ad4980..b97d529d1 100644 --- a/packages/server/sources/fetch/package.json +++ b/packages/server/sources/fetch/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/server-source-fetch", "description": "Web Standard Fetch API source for walkerOS (Cloudflare Workers, Vercel Edge, Deno, Bun)", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -18,7 +18,7 @@ "update": "npx npm-check-updates -u && npm update" }, "dependencies": { - "@walkeros/core": "1.1.0" + "@walkeros/core": "1.2.0" }, "devDependencies": {}, "repository": { diff --git a/packages/server/sources/fetch/src/index.ts b/packages/server/sources/fetch/src/index.ts index 97bebef41..e5101c703 100644 --- a/packages/server/sources/fetch/src/index.ts +++ b/packages/server/sources/fetch/src/index.ts @@ -291,3 +291,5 @@ export * as SourceFetch from './types'; export * from './utils'; export * as schemas from './schemas'; export * as examples from './examples'; + +export default sourceFetch; diff --git a/packages/server/sources/gcp/CHANGELOG.md b/packages/server/sources/gcp/CHANGELOG.md index 3e28f9005..c19a9812c 100644 --- a/packages/server/sources/gcp/CHANGELOG.md +++ b/packages/server/sources/gcp/CHANGELOG.md @@ -1,5 +1,13 @@ # @walkeros/server-source-gcp +## 1.0.2 + +### Patch Changes + +- Updated dependencies [f39d9fb] +- Updated dependencies [888bbdf] + - @walkeros/core@1.2.0 + ## 1.0.1 ### Patch Changes diff --git a/packages/server/sources/gcp/package.json b/packages/server/sources/gcp/package.json index 1d9f1ab12..835a38892 100644 --- a/packages/server/sources/gcp/package.json +++ b/packages/server/sources/gcp/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/server-source-gcp", "description": "Google Cloud Platform server sources for walkerOS (Cloud Functions)", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -18,7 +18,7 @@ "update": "npx npm-check-updates -u && npm update" }, "dependencies": { - "@walkeros/core": "1.1.0" + "@walkeros/core": "1.2.0" }, "peerDependencies": { "@google-cloud/functions-framework": "^3.0.0" diff --git a/packages/server/transformers/fingerprint/CHANGELOG.md b/packages/server/transformers/fingerprint/CHANGELOG.md index 0559e63a9..230598e6c 100644 --- a/packages/server/transformers/fingerprint/CHANGELOG.md +++ b/packages/server/transformers/fingerprint/CHANGELOG.md @@ -1,5 +1,15 @@ # @walkeros/server-transformer-fingerprint +## 6.0.0 + +### Patch Changes + +- 6778ab2: Add default exports for simpler CLI flow.json configuration +- Updated dependencies [f39d9fb] +- Updated dependencies [888bbdf] + - @walkeros/core@1.2.0 + - @walkeros/server-core@1.0.2 + ## 5.0.0 ### Patch Changes diff --git a/packages/server/transformers/fingerprint/package.json b/packages/server/transformers/fingerprint/package.json index 9c3178aa0..87137686a 100644 --- a/packages/server/transformers/fingerprint/package.json +++ b/packages/server/transformers/fingerprint/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/server-transformer-fingerprint", "description": "Fingerprint transformer for walkerOS server - hash configurable fields for session continuity", - "version": "5.0.0", + "version": "6.0.0", "license": "MIT", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -25,12 +25,12 @@ "update": "npx npm-check-updates -u && npm update" }, "peerDependencies": { - "@walkeros/core": "^1.1.0", - "@walkeros/server-core": "^1.0.1" + "@walkeros/core": "^1.2.0", + "@walkeros/server-core": "^1.0.2" }, "devDependencies": { - "@walkeros/core": "1.1.0", - "@walkeros/server-core": "1.0.1" + "@walkeros/core": "1.2.0", + "@walkeros/server-core": "1.0.2" }, "repository": { "url": "git+https://github.com/elbwalker/walkerOS.git", diff --git a/packages/server/transformers/fingerprint/src/index.ts b/packages/server/transformers/fingerprint/src/index.ts index 855822dd1..451eb8e3e 100644 --- a/packages/server/transformers/fingerprint/src/index.ts +++ b/packages/server/transformers/fingerprint/src/index.ts @@ -1,2 +1,4 @@ export { transformerFingerprint } from './transformer'; export type { FingerprintSettings } from './types'; + +export { transformerFingerprint as default } from './transformer'; diff --git a/packages/transformers/validator/CHANGELOG.md b/packages/transformers/validator/CHANGELOG.md index fa695e692..6d6efaac3 100644 --- a/packages/transformers/validator/CHANGELOG.md +++ b/packages/transformers/validator/CHANGELOG.md @@ -1,5 +1,14 @@ # @walkeros/transformer-validator +## 3.0.0 + +### Patch Changes + +- 6778ab2: Add default exports for simpler CLI flow.json configuration +- Updated dependencies [f39d9fb] +- Updated dependencies [888bbdf] + - @walkeros/core@1.2.0 + ## 2.0.0 ### Patch Changes diff --git a/packages/transformers/validator/package.json b/packages/transformers/validator/package.json index 9195a087b..f04ee05f6 100644 --- a/packages/transformers/validator/package.json +++ b/packages/transformers/validator/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/transformer-validator", "description": "Event validation transformer for walkerOS using AJV and JSON Schema", - "version": "2.0.0", + "version": "3.0.0", "license": "MIT", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -33,10 +33,10 @@ "ajv": "^8.17.1" }, "peerDependencies": { - "@walkeros/core": "^1.1.0" + "@walkeros/core": "^1.2.0" }, "devDependencies": { - "@walkeros/core": "1.1.0" + "@walkeros/core": "1.2.0" }, "repository": { "url": "git+https://github.com/elbwalker/walkerOS.git", diff --git a/packages/transformers/validator/src/index.ts b/packages/transformers/validator/src/index.ts index 8f4875893..b9bd181e3 100644 --- a/packages/transformers/validator/src/index.ts +++ b/packages/transformers/validator/src/index.ts @@ -5,3 +5,5 @@ export type { ContractRule, JsonSchema, } from './types'; + +export { transformerValidator as default } from './transformer'; diff --git a/packages/web/core/CHANGELOG.md b/packages/web/core/CHANGELOG.md index b5503dd0b..f3514b70b 100644 --- a/packages/web/core/CHANGELOG.md +++ b/packages/web/core/CHANGELOG.md @@ -1,5 +1,13 @@ # @walkeros/web-core +## 1.0.2 + +### Patch Changes + +- Updated dependencies [f39d9fb] +- Updated dependencies [888bbdf] + - @walkeros/core@1.2.0 + ## 1.0.1 ### Patch Changes diff --git a/packages/web/core/README.md b/packages/web/core/README.md index 2c4d1e77d..dbedd1fd0 100644 --- a/packages/web/core/README.md +++ b/packages/web/core/README.md @@ -11,14 +11,18 @@ Web core utilities are browser-specific functions designed for client-side walkerOS implementations. These utilities handle DOM interactions, browser -information, storage, sessions, and web-based communication. +information, storage, element visibility, and web-based communication. + +> **Note**: Session management has been moved to `@walkeros/web-source-session`. +> See the [session source package](../sources/session/README.md) for session +> tracking. ## Installation Import web utilities from the `@walkeros/web-core` package: ```ts -import { getAttribute, sendWeb, sessionStart } from '@walkeros/web-core'; +import { getAttribute, sendWeb, isVisible } from '@walkeros/web-core'; ``` ## Utilities @@ -124,11 +128,11 @@ This function considers: data from browser storage with automatic type conversion. ```js -// Default uses localStorage +// Default uses sessionStorage const userId = storageRead('walker_user_id'); -// Use sessionStorage -const sessionData = storageRead('session_data', 'sessionStorage'); +// Use localStorage +const data = storageRead('data', 'local'); ``` ##### storageWrite @@ -140,8 +144,8 @@ writes data to storage with expiration and domain options. // Store with 30-minute expiration storageWrite('user_preference', 'dark-mode', 30); -// Store in sessionStorage -storageWrite('temp_data', { id: 123 }, undefined, 'sessionStorage'); +// Store in localStorage +storageWrite('temp_data', { id: 123 }, undefined, 'local'); // Store with custom domain for cookies storageWrite('tracking_id', 'abc123', 1440, 'cookie', '.example.com'); @@ -153,43 +157,9 @@ storageWrite('tracking_id', 'abc123', 1440, 'cookie', '.example.com'); ```js storageDelete('expired_data'); -storageDelete('session_temp', 'sessionStorage'); +storageDelete('session_temp', 'local'); ``` -### Session Management - -#### sessionStart - -`sessionStart(config?: SessionConfig): WalkerOS.SessionData | void` initializes -and manages user sessions with automatic renewal and tracking. - -```js -// Start session with default config -const session = sessionStart(); - -// Custom session configuration -const session = sessionStart({ - storage: true, - domain: '.example.com', - maxAge: 1440, // 24 hours - sampling: 1.0, // 100% sampling -}); -``` - -Session data includes: - -- `id` - Unique session identifier -- `start` - Session start timestamp -- `isNew` - Whether this is a new session -- `count` - Number of events in session -- `device` - Device identifier -- `storage` - Whether storage is available - -#### Advanced Session Functions - -- `sessionStorage` - Session-specific storage operations -- `sessionWindow` - Window/tab session management - ### Web Communication #### sendWeb @@ -282,35 +252,20 @@ interface SendWebOptionsFetch extends SendWebOptions { } ``` -### SessionConfig - -```ts -interface SessionConfig { - storage?: boolean; // Enable storage persistence - domain?: string; // Cookie domain - maxAge?: number; // Session duration in minutes - sampling?: number; // Sampling rate (0-1) -} -``` - ### StorageType ```ts -type StorageType = 'localStorage' | 'sessionStorage' | 'cookie'; +type StorageType = 'local' | 'session' | 'cookie'; ``` ## Usage Notes - **Consent Required**: Browser information functions may require user consent depending on privacy regulations -- **Storage Fallbacks**: Storage functions gracefully handle unavailable storage - with fallbacks - **Transport Selection**: Choose transport based on use case: - `fetch` - Modern, flexible, supports responses - `beacon` - Reliable during page unload, small payloads - `xhr` - Synchronous when needed, broader browser support -- **Performance**: Session and storage operations are optimized for minimal - performance impact --- diff --git a/packages/web/core/package.json b/packages/web/core/package.json index 670f12bc6..54aaf9c76 100644 --- a/packages/web/core/package.json +++ b/packages/web/core/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/web-core", "description": "Web-specific utilities for walkerOS", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -25,7 +25,7 @@ "update": "npx npm-check-updates -u && npm update" }, "dependencies": { - "@walkeros/core": "1.1.0" + "@walkeros/core": "1.2.0" }, "repository": { "url": "git+https://github.com/elbwalker/walkerOS.git", diff --git a/packages/web/core/src/__tests__/sessionStart.test.ts b/packages/web/core/src/__tests__/sessionStart.test.ts deleted file mode 100644 index bc007fca8..000000000 --- a/packages/web/core/src/__tests__/sessionStart.test.ts +++ /dev/null @@ -1,192 +0,0 @@ -import type { On, WalkerOS, Collector } from '@walkeros/core'; -import { elb, sessionStart, sessionStorage, sessionWindow } from '..'; - -let consent: On.ConsentConfig; - -jest.mock('../elb', () => ({ - elb: jest.fn().mockImplementation((event, data, options) => { - if (event === 'walker on' && data == 'consent') { - consent = options; - } - }), -})); -jest.mock('../session/sessionStorage', () => ({ - sessionStorage: jest.fn().mockImplementation((config) => { - return { ...config.data, mock: 'storage' }; - }), -})); -jest.mock('../session/sessionWindow', () => ({ - sessionWindow: jest.fn().mockImplementation((config) => { - return { ...config.data, mock: 'window' }; - }), -})); - -describe('sessionStart', () => { - const w = window; - - const mockElb = elb as jest.Mock; - const mockSessionStorage = sessionStorage as jest.Mock; - const mockSessionWindow = sessionWindow as jest.Mock; - - beforeEach(() => { - Object.defineProperty(w, 'performance', { - value: { - getEntriesByType: jest.fn().mockReturnValue([{ type: 'navigate' }]), - }, - writable: true, - }); - - jest.clearAllMocks(); - jest.resetModules(); - - consent = {}; - }); - - test('Default', () => { - sessionStart(); - expect(mockSessionWindow).toHaveBeenCalledWith({}); - }); - - test('Storage', () => { - sessionStart({ storage: true }); - expect(mockSessionStorage).toHaveBeenCalledWith({ storage: true }); - }); - - test('Consent', () => { - const consentName = 'foo'; - const config = { consent: consentName }; - sessionStart(config); - expect(mockElb).toHaveBeenCalledTimes(1); - expect(mockElb).toHaveBeenCalledWith('walker on', 'consent', { - foo: expect.any(Function), - }); - - // Simulate granted consent call from walker.js collector - // Granted - const mockCollector = { - command: jest.fn(), - push: jest.fn(), - } as unknown as Collector.Instance; - - expect(mockSessionStorage).toHaveBeenCalledTimes(0); - consent[consentName](mockCollector, { - [consentName]: true, - }); - expect(mockSessionStorage).toHaveBeenCalledWith(config); - - // Denied - expect(mockSessionWindow).toHaveBeenCalledTimes(0); - consent[consentName](mockCollector, { - [consentName]: false, - }); - expect(mockSessionWindow).toHaveBeenCalledWith(config); - }); - - test('Callback without consent', () => { - const collector = { - command: jest.fn(), - push: jest.fn(), - } as unknown as Collector.Instance; - const mockCb = jest.fn(); - const config = { cb: mockCb, collector, storage: false }; - sessionStart(config); - - expect(mockCb).toHaveBeenCalledTimes(1); - expect(mockCb).toHaveBeenCalledWith( - { - mock: 'window', - }, - collector, - expect.any(Function), - ); - }); - - test('Callback with consent', () => { - const consentName = 'foo'; - const collector = { - command: jest.fn(), - push: jest.fn(), - } as unknown as Collector.Instance; - const mockCb = jest.fn(); - const config = { cb: mockCb, consent: consentName, storage: true }; - sessionStart(config); - - // Granted, use sessionStorage - consent[consentName](collector, { - [consentName]: true, - }); - expect(mockCb).toHaveBeenCalledTimes(1); - expect(mockCb).toHaveBeenCalledWith( - { - mock: 'storage', - }, - collector, - expect.any(Function), - ); - - // Denied, use sessionWindow - consent[consentName](collector, { - [consentName]: false, - }); - expect(mockCb).toHaveBeenCalledTimes(2); - expect(mockCb).toHaveBeenCalledWith( - { - mock: 'window', - }, - collector, - expect.any(Function), - ); - }); - - test('Callback default', () => { - // No elb calls if no session is started - sessionStart(); - expect(mockElb).toHaveBeenCalledTimes(1); - expect(mockElb).toHaveBeenCalledWith('walker user', expect.any(Object)); - - jest.clearAllMocks(); - sessionStart({ data: { isStart: true } }); - expect(mockElb).toHaveBeenCalledTimes(2); - expect(mockElb).toHaveBeenNthCalledWith( - 1, - 'walker user', - expect.any(Object), - ); - expect(mockElb).toHaveBeenNthCalledWith(2, { - name: 'session start', - data: expect.any(Object), - }); - }); - - test('Callback default storage', () => { - sessionStart({ - data: { storage: true, isNew: true, device: 'd3v1c3', id: 's3ss10n' }, - }); - expect(mockElb).toHaveBeenCalledWith('walker user', { - device: 'd3v1c3', - session: 's3ss10n', - }); - }); - - test('Callback disabled', () => { - // No elb calls if no session is started - sessionStart({ cb: false, data: { isStart: true } }); - expect(mockElb).toHaveBeenCalledTimes(0); - }); - - test('Callback default elb calls', () => { - const session = sessionStart({ data: { isNew: true, isStart: true } }); - expect(mockElb).toHaveBeenCalledWith({ - name: 'session start', - data: session, - }); - }); - - test('multiple consent keys', () => { - sessionStart({ consent: ['foo', 'bar'] }); - expect(mockElb).toHaveBeenCalledWith('walker on', 'consent', { - foo: expect.any(Function), - bar: expect.any(Function), - }); - }); -}); diff --git a/packages/web/core/src/__tests__/storage.test.ts b/packages/web/core/src/__tests__/storage.test.ts deleted file mode 100644 index b91b1167f..000000000 --- a/packages/web/core/src/__tests__/storage.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Const } from '@walkeros/core'; -import { storageDelete, storageRead, storageWrite } from '..'; - -describe('Storage', () => { - const w = window; - - beforeEach(() => {}); - - test('Storage', async () => { - const key = 'id'; - const value = 'abc'; - - // Session - storageWrite(key, value); - expect(storageRead(key)).toBe(value); - storageDelete(key); - expect(storageRead(key)).toBe(''); - w.sessionStorage.setItem(key, 's'); - expect(storageRead(key)).toBe('s'); - - // Local - storageWrite(key, value, 1, Const.Utils.Storage.Local); - expect(storageRead(key, Const.Utils.Storage.Local)).toBe(value); - storageDelete(key, Const.Utils.Storage.Local); - expect(storageRead(key, Const.Utils.Storage.Local)).toBe(''); - - // Cookie - Object.defineProperty(document, 'cookie', { - writable: true, - value: '', - }); - expect(storageWrite(key, value, 1, Const.Utils.Storage.Cookie)).toBe(value); - storageDelete(key, Const.Utils.Storage.Cookie); - expect(storageRead(key, Const.Utils.Storage.Cookie)).toBe(''); - expect(storageRead('foo', Const.Utils.Storage.Cookie)).toBe(''); - storageWrite(key, value, 1, Const.Utils.Storage.Cookie, 'walkeros.io'); - expect(document.cookie).toContain('domain=walkeros.io'); - - // Expiration Session - expect(storageWrite(key, value, 5)).toBe(value); - jest.advanceTimersByTime(6 * 60 * 1000); - expect(w.sessionStorage.getItem(key)).toBeDefined(); - expect(storageRead(key)).toBe(''); - expect(w.sessionStorage.getItem(key)).toBeNull(); - - // Expiration Local - expect(storageWrite(key, value, 5, Const.Utils.Storage.Local)).toBe(value); - jest.advanceTimersByTime(6 * 60 * 1000); - expect(w.localStorage.getItem(key)).toBeDefined(); - expect(storageRead(key, Const.Utils.Storage.Local)).toBe(''); - expect(w.localStorage.getItem(key)).toBeNull(); - - // Expiration Cookie - storageWrite(key, value, 5, Const.Utils.Storage.Cookie); - expect(document.cookie).toContain('max-age=300'); - - // Cast - expect(storageWrite(key, true)).toBe(true); - }); -}); diff --git a/packages/web/core/src/index.ts b/packages/web/core/src/index.ts index 7f9534415..fcc8e24f9 100644 --- a/packages/web/core/src/index.ts +++ b/packages/web/core/src/index.ts @@ -5,7 +5,6 @@ export * from './environment'; export * from './getHashWeb'; export * from './isVisible'; export * from './sendWeb'; -export * from './session/'; export * from './storage'; // Export web-specific types diff --git a/packages/web/core/src/types/collector.ts b/packages/web/core/src/types/collector.ts index 11d1ed649..2ab5504e8 100644 --- a/packages/web/core/src/types/collector.ts +++ b/packages/web/core/src/types/collector.ts @@ -4,12 +4,11 @@ import type { Hooks, WalkerOS, Destination as WalkerOSDestination, + On, } from '@walkeros/core'; -import type { SessionConfig } from '../session'; import type { Destination, Config as DestConfig } from './destination'; import type { Layer } from './elb'; import type { Events, Trigger } from './walker'; -import type { On } from '@walkeros/core'; declare global { interface Window { @@ -28,9 +27,6 @@ export interface Collector extends Omit { getAllEvents: (scope: Element, prefix: string) => Events; getEvents: (target: Element, trigger: Trigger, prefix: string) => Events; getGlobals: () => WalkerOS.Properties; - sessionStart: ( - options?: SessionStartOptions, - ) => void | CoreCollector.SessionData; _visibilityState?: { observer: IntersectionObserver | undefined; timers: WeakMap; @@ -56,7 +52,6 @@ export interface Config { prefix: string; run: boolean; scope: Scope; - session: false | SessionConfig; elb?: string; name?: string; } @@ -71,11 +66,6 @@ export interface InitConfig extends Partial { user?: WalkerOS.User; } -export interface SessionStartOptions { - config?: SessionConfig; - data?: Partial; -} - export interface Destinations { [name: string]: Destination; } diff --git a/packages/web/destinations/api/CHANGELOG.md b/packages/web/destinations/api/CHANGELOG.md index 8906335dd..640a23282 100644 --- a/packages/web/destinations/api/CHANGELOG.md +++ b/packages/web/destinations/api/CHANGELOG.md @@ -1,5 +1,11 @@ # @walkeros/web-destination-api +## 1.1.2 + +### Patch Changes + +- @walkeros/web-core@1.0.2 + ## 1.1.1 ### Patch Changes diff --git a/packages/web/destinations/api/package.json b/packages/web/destinations/api/package.json index e77e6769e..e3f04c2e6 100644 --- a/packages/web/destinations/api/package.json +++ b/packages/web/destinations/api/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/web-destination-api", "description": "Web API destination for walkerOS", - "version": "1.1.1", + "version": "1.1.2", "license": "MIT", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -35,7 +35,7 @@ "update": "npx npm-check-updates -u && npm update" }, "dependencies": { - "@walkeros/web-core": "1.0.1" + "@walkeros/web-core": "1.0.2" }, "devDependencies": {}, "repository": { diff --git a/packages/web/destinations/gtag/CHANGELOG.md b/packages/web/destinations/gtag/CHANGELOG.md index b0606fc3c..ecd46e007 100644 --- a/packages/web/destinations/gtag/CHANGELOG.md +++ b/packages/web/destinations/gtag/CHANGELOG.md @@ -1,5 +1,11 @@ # @walkeros/web-destination-gtag +## 1.0.2 + +### Patch Changes + +- @walkeros/web-core@1.0.2 + ## 1.0.1 ### Patch Changes diff --git a/packages/web/destinations/gtag/package.json b/packages/web/destinations/gtag/package.json index 17281f77c..cabbe29e5 100644 --- a/packages/web/destinations/gtag/package.json +++ b/packages/web/destinations/gtag/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/web-destination-gtag", "description": "Unified Google destination for walkerOS (GA4, Ads, GTM)", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -35,7 +35,7 @@ "update": "npx npm-check-updates -u && npm update" }, "dependencies": { - "@walkeros/web-core": "1.0.1" + "@walkeros/web-core": "1.0.2" }, "repository": { "url": "git+https://github.com/elbwalker/walkerOS.git", diff --git a/packages/web/destinations/meta/CHANGELOG.md b/packages/web/destinations/meta/CHANGELOG.md index 0a9866d03..4b960a384 100644 --- a/packages/web/destinations/meta/CHANGELOG.md +++ b/packages/web/destinations/meta/CHANGELOG.md @@ -1,5 +1,11 @@ # @walkeros/web-destination-meta +## 1.0.2 + +### Patch Changes + +- @walkeros/web-core@1.0.2 + ## 1.0.1 ### Patch Changes diff --git a/packages/web/destinations/meta/package.json b/packages/web/destinations/meta/package.json index 28858f8d6..a79df12ff 100644 --- a/packages/web/destinations/meta/package.json +++ b/packages/web/destinations/meta/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/web-destination-meta", "description": "Meta pixel web destination for walkerOS", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -35,7 +35,7 @@ "update": "npx npm-check-updates -u && npm update" }, "dependencies": { - "@walkeros/web-core": "1.0.1" + "@walkeros/web-core": "1.0.2" }, "devDependencies": { "@types/facebook-pixel": "^0.0.31" diff --git a/packages/web/destinations/piwikpro/CHANGELOG.md b/packages/web/destinations/piwikpro/CHANGELOG.md index 71f04e3f4..9f4183827 100644 --- a/packages/web/destinations/piwikpro/CHANGELOG.md +++ b/packages/web/destinations/piwikpro/CHANGELOG.md @@ -1,5 +1,14 @@ # @walkeros/web-destination-piwikpro +## 1.0.2 + +### Patch Changes + +- Updated dependencies [f39d9fb] +- Updated dependencies [888bbdf] + - @walkeros/core@1.2.0 + - @walkeros/web-core@1.0.2 + ## 1.0.1 ### Patch Changes diff --git a/packages/web/destinations/piwikpro/package.json b/packages/web/destinations/piwikpro/package.json index 39af982e5..fb961c082 100644 --- a/packages/web/destinations/piwikpro/package.json +++ b/packages/web/destinations/piwikpro/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/web-destination-piwikpro", "description": "Piwik PRO destination for walkerOS", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -35,8 +35,8 @@ "update": "npx npm-check-updates -u && npm update" }, "dependencies": { - "@walkeros/core": "1.1.0", - "@walkeros/web-core": "1.0.1" + "@walkeros/core": "1.2.0", + "@walkeros/web-core": "1.0.2" }, "devDependencies": {}, "repository": { diff --git a/packages/web/destinations/plausible/CHANGELOG.md b/packages/web/destinations/plausible/CHANGELOG.md index c79723490..55678b1bc 100644 --- a/packages/web/destinations/plausible/CHANGELOG.md +++ b/packages/web/destinations/plausible/CHANGELOG.md @@ -1,5 +1,11 @@ # @walkeros/web-destination-plausible +## 1.0.2 + +### Patch Changes + +- @walkeros/web-core@1.0.2 + ## 1.0.1 ### Patch Changes diff --git a/packages/web/destinations/plausible/package.json b/packages/web/destinations/plausible/package.json index d79f2c042..2f123a5ca 100644 --- a/packages/web/destinations/plausible/package.json +++ b/packages/web/destinations/plausible/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/web-destination-plausible", "description": "Plausible web destination for walkerOS", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -35,7 +35,7 @@ "update": "npx npm-check-updates -u && npm update" }, "dependencies": { - "@walkeros/web-core": "1.0.1" + "@walkeros/web-core": "1.0.2" }, "devDependencies": {}, "repository": { diff --git a/packages/web/destinations/snowplow/CHANGELOG.md b/packages/web/destinations/snowplow/CHANGELOG.md index ee8b30c78..3cb58eca8 100644 --- a/packages/web/destinations/snowplow/CHANGELOG.md +++ b/packages/web/destinations/snowplow/CHANGELOG.md @@ -1,5 +1,11 @@ # @walkeros/web-destination-snowplow +## 0.0.9 + +### Patch Changes + +- @walkeros/web-core@1.0.2 + ## 0.0.8 ### Patch Changes diff --git a/packages/web/destinations/snowplow/package.json b/packages/web/destinations/snowplow/package.json index e8564aee3..8ee1c1685 100644 --- a/packages/web/destinations/snowplow/package.json +++ b/packages/web/destinations/snowplow/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/web-destination-snowplow", "description": "Snowplow web destination for walkerOS", - "version": "0.0.8", + "version": "0.0.9", "license": "MIT", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -35,7 +35,7 @@ "update": "npx npm-check-updates -u && npm update" }, "dependencies": { - "@walkeros/web-core": "1.0.1" + "@walkeros/web-core": "1.0.2" }, "devDependencies": { "@walkeros/collector": "latest", diff --git a/packages/web/sources/browser/CHANGELOG.md b/packages/web/sources/browser/CHANGELOG.md index 37cc14f5b..a9b3920da 100644 --- a/packages/web/sources/browser/CHANGELOG.md +++ b/packages/web/sources/browser/CHANGELOG.md @@ -1,5 +1,28 @@ # @walkeros/web-source-browser +## 1.1.0 + +### Minor Changes + +- a38d791: Session detection extracted to standalone sourceSession + - New `sourceSession` for composable session management + - Browser source no longer includes session by default + - To restore previous behavior, add sourceSession alongside browser source: + + ```typescript + sources: { + browser: sourceBrowser, + session: { code: sourceSession, config: { storage: true } } + } + ``` + +### Patch Changes + +- Updated dependencies [f39d9fb] +- Updated dependencies [888bbdf] + - @walkeros/collector@1.1.0 + - @walkeros/web-core@1.0.2 + ## 1.0.1 ### Patch Changes diff --git a/packages/web/sources/browser/package.json b/packages/web/sources/browser/package.json index 8c74c5478..fcc4906f9 100644 --- a/packages/web/sources/browser/package.json +++ b/packages/web/sources/browser/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/web-source-browser", "description": "Browser DOM source for walkerOS", - "version": "1.0.1", + "version": "1.1.0", "license": "MIT", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -30,8 +30,8 @@ "update": "npx npm-check-updates -u && npm update" }, "dependencies": { - "@walkeros/collector": "1.0.1", - "@walkeros/web-core": "1.0.1" + "@walkeros/collector": "1.1.0", + "@walkeros/web-core": "1.0.2" }, "repository": { "url": "git+https://github.com/elbwalker/walkerOS.git", diff --git a/packages/web/sources/browser/src/__tests__/session.test.ts b/packages/web/sources/browser/src/__tests__/session.test.ts deleted file mode 100644 index b8f7508df..000000000 --- a/packages/web/sources/browser/src/__tests__/session.test.ts +++ /dev/null @@ -1,163 +0,0 @@ -import type { WalkerOS, Collector, Elb } from '@walkeros/core'; -import { createSessionStart, sessionStart } from '../session'; - -// Mock dependencies - declare before use in jest.mock -jest.mock('@walkeros/core', () => ({ - ...jest.requireActual('@walkeros/core'), - assign: jest.fn((target, source) => Object.assign({}, target, source)), - isSameType: jest.fn(() => true), - useHooks: jest.fn(), -})); - -jest.mock('@walkeros/collector', () => ({ - onApply: jest.fn(), -})); - -jest.mock('@walkeros/web-core', () => ({ - sessionStart: jest.fn().mockReturnValue({ - id: 'test-session', - start: Date.now(), - isStart: true, - storage: true, - }), -})); - -// Get references to the mocked functions -const { useHooks } = require('@walkeros/core'); -const { onApply } = require('@walkeros/collector'); -const { sessionStart: sessionStartOrg } = require('@walkeros/web-core'); - -describe('Session', () => { - let mockElb: jest.MockedFunction; - - beforeEach(() => { - jest.clearAllMocks(); - - mockElb = jest.fn().mockImplementation(() => - Promise.resolve({ - ok: true, - }), - ); - - // Mock the useHooks to return a function that calls the original - (useHooks as jest.Mock).mockImplementation(() => { - return () => ({ - id: 'new-session-id', - start: Date.now(), - isStart: true, - storage: true, - }); - }); - }); - - test('createSessionStart returns a function', () => { - const sessionStartFn = createSessionStart(mockElb); - - expect(typeof sessionStartFn).toBe('function'); - }); - - test('createSessionStart function calls sessionStart with correct config', () => { - const sessionStartFn = createSessionStart(mockElb); - const options = { - config: { storage: false }, - data: { - isStart: true, - storage: true, - }, - }; - - const result = sessionStartFn(options); - - expect(result).toBeDefined(); - expect(sessionStartOrg).toHaveBeenCalled(); - }); - - test('createSessionStart merges config with default pulse setting', () => { - const sessionStartFn = createSessionStart(mockElb); - const customConfig = { storage: false, custom: true }; - - sessionStartFn({ config: customConfig }); - - expect(sessionStartOrg).toHaveBeenCalled(); - }); - - test('createSessionStart updates session data with current timestamp', () => { - const sessionStartFn = createSessionStart(mockElb); - const beforeCall = Date.now(); - - sessionStartFn({}); - - const afterCall = Date.now(); - - expect(sessionStartOrg).toHaveBeenCalled(); - }); - - test('sessionStart processes options correctly', () => { - const options = { - config: { storage: true }, - data: { - isStart: true, - storage: true, - }, - }; - - const result = sessionStart(mockElb, options); - - expect(sessionStartOrg).toHaveBeenCalled(); - }); - - test('sessionStart handles empty options', () => { - const result = sessionStart(mockElb); - - expect(sessionStartOrg).toHaveBeenCalled(); - expect(result).toBeDefined(); - }); - - test('sessionStart merges session config correctly', () => { - const options = { - storage: false, - }; - - sessionStart(mockElb, options); - - expect(sessionStartOrg).toHaveBeenCalled(); - }); - - test('sessionStart handles missing session config', () => { - const result = sessionStart(mockElb, {}); - - expect(sessionStartOrg).toHaveBeenCalled(); - expect(result).toBeDefined(); - }); - - test('sessionStart handles missing sessionStatic config', () => { - const result = sessionStart(mockElb, {}); - - expect(sessionStartOrg).toHaveBeenCalled(); - expect(result).toBeDefined(); - }); - - test('session callback function updates collector session', () => { - const result = sessionStart(mockElb, {}); - - expect(sessionStartOrg).toHaveBeenCalled(); - expect(result).toBeDefined(); - }); - - test('session callback respects cb: false configuration', () => { - const options = { - cb: false as const, - }; - - const result = sessionStart(mockElb, options); - - expect(sessionStartOrg).toHaveBeenCalled(); - }); - - test('session returns the session data from useHooks', () => { - const result = sessionStart(mockElb, {}); - - expect(sessionStartOrg).toHaveBeenCalled(); - expect(result).toBeDefined(); - }); -}); diff --git a/packages/web/sources/browser/src/__tests__/test-utils.ts b/packages/web/sources/browser/src/__tests__/test-utils.ts index 6ac8c6ab5..8264271e8 100644 --- a/packages/web/sources/browser/src/__tests__/test-utils.ts +++ b/packages/web/sources/browser/src/__tests__/test-utils.ts @@ -16,7 +16,6 @@ export async function createBrowserSource( prefix: 'data-elb', scope: document, pageview: false, - session: false, elb: 'elb', elbLayer: 'elbLayer', ...settings, diff --git a/packages/web/sources/browser/src/__tests__/translation.test.ts b/packages/web/sources/browser/src/__tests__/translation.test.ts index b6f830af5..c10afea5e 100644 --- a/packages/web/sources/browser/src/__tests__/translation.test.ts +++ b/packages/web/sources/browser/src/__tests__/translation.test.ts @@ -9,7 +9,6 @@ const createTestSettings = (prefix = 'data-elb'): Settings => ({ prefix, scope: document, pageview: false, - session: false, elb: '', elbLayer: false, }); diff --git a/packages/web/sources/browser/src/__tests__/trigger.test.ts b/packages/web/sources/browser/src/__tests__/trigger.test.ts index e6a5900ea..f8e5fee17 100644 --- a/packages/web/sources/browser/src/__tests__/trigger.test.ts +++ b/packages/web/sources/browser/src/__tests__/trigger.test.ts @@ -15,7 +15,6 @@ const createTestSettings = (prefix = 'data-elb'): Settings => ({ prefix, scope: document, pageview: false, - session: false, elb: '', elbLayer: false, }); @@ -118,7 +117,6 @@ describe('Trigger System', () => { prefix: 'data-elb', scope: document, pageview: true, - session: true, elb: 'elb', elbLayer: 'elbLayer', }); @@ -206,7 +204,6 @@ describe('Trigger System', () => { prefix: 'data-elb', scope: document, pageview: false, - session: false, elb: '', elbLayer: false, } as Settings; diff --git a/packages/web/sources/browser/src/__tests__/triggerVisible.test.ts b/packages/web/sources/browser/src/__tests__/triggerVisible.test.ts index 3fe8306c6..b9e14ccbd 100644 --- a/packages/web/sources/browser/src/__tests__/triggerVisible.test.ts +++ b/packages/web/sources/browser/src/__tests__/triggerVisible.test.ts @@ -16,7 +16,6 @@ const createTestContext = (elb: Elb.Fn, prefix = 'data-elb'): Context => ({ prefix, scope: document, pageview: false, - session: false, elb: '', elbLayer: false, }, @@ -158,7 +157,6 @@ describe('triggerVisible', () => { prefix: 'data-elb', scope: expect.any(Object), pageview: false, - session: false, elb: '', elbLayer: false, }), @@ -198,7 +196,6 @@ describe('triggerVisible', () => { prefix: 'data-elb', scope: expect.any(Object), pageview: false, - session: false, elb: '', elbLayer: false, }), diff --git a/packages/web/sources/browser/src/config.ts b/packages/web/sources/browser/src/config.ts index 363faca90..3707442ea 100644 --- a/packages/web/sources/browser/src/config.ts +++ b/packages/web/sources/browser/src/config.ts @@ -13,7 +13,6 @@ export function getConfig( return { prefix: 'data-elb', pageview: true, - session: true, elb: 'elb', elbLayer: 'elbLayer', scope: envDocument || undefined, diff --git a/packages/web/sources/browser/src/elbLayer.ts b/packages/web/sources/browser/src/elbLayer.ts index afae1a306..4b535b6a2 100644 --- a/packages/web/sources/browser/src/elbLayer.ts +++ b/packages/web/sources/browser/src/elbLayer.ts @@ -173,7 +173,6 @@ function pushCommand( prefix, scope: document, pageview: false, - session: false, elb: '', elbLayer: false, }, diff --git a/packages/web/sources/browser/src/index.ts b/packages/web/sources/browser/src/index.ts index 776728cf2..ae0b7e1e6 100644 --- a/packages/web/sources/browser/src/index.ts +++ b/packages/web/sources/browser/src/index.ts @@ -11,7 +11,6 @@ import { initTriggers, processLoadTriggers, ready } from './trigger'; import { destroyVisibilityTracking } from './triggerVisible'; import { initElbLayer } from './elbLayer'; import { translateToCoreCollector } from './translation'; -import { sessionStart } from './session'; import { getPageViewData } from './walker'; import { getConfig } from './config'; @@ -36,7 +35,7 @@ export type { TaggerConfig, TaggerInstance } from './tagger'; /** * Browser source implementation using environment injection. * - * This source captures DOM events, manages sessions, handles pageviews, + * This source captures DOM events, handles pageviews, * and processes the elbLayer for browser environments. */ export const sourceBrowser: Source.Init = async (context) => { @@ -67,6 +66,23 @@ export const sourceBrowser: Source.Init = async (context) => { settings, }; + // Helper to send pageview event if enabled + const sendPageview = (settings: Source.Settings) => { + if (settings.pageview) { + const [data, contextData] = getPageViewData( + settings.prefix || 'data-elb', + settings.scope as Scope, + ); + translateToCoreCollector( + translationContext, + 'page view', + data, + 'load', + contextData, + ); + } + }; + // Initialize features if environment is available // Skip all DOM-related functionality when not in browser environment @@ -80,37 +96,16 @@ export const sourceBrowser: Source.Init = async (context) => { }); } - // Initialize session if enabled - if (settings.session && elb) { - const sessionConfig = - typeof settings.session === 'boolean' ? {} : settings.session; - sessionStart(elb, sessionConfig, command); - } - // Setup global triggers (click, submit) when DOM is ready await ready(initTriggers, translationContext, settings); // Setup load triggers and pageview on each run const handleRun = () => { processLoadTriggers(translationContext, settings); - - // Send pageview if enabled - if (settings.pageview) { - const [data, context] = getPageViewData( - settings.prefix || 'data-elb', - settings.scope as Scope, - ); - translateToCoreCollector( - translationContext, - 'page view', - data, - 'load', - context, - ); - } + sendPageview(settings); }; - // Trigger initial run if this is a new session/page load + // Trigger initial run on page load handleRun(); // Set up automatic window.elb assignment if configured @@ -132,24 +127,9 @@ export const sourceBrowser: Source.Init = async (context) => { } } - // Handle events pushed from collector (consent, session, ready, run) - const handleEvent = async (event: On.Types, context?: unknown) => { + // Handle events pushed from collector (ready, run) + const handleEvent = async (event: On.Types) => { switch (event) { - case 'consent': - // React to consent changes - sources can implement specific consent handling - // For browser source, we might want to re-evaluate session settings - if (settings.session && context && elb) { - const sessionConfig = - typeof settings.session === 'boolean' ? {} : settings.session; - sessionStart(elb, sessionConfig, command); - } - break; - - case 'session': - // React to session events if needed - // Browser source typically handles session creation, not reaction - break; - case 'ready': // React to collector ready state // Browser source initialization already handles this @@ -159,21 +139,7 @@ export const sourceBrowser: Source.Init = async (context) => { // React to collector run events - re-process load triggers if (actualDocument && actualWindow) { processLoadTriggers(translationContext, settings); - - // Send pageview if enabled - if (settings.pageview) { - const [data, contextData] = getPageViewData( - settings.prefix || 'data-elb', - settings.scope as Scope, - ); - translateToCoreCollector( - translationContext, - 'page view', - data, - 'load', - contextData, - ); - } + sendPageview(settings); } break; diff --git a/packages/web/sources/browser/src/schemas/settings.ts b/packages/web/sources/browser/src/schemas/settings.ts index 87371d314..eebcdd4de 100644 --- a/packages/web/sources/browser/src/schemas/settings.ts +++ b/packages/web/sources/browser/src/schemas/settings.ts @@ -5,20 +5,6 @@ import { ScopeSelector, } from './primitives'; -/** - * Session configuration schema - * Note: Runtime type can be boolean | SessionConfig - * SessionConfig is non-serializable, so we use z.any() for the complex form - */ -const SessionConfigSchema = z.union([ - z.boolean(), - z - .any() - .describe( - 'SessionConfig object from @walkeros/web-core with tracking settings', - ), -]); - /** * ELB Layer configuration schema * Note: Runtime type can be boolean | string | Elb.Layer @@ -46,10 +32,6 @@ export const SettingsSchema = z.object({ .default(true) .describe('Enable automatic pageview tracking'), - session: SessionConfigSchema.default(true).describe( - 'Enable session tracking (boolean or SessionConfig object)', - ), - elb: JavaScriptVarName.default('elb').describe( 'Name for global elb function', ), diff --git a/packages/web/sources/browser/src/session.ts b/packages/web/sources/browser/src/session.ts deleted file mode 100644 index 9b5b2a93e..000000000 --- a/packages/web/sources/browser/src/session.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { Collector, Elb } from '@walkeros/core'; -import { - sessionStart as sessionStartOrg, - SessionConfig, -} from '@walkeros/web-core'; - -export function createSessionStart(elb: Elb.Fn, command?: Collector.CommandFn) { - return function ( - options: { config?: SessionConfig; data?: Collector.SessionData } = {}, - ): void | Collector.SessionData { - const { config = {} } = options; - return sessionStart( - elb, - { - ...config, - pulse: config.pulse !== undefined ? config.pulse : true, - }, - command, - ); - }; -} - -export function sessionStart( - elb: Elb.Fn, - config: SessionConfig = {}, - command?: Collector.CommandFn, -): void | Collector.SessionData { - // Create minimal collector interface for sessionStart that needs elb and group tracking - const collectorInterface: Partial = { - push: elb, - group: undefined, // Session tracking doesn't need group initially - command, // Include command for session to call walker command - }; - - return sessionStartOrg({ - ...config, - collector: collectorInterface as Collector.Instance, - }); -} diff --git a/packages/web/sources/browser/src/types/index.ts b/packages/web/sources/browser/src/types/index.ts index add0b4b13..522186f70 100644 --- a/packages/web/sources/browser/src/types/index.ts +++ b/packages/web/sources/browser/src/types/index.ts @@ -1,5 +1,4 @@ -import type { Source, Elb, Collector } from '@walkeros/core'; -import type { SessionConfig, SessionCallback } from '@walkeros/web-core'; +import type { Source, Elb } from '@walkeros/core'; import type { SettingsSchema } from '../schemas'; import { z } from '@walkeros/core/dev'; @@ -12,19 +11,17 @@ type BaseSettings = z.infer; // InitSettings: what users provide (all optional) // Override specific fields with non-serializable types -export interface InitSettings - extends Partial> { +export interface InitSettings extends Partial< + Omit +> { scope?: Element | Document; - session?: boolean | SessionConfig; elbLayer?: boolean | string | Elb.Layer; } // Settings: resolved configuration // Override specific fields with non-serializable types -export interface Settings - extends Omit { +export interface Settings extends Omit { scope?: Element | Document; - session: boolean | SessionConfig; elbLayer: boolean | string | Elb.Layer; } @@ -47,9 +44,6 @@ export interface Context { settings: Settings; } -// Re-export session types from web-core to avoid duplication -export type { SessionConfig, SessionCallback }; - // ELB Layer types for async command handling export type ELBLayer = Array; export interface ELBLayerConfig { diff --git a/packages/web/sources/dataLayer/CHANGELOG.md b/packages/web/sources/dataLayer/CHANGELOG.md index 0904093f0..01afa8af9 100644 --- a/packages/web/sources/dataLayer/CHANGELOG.md +++ b/packages/web/sources/dataLayer/CHANGELOG.md @@ -1,5 +1,14 @@ # @walkeros/web-source-datalayer +## 1.0.2 + +### Patch Changes + +- Updated dependencies [f39d9fb] +- Updated dependencies [888bbdf] + - @walkeros/collector@1.1.0 + - @walkeros/core@1.2.0 + ## 1.0.1 ### Patch Changes diff --git a/packages/web/sources/dataLayer/package.json b/packages/web/sources/dataLayer/package.json index 03b5d0658..1ee4ba73b 100644 --- a/packages/web/sources/dataLayer/package.json +++ b/packages/web/sources/dataLayer/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/web-source-datalayer", "description": "DataLayer source for walkerOS", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -35,8 +35,8 @@ "update": "npx npm-check-updates -u && npm update" }, "dependencies": { - "@walkeros/core": "1.1.0", - "@walkeros/collector": "1.0.1" + "@walkeros/core": "1.2.0", + "@walkeros/collector": "1.1.0" }, "devDependencies": { "@types/gtag.js": "^0.0.20" diff --git a/packages/web/sources/session/CHANGELOG.md b/packages/web/sources/session/CHANGELOG.md new file mode 100644 index 000000000..d93fd5310 --- /dev/null +++ b/packages/web/sources/session/CHANGELOG.md @@ -0,0 +1,24 @@ +# @walkeros/web-source-session + +## 1.1.0 + +### Minor Changes + +- a38d791: Session detection extracted to standalone sourceSession + - New `sourceSession` for composable session management + - Browser source no longer includes session by default + - To restore previous behavior, add sourceSession alongside browser source: + + ```typescript + sources: { + browser: sourceBrowser, + session: { code: sourceSession, config: { storage: true } } + } + ``` + +### Patch Changes + +- Updated dependencies [f39d9fb] +- Updated dependencies [888bbdf] + - @walkeros/core@1.2.0 + - @walkeros/web-core@1.0.2 diff --git a/packages/web/sources/session/README.md b/packages/web/sources/session/README.md new file mode 100644 index 000000000..0feb869e8 --- /dev/null +++ b/packages/web/sources/session/README.md @@ -0,0 +1,212 @@ +# @walkeros/web-source-session + +Standalone session detection and management for walkerOS. + +[Source Code](https://github.com/elbwalker/walkerOS/tree/main/packages/web/sources/session) +| [NPM](https://www.npmjs.com/package/@walkeros/web-source-session) | +[Documentation](https://www.walkeros.io/docs/sources/web/session) + +## Quick Start + +```typescript +import { startFlow } from '@walkeros/collector'; +import { sourceBrowser } from '@walkeros/web-source-browser'; +import { sourceSession } from '@walkeros/web-source-session'; + +const { collector, elb } = await startFlow({ + sources: { + browser: sourceBrowser, + session: { + code: sourceSession, + config: { + settings: { + storage: true, // Enable persistent session storage + }, + }, + }, + }, +}); +``` + +## Features + +- **Session detection**: Automatic detection of new sessions based on + navigation, referrer, and marketing parameters +- **Device tracking**: Persistent device ID across sessions (with consent) +- **Consent-aware**: Switches between window-only and storage-based sessions + based on user consent +- **Composable**: Works alongside any source (browser, dataLayer, custom) + +## Installation + +```bash +npm install @walkeros/web-source-session +``` + +## Why Separate Session Source? + +The session source was extracted from the browser source to enable: + +1. **Composability**: Use session detection with any source, not just browser +2. **Single responsibility**: Browser source focuses on DOM events, session + source on identity +3. **Flexibility**: Configure session independently from event capture +4. **Server-side ready**: Session logic can work with server sources too + +## Configuration Reference + +| Name | Type | Description | Default | +| ---------------- | -------------------------- | ------------------------------------------------ | ---------------- | +| `storage` | `boolean` | Enable persistent storage for session/device IDs | `false` | +| `consent` | `string \| string[]` | Consent key(s) required to enable storage mode | - | +| `length` | `number` | Session timeout in minutes | `30` | +| `pulse` | `boolean` | Keep session alive on each event | `false` | +| `sessionKey` | `string` | Storage key for session ID | `'elbSessionId'` | +| `sessionStorage` | `'local' \| 'session'` | Storage type for session | `'local'` | +| `deviceKey` | `string` | Storage key for device ID | `'elbDeviceId'` | +| `deviceStorage` | `'local' \| 'session'` | Storage type for device | `'local'` | +| `cb` | `SessionCallback \| false` | Custom callback or disable default | - | + +## Examples + +### Basic (Window-only) + +No persistent storage, session detected per page load: + +```typescript +const { elb } = await startFlow({ + sources: { + browser: sourceBrowser, + session: sourceSession, // Defaults to window-only mode + }, +}); +``` + +### With Persistent Storage + +Store session and device IDs in localStorage: + +```typescript +const { elb } = await startFlow({ + sources: { + browser: sourceBrowser, + session: { + code: sourceSession, + config: { + settings: { + storage: true, + length: 30, // 30-minute session timeout + }, + }, + }, + }, +}); +``` + +### Consent-Aware + +Switch to storage mode only when user grants consent: + +```typescript +const { elb } = await startFlow({ + sources: { + browser: sourceBrowser, + session: { + code: sourceSession, + config: { + settings: { + consent: 'analytics', // Wait for analytics consent + storage: true, + }, + }, + }, + }, +}); + +// Later, when user grants consent: +elb('walker consent', { analytics: true }); +// Session source automatically switches to storage mode +``` + +## Session Start Event + +When a new session is detected, the source pushes a `session start` event: + +```typescript +{ + name: 'session start', + data: { + isStart: true, + id: 'abc123', // Session ID + device: 'xyz789', // Device ID (if storage enabled) + storage: true, // Whether storage mode is active + // ... additional session metadata + } +} +``` + +## Migration from Browser Source + +If you were using the browser source with session enabled: + +```typescript +// Before (browser source with built-in session) +const { elb } = await startFlow({ + sources: { + browser: { + code: sourceBrowser, + config: { settings: { session: true } }, + }, + }, +}); + +// After (separate session source) +const { elb } = await startFlow({ + sources: { + browser: sourceBrowser, + session: { + code: sourceSession, + config: { settings: { storage: true } }, + }, + }, +}); +``` + +## Exported Functions + +The session source exports session functions for direct usage: + +```typescript +import { + // Session functions + sessionStart, + sessionWindow, + sessionStorage, +} from '@walkeros/web-source-session'; + +// Use sessionStart directly (advanced usage) +const session = sessionStart({ + storage: true, + collector: collectorInstance, +}); +``` + +Storage utilities are available from `@walkeros/web-core`: + +```typescript +import { storageRead, storageWrite, storageDelete } from '@walkeros/web-core'; + +storageWrite('key', 'value', 30); // 30-minute expiration +const value = storageRead('key'); +storageDelete('key'); +``` + +## Type Definitions + +See [src/types/index.ts](./src/types/index.ts) for TypeScript interfaces. + +## Related + +- [Browser Source](/docs/sources/web/browser) - DOM-based event tracking +- [DataLayer Source](/docs/sources/web/dataLayer) - GTM/GA4 integration +- [Session Documentation](https://www.walkeros.io/docs/sources/web/session) diff --git a/packages/web/sources/session/jest.config.mjs b/packages/web/sources/session/jest.config.mjs new file mode 100644 index 000000000..9f5dcec3d --- /dev/null +++ b/packages/web/sources/session/jest.config.mjs @@ -0,0 +1,5 @@ +import baseConfig from '@walkeros/config/jest/web.config'; + +const config = {}; + +export default { ...baseConfig, ...config }; diff --git a/packages/web/sources/session/package.json b/packages/web/sources/session/package.json new file mode 100644 index 000000000..f305d1131 --- /dev/null +++ b/packages/web/sources/session/package.json @@ -0,0 +1,53 @@ +{ + "name": "@walkeros/web-source-session", + "description": "Session source for walkerOS", + "version": "1.1.0", + "license": "MIT", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + }, + "./dev": { + "types": "./dist/dev.d.ts", + "import": "./dist/dev.mjs", + "require": "./dist/dev.js" + } + }, + "files": [ + "dist/**" + ], + "scripts": { + "build": "tsup --silent", + "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist", + "dev": "jest --watchAll --colors", + "lint": "tsc && eslint \"**/*.ts*\"", + "test": "jest", + "update": "npx npm-check-updates -u && npm update" + }, + "dependencies": { + "@walkeros/core": "1.2.0", + "@walkeros/web-core": "1.0.2" + }, + "devDependencies": {}, + "repository": { + "url": "git+https://github.com/elbwalker/walkerOS.git", + "directory": "packages/web/sources/session" + }, + "author": "elbwalker ", + "homepage": "https://github.com/elbwalker/walkerOS#readme", + "bugs": { + "url": "https://github.com/elbwalker/walkerOS/issues" + }, + "keywords": [ + "walker", + "walkerOS", + "source", + "web", + "session" + ] +} diff --git a/packages/web/sources/session/src/__tests__/index.test.ts b/packages/web/sources/session/src/__tests__/index.test.ts new file mode 100644 index 000000000..da07c61d3 --- /dev/null +++ b/packages/web/sources/session/src/__tests__/index.test.ts @@ -0,0 +1,101 @@ +import { startFlow } from '@walkeros/collector'; +import type { WalkerOS, Collector } from '@walkeros/core'; +import { + createMockPush, + createMockCommand, + createSessionSource, +} from './test-utils'; + +describe('Session Source', () => { + let collectedEvents: WalkerOS.Event[]; + let collector: Collector.Instance; + let mockCommand: jest.MockedFunction; + + beforeEach(async () => { + collectedEvents = []; + mockCommand = createMockCommand(); + + // Initialize collector + ({ collector } = await startFlow({ + tagging: 2, + })); + + // Override push with synchronous mock + collector.push = createMockPush(collectedEvents); + collector.command = mockCommand; + }); + + test('source initializes without errors', async () => { + expect(async () => { + await createSessionSource(collector); + }).not.toThrow(); + }); + + test('returns source instance with type "session"', async () => { + const source = await createSessionSource(collector); + + expect(source.type).toBe('session'); + }); + + test('returns source instance with push function', async () => { + const source = await createSessionSource(collector); + + expect(source.push).toBeDefined(); + expect(typeof source.push).toBe('function'); + }); + + test('returns source instance with on function', async () => { + const source = await createSessionSource(collector); + + expect(source.on).toBeDefined(); + expect(typeof source.on).toBe('function'); + }); + + describe('Session Start', () => { + test('calls sessionStart on initialization', async () => { + await createSessionSource(collector); + + // Session start should have been called, which calls command('user', ...) + expect(mockCommand).toHaveBeenCalled(); + }); + + test('pushes session start event when session is new', async () => { + await createSessionSource(collector); + + // Should have pushed a session start event + const sessionEvent = collectedEvents.find( + (e) => e.name === 'session start', + ); + expect(sessionEvent).toBeDefined(); + }); + }); + + describe('Consent Handling', () => { + test('re-initializes session on consent event', async () => { + const source = await createSessionSource(collector); + + // Clear previous calls + mockCommand.mockClear(); + + // Trigger consent event + await source.on?.('consent'); + + // Session should be re-initialized + expect(mockCommand).toHaveBeenCalled(); + }); + + test('does not re-initialize on other events', async () => { + const source = await createSessionSource(collector); + + // Clear previous calls + mockCommand.mockClear(); + + // Trigger other events + await source.on?.('ready'); + await source.on?.('run'); + + // Session should NOT be re-initialized for these events + expect(mockCommand).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/web/sources/session/src/__tests__/sessionStart.test.ts b/packages/web/sources/session/src/__tests__/sessionStart.test.ts new file mode 100644 index 000000000..0873fb0b2 --- /dev/null +++ b/packages/web/sources/session/src/__tests__/sessionStart.test.ts @@ -0,0 +1,204 @@ +import type { On, Collector } from '@walkeros/core'; +import { sessionStart, sessionStorage, sessionWindow } from '../lib'; + +let consent: On.ConsentConfig = {}; + +jest.mock('../lib/sessionStorage', () => ({ + sessionStorage: jest.fn().mockImplementation((config) => { + return { ...config.data, mock: 'storage' }; + }), +})); +jest.mock('../lib/sessionWindow', () => ({ + sessionWindow: jest.fn().mockImplementation((config) => { + return { ...config.data, mock: 'window' }; + }), +})); + +describe('sessionStart', () => { + const w = window; + + const mockSessionStorage = sessionStorage as jest.Mock; + const mockSessionWindow = sessionWindow as jest.Mock; + + const createMockCollector = () => ({ + command: jest.fn().mockImplementation((cmd, type, options) => { + if (cmd === 'on' && type === 'consent') { + consent = options; + } + }), + push: jest.fn(), + group: undefined, + }); + + beforeEach(() => { + Object.defineProperty(w, 'performance', { + value: { + getEntriesByType: jest.fn().mockReturnValue([{ type: 'navigate' }]), + }, + writable: true, + }); + + jest.clearAllMocks(); + jest.resetModules(); + + consent = {}; + }); + + test('Default', () => { + sessionStart(); + expect(mockSessionWindow).toHaveBeenCalledWith({}); + }); + + test('Storage', () => { + sessionStart({ storage: true }); + expect(mockSessionStorage).toHaveBeenCalledWith({ storage: true }); + }); + + test('Consent with collector', () => { + const consentName = 'foo'; + const collector = createMockCollector() as unknown as Collector.Instance; + const config = { consent: consentName, collector }; + sessionStart(config); + + expect(collector.command).toHaveBeenCalledTimes(1); + expect(collector.command).toHaveBeenCalledWith('on', 'consent', { + foo: expect.any(Function), + }); + + // Simulate granted consent call from collector + expect(mockSessionStorage).toHaveBeenCalledTimes(0); + consent[consentName](collector, { + [consentName]: true, + }); + expect(mockSessionStorage).toHaveBeenCalledWith(config); + + // Denied + expect(mockSessionWindow).toHaveBeenCalledTimes(0); + consent[consentName](collector, { + [consentName]: false, + }); + expect(mockSessionWindow).toHaveBeenCalledWith(config); + }); + + test('Callback without consent', () => { + const collector = createMockCollector() as unknown as Collector.Instance; + const mockCb = jest.fn(); + const config = { cb: mockCb, collector, storage: false }; + sessionStart(config); + + expect(mockCb).toHaveBeenCalledTimes(1); + expect(mockCb).toHaveBeenCalledWith( + { + mock: 'window', + }, + collector, + expect.any(Function), + ); + }); + + test('Callback with consent', () => { + const consentName = 'foo'; + const collector = createMockCollector() as unknown as Collector.Instance; + const mockCb = jest.fn(); + const config = { + cb: mockCb, + consent: consentName, + storage: true, + collector, + }; + sessionStart(config); + + // Granted, use sessionStorage + consent[consentName](collector, { + [consentName]: true, + }); + expect(mockCb).toHaveBeenCalledTimes(1); + expect(mockCb).toHaveBeenCalledWith( + { + mock: 'storage', + }, + collector, + expect.any(Function), + ); + + // Denied, use sessionWindow + consent[consentName](collector, { + [consentName]: false, + }); + expect(mockCb).toHaveBeenCalledTimes(2); + expect(mockCb).toHaveBeenCalledWith( + { + mock: 'window', + }, + collector, + expect.any(Function), + ); + }); + + test('Callback default with collector', () => { + const collector = createMockCollector() as unknown as Collector.Instance; + + // No push calls if no session is started + sessionStart({ collector }); + expect(collector.command).toHaveBeenCalledTimes(1); + expect(collector.command).toHaveBeenCalledWith('user', expect.any(Object)); + + jest.clearAllMocks(); + sessionStart({ data: { isStart: true }, collector }); + expect(collector.command).toHaveBeenCalledTimes(1); + expect(collector.command).toHaveBeenCalledWith('user', expect.any(Object)); + expect(collector.push).toHaveBeenCalledTimes(1); + expect(collector.push).toHaveBeenCalledWith({ + name: 'session start', + data: expect.any(Object), + }); + }); + + test('Callback default storage', () => { + const collector = createMockCollector() as unknown as Collector.Instance; + sessionStart({ + data: { storage: true, isNew: true, device: 'd3v1c3', id: 's3ss10n' }, + collector, + }); + expect(collector.command).toHaveBeenCalledWith('user', { + device: 'd3v1c3', + session: 's3ss10n', + }); + }); + + test('Callback disabled', () => { + const collector = createMockCollector() as unknown as Collector.Instance; + // No push calls if callback is disabled + sessionStart({ cb: false, data: { isStart: true }, collector }); + expect(collector.push).toHaveBeenCalledTimes(0); + expect(collector.command).toHaveBeenCalledTimes(0); + }); + + test('Callback default push calls', () => { + const collector = createMockCollector() as unknown as Collector.Instance; + const session = sessionStart({ + data: { isNew: true, isStart: true }, + collector, + }); + expect(collector.push).toHaveBeenCalledWith({ + name: 'session start', + data: session, + }); + }); + + test('multiple consent keys', () => { + const collector = createMockCollector() as unknown as Collector.Instance; + sessionStart({ consent: ['foo', 'bar'], collector }); + expect(collector.command).toHaveBeenCalledWith('on', 'consent', { + foo: expect.any(Function), + bar: expect.any(Function), + }); + }); + + test('without collector - no crash', () => { + // Session start should work without collector (just no commands/push) + expect(() => sessionStart()).not.toThrow(); + expect(() => sessionStart({ storage: true })).not.toThrow(); + expect(() => sessionStart({ data: { isStart: true } })).not.toThrow(); + }); +}); diff --git a/packages/web/core/src/__tests__/sessionStorage.test.ts b/packages/web/sources/session/src/__tests__/sessionStorage.test.ts similarity index 96% rename from packages/web/core/src/__tests__/sessionStorage.test.ts rename to packages/web/sources/session/src/__tests__/sessionStorage.test.ts index 351c421db..25ca6944f 100644 --- a/packages/web/core/src/__tests__/sessionStorage.test.ts +++ b/packages/web/sources/session/src/__tests__/sessionStorage.test.ts @@ -1,8 +1,9 @@ -import { sessionStorage, storageRead, storageWrite } from '..'; +import { sessionStorage } from '../lib'; +import { storageRead, storageWrite } from '@walkeros/web-core'; -// Automatically mock the storage module -jest.mock('../storage', () => ({ - ...jest.requireActual('../storage'), +// Automatically mock the storage module from web-core +jest.mock('@walkeros/web-core', () => ({ + ...jest.requireActual('@walkeros/web-core'), storageRead: jest.fn(), storageWrite: jest.fn(), })); diff --git a/packages/web/core/src/__tests__/sessionWindow.test.ts b/packages/web/sources/session/src/__tests__/sessionWindow.test.ts similarity index 98% rename from packages/web/core/src/__tests__/sessionWindow.test.ts rename to packages/web/sources/session/src/__tests__/sessionWindow.test.ts index 7e8503349..a91341efc 100644 --- a/packages/web/core/src/__tests__/sessionWindow.test.ts +++ b/packages/web/sources/session/src/__tests__/sessionWindow.test.ts @@ -1,4 +1,4 @@ -import { sessionWindow } from '..'; +import { sessionWindow } from '../lib'; describe('SessionStart', () => { const w = window; diff --git a/packages/web/sources/session/src/__tests__/test-utils.ts b/packages/web/sources/session/src/__tests__/test-utils.ts new file mode 100644 index 000000000..1a27e0744 --- /dev/null +++ b/packages/web/sources/session/src/__tests__/test-utils.ts @@ -0,0 +1,80 @@ +import type { WalkerOS, Collector, Source } from '@walkeros/core'; +import { createMockLogger } from '@walkeros/core'; +import { sourceSession } from '../index'; +import type { Types } from '../types'; + +// Test utility for creating properly typed mock push function +export function createMockPush(collectedEvents: WalkerOS.Event[]) { + const mockPush = jest.fn(); + + mockPush.mockImplementation((event: WalkerOS.DeepPartialEvent) => { + const nameParts = (event.name || '').split(' '); + const entity = nameParts[0] || ''; + const action = nameParts.slice(1).join(' ') || ''; + + const fullEvent: WalkerOS.Event = { + id: `test-${Date.now()}-${Math.random()}`, + name: event.name || 'unknown', + entity, + action, + data: event.data || {}, + context: event.context || {}, + globals: event.globals || {}, + user: event.user || {}, + nested: (event.nested || []).filter( + (n): n is WalkerOS.Entity => n !== undefined, + ), + consent: Object.entries(event.consent || {}).reduce((acc, [key, val]) => { + if (val !== undefined) acc[key] = val; + return acc; + }, {} as WalkerOS.Consent), + custom: event.custom || {}, + trigger: event.trigger || '', + timestamp: event.timestamp || Date.now(), + timing: event.timing || 0, + group: event.group || '', + count: event.count || 0, + version: { + source: event.version?.source || '1.0.0', + tagging: event.version?.tagging || 2, + }, + source: { + type: event.source?.type || 'session', + id: event.source?.id || '', + previous_id: event.source?.previous_id || '', + }, + }; + collectedEvents.push(fullEvent); + return Promise.resolve({ + ok: true, + event: fullEvent, + }); + }); + + return mockPush as jest.MockedFunction; +} + +// Helper function to create mock command function +export function createMockCommand() { + return jest.fn() as jest.MockedFunction; +} + +// Helper function to create and initialize a session source with proper environment +export async function createSessionSource( + collector: Collector.Instance, + config?: Partial>, +): Promise> { + return await sourceSession({ + collector, + config: config || {}, + env: { + push: collector.push.bind(collector), + command: collector.command.bind(collector), + elb: collector.sources?.elb?.push, + logger: createMockLogger(), + }, + id: 'test-session', + logger: createMockLogger(), + setIngest: async () => {}, + }); +} diff --git a/packages/web/sources/session/src/dev.ts b/packages/web/sources/session/src/dev.ts new file mode 100644 index 000000000..197f1788e --- /dev/null +++ b/packages/web/sources/session/src/dev.ts @@ -0,0 +1 @@ +export * as schemas from './schemas'; diff --git a/packages/web/sources/session/src/index.ts b/packages/web/sources/session/src/index.ts new file mode 100644 index 000000000..3393c65f3 --- /dev/null +++ b/packages/web/sources/session/src/index.ts @@ -0,0 +1,67 @@ +import type { Source, On, Collector } from '@walkeros/core'; +import type { Types, Settings } from './types'; +import { sessionStart } from './lib'; + +// Export types for external usage +export * as SourceSession from './types'; + +// Export lib functions for direct usage +export { sessionStart, sessionStorage, sessionWindow } from './lib'; +export type { + SessionConfig, + SessionCallback, + SessionFunction, + SessionStorageConfig, + SessionWindowConfig, +} from './lib'; + +/** + * Session source implementation. + * + * This source handles session detection and management. + */ +export const sourceSession: Source.Init = async (context) => { + const { config, env } = context; + const { elb, command } = env; + + const settings: Settings = { + ...config?.settings, + }; + + const fullConfig: Source.Config = { + settings, + }; + + // Create minimal collector interface for sessionStart + const collectorInterface: Partial = { + push: elb, + group: undefined, + command, + }; + + // Initialize session using local lib + sessionStart({ + ...settings, + collector: collectorInterface as Collector.Instance, + }); + + // Handle events pushed from collector (consent, session, ready, run) + const handleEvent = async (event: On.Types) => { + if (event === 'consent') { + // Re-initialize session on consent changes + sessionStart({ + ...settings, + collector: collectorInterface as Collector.Instance, + }); + } + }; + + return { + type: 'session', + config: fullConfig, + push: elb, + on: handleEvent, + }; +}; + +export default sourceSession; diff --git a/packages/web/core/src/session/index.ts b/packages/web/sources/session/src/lib/index.ts similarity index 100% rename from packages/web/core/src/session/index.ts rename to packages/web/sources/session/src/lib/index.ts diff --git a/packages/web/core/src/session/sessionStart.ts b/packages/web/sources/session/src/lib/sessionStart.ts similarity index 88% rename from packages/web/core/src/session/sessionStart.ts rename to packages/web/sources/session/src/lib/sessionStart.ts index 0b8a28742..f48046850 100644 --- a/packages/web/core/src/session/sessionStart.ts +++ b/packages/web/sources/session/src/lib/sessionStart.ts @@ -1,7 +1,7 @@ import type { Collector, WalkerOS, On } from '@walkeros/core'; -import type { SessionStorageConfig } from './'; -import { sessionStorage, sessionWindow } from './'; -import { elb as elbOrg } from '../elb'; +import type { SessionStorageConfig } from './sessionStorage'; +import { sessionStorage } from './sessionStorage'; +import { sessionWindow } from './sessionWindow'; import { getGrantedConsent, isArray, isDefined } from '@walkeros/core'; export interface SessionConfig extends SessionStorageConfig { @@ -37,10 +37,8 @@ export function sessionStart( // Register consent handlers with the collector if (collector) { collector.command('on', 'consent', consentConfig); - } else { - // Fallback to elbLayer - elbOrg('walker on', 'consent', consentConfig); } + // No fallback - session source always provides collector } else { // just do it return callFuncAndCb(sessionFn(config), collector, cb); @@ -105,10 +103,8 @@ const defaultCb: SessionCallback = ( // Set user IDs if (collector) { collector.command('user', user); - } else { - // Fallback to elbLayer - elbOrg('walker user', user); } + // No fallback - session source always provides collector if (session.isStart) { // Convert session start to an event object @@ -117,13 +113,8 @@ const defaultCb: SessionCallback = ( name: 'session start', data: session, }); - } else { - // Fallback to elbLayer - elbOrg({ - name: 'session start', - data: session, - }); } + // No fallback - session source always provides collector } return session; diff --git a/packages/web/core/src/session/sessionStorage.ts b/packages/web/sources/session/src/lib/sessionStorage.ts similarity index 93% rename from packages/web/core/src/session/sessionStorage.ts rename to packages/web/sources/session/src/lib/sessionStorage.ts index 0954be98f..b100070d6 100644 --- a/packages/web/core/src/session/sessionStorage.ts +++ b/packages/web/sources/session/src/lib/sessionStorage.ts @@ -1,9 +1,9 @@ -import type { Collector, WalkerOS } from '@walkeros/core'; -import type { SessionWindowConfig } from '.'; +import type { Collector } from '@walkeros/core'; +import type { SessionWindowConfig } from './sessionWindow'; import type { StorageType } from '@walkeros/core'; import { getId, tryCatch } from '@walkeros/core'; -import { storageRead, storageWrite } from '../storage'; -import { sessionWindow } from '.'; +import { storageRead, storageWrite } from '@walkeros/web-core'; +import { sessionWindow } from './sessionWindow'; export interface SessionStorageConfig extends SessionWindowConfig { deviceKey?: string; diff --git a/packages/web/core/src/session/sessionWindow.ts b/packages/web/sources/session/src/lib/sessionWindow.ts similarity index 100% rename from packages/web/core/src/session/sessionWindow.ts rename to packages/web/sources/session/src/lib/sessionWindow.ts diff --git a/packages/web/sources/session/src/schemas/index.ts b/packages/web/sources/session/src/schemas/index.ts new file mode 100644 index 000000000..52526f87e --- /dev/null +++ b/packages/web/sources/session/src/schemas/index.ts @@ -0,0 +1,8 @@ +import { zodToSchema } from '@walkeros/core/dev'; +import { SettingsSchema } from './settings'; + +// Export Zod schemas and types +export { SettingsSchema, type Settings } from './settings'; + +// JSON Schema exports (for website PropertyTable) +export const settings = zodToSchema(SettingsSchema); diff --git a/packages/web/sources/session/src/schemas/settings.ts b/packages/web/sources/session/src/schemas/settings.ts new file mode 100644 index 000000000..77d3f365a --- /dev/null +++ b/packages/web/sources/session/src/schemas/settings.ts @@ -0,0 +1,68 @@ +import { z } from '@walkeros/core/dev'; + +/** + * Session source settings schema + */ +export const SettingsSchema = z.object({ + storage: z + .boolean() + .default(false) + .describe('Enable persistent storage for session/device IDs') + .optional(), + + consent: z + .union([z.string(), z.array(z.string())]) + .describe('Consent key(s) required to enable storage mode') + .optional(), + + length: z + .number() + .default(30) + .describe('Session timeout in minutes') + .optional(), + + pulse: z + .boolean() + .default(false) + .describe('Keep session alive on each event') + .optional(), + + sessionKey: z + .string() + .default('elbSessionId') + .describe('Storage key for session ID') + .optional(), + + sessionStorage: z + .enum(['local', 'session']) + .default('local') + .describe('Storage type for session') + .optional(), + + deviceKey: z + .string() + .default('elbDeviceId') + .describe('Storage key for device ID') + .optional(), + + deviceStorage: z + .enum(['local', 'session']) + .default('local') + .describe('Storage type for device') + .optional(), + + deviceAge: z + .number() + .default(30) + .describe('Device ID age in days') + .optional(), + + // Note: Using z.any() because z.custom() cannot be converted to JSON Schema + // TypeScript types provide compile-time safety; runtime accepts function or false + cb: z + .any() + .describe('Custom session callback function or false to disable') + .optional(), +}); + +export type Settings = z.infer; diff --git a/packages/web/sources/session/src/types/index.ts b/packages/web/sources/session/src/types/index.ts new file mode 100644 index 000000000..f64e02363 --- /dev/null +++ b/packages/web/sources/session/src/types/index.ts @@ -0,0 +1,38 @@ +import type { Source, Elb } from '@walkeros/core'; +import type { SessionConfig, SessionCallback } from '../lib'; + +// Settings: configuration for session source +export interface Settings extends SessionConfig { + // All settings inherited from SessionConfig: + // - consent?: string | string[] + // - storage?: boolean + // - cb?: SessionCallback | false + // - pulse?: boolean + // - sessionStorage?: 'local' | 'session' + // - deviceStorage?: 'local' | 'session' + // - sessionKey?: string + // - deviceKey?: string + // - length?: number (session timeout in minutes) +} + +// InitSettings: user input (all optional) +export type InitSettings = Partial; + +export interface Mapping {} + +export type Push = Elb.Fn; + +export interface Env extends Source.BaseEnv {} + +export type Types = Source.Types; + +export type Config = Source.Config; + +// Re-export session types from lib +export type { + SessionConfig, + SessionCallback, + SessionFunction, + SessionStorageConfig, + SessionWindowConfig, +} from '../lib'; diff --git a/packages/web/sources/session/tsconfig.json b/packages/web/sources/session/tsconfig.json new file mode 100644 index 000000000..e9f0a4037 --- /dev/null +++ b/packages/web/sources/session/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@walkeros/config/tsconfig/web.json", + "compilerOptions": { + "rootDir": "src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/web/sources/session/tsup.config.ts b/packages/web/sources/session/tsup.config.ts new file mode 100644 index 000000000..3af5daf23 --- /dev/null +++ b/packages/web/sources/session/tsup.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts', 'src/dev.ts'], + format: ['cjs', 'esm'], + dts: true, + clean: true, + sourcemap: true, +}); diff --git a/skills/understanding-flow/SKILL.md b/skills/understanding-flow/SKILL.md index 9aa4a5f88..86ee77d2d 100644 --- a/skills/understanding-flow/SKILL.md +++ b/skills/understanding-flow/SKILL.md @@ -47,8 +47,9 @@ the canonical interface. // Conceptual structure (see source for full type) interface Flow { sources?: Record; - collector: Collector; + transformers?: Record; destinations?: Record; + collector?: Collector.InitConfig; } ``` @@ -77,6 +78,9 @@ const { collector, elb } = await startFlow({ sources: { /* ... */ }, + transformers: { + /* ... */ + }, destinations: { /* ... */ }, @@ -102,40 +106,85 @@ Transformers run at two points in the pipeline, configured via `next` and Runs after source captures event, before collector processing: +**Bundled mode (flow.json):** + +```json +{ + "sources": { + "browser": { + "package": "@walkeros/web-source-browser", + "next": "validate" + } + }, + "transformers": { + "validate": { + "package": "@walkeros/transformer-validator", + "next": "enrich" + }, + "enrich": { + "package": "@walkeros/transformer-enricher" + } + } +} +``` + +**Integrated mode (TypeScript):** + ```typescript sources: { browser: { code: sourceBrowser, - next: 'validate' // First transformer in pre-chain + next: 'validate' } }, transformers: { validate: { code: transformerValidator, - config: { next: 'enrich' } // Chain continues + config: { next: 'enrich' } }, enrich: { code: transformerEnrich - // No next = chain ends, event goes to collector } } ``` +Note: In flow.json, `next` is at the reference level. The CLI bundler moves it +into `config.next` for runtime. + ### Post-Collector Chain Runs after collector enrichment, before destination receives event: +**Bundled mode (flow.json):** + +```json +{ + "destinations": { + "gtag": { + "package": "@walkeros/web-destination-gtag", + "before": "redact" + } + }, + "transformers": { + "redact": { + "package": "@walkeros/transformer-redact" + } + } +} +``` + +**Integrated mode (TypeScript):** + ```typescript destinations: { gtag: { code: destinationGtag, - before: 'redact' // First transformer in post-chain + before: 'redact' } }, transformers: { redact: { code: transformerRedact - // Event then goes to destination } } ``` @@ -143,7 +192,8 @@ transformers: { ### Chain Resolution - `source.next` → starts pre-collector chain -- `transformer.config.next` → links transformers together +- `transformer.next` (flow.json) or `transformer.config.next` (runtime) → links + transformers - `destination.before` → starts post-collector chain per destination ## Related diff --git a/skills/understanding-transformers/SKILL.md b/skills/understanding-transformers/SKILL.md index 966c0484f..d2db5bb58 100644 --- a/skills/understanding-transformers/SKILL.md +++ b/skills/understanding-transformers/SKILL.md @@ -92,6 +92,74 @@ push(event, context) { } ``` +## Inline Code Transformers + +For simple transformations without external packages, use inline code with the +`$code:` prefix: + +```json +{ + "transformers": { + "enrich": { + "code": { + "push": "$code:(event) => { event.data.enrichedAt = Date.now(); return event; }" + }, + "next": "validate" + } + } +} +``` + +**Inline code structure:** + +| Property | Purpose | +| ----------- | ----------------------------------- | +| `code.init` | Code run once during initialization | +| `code.push` | Code run for each event | + +**Push code has access to:** + +- `event` - The event being processed +- `context` - Push context with logger, config, etc. + +**Return values in push code:** + +- Return modified event to continue chain +- Return `undefined` to pass event unchanged +- Return `false` to drop event from chain + +**Example: Filtering internal events** + +```json +{ + "transformers": { + "filter": { + "code": { + "push": "$code:(event) => { if (event.name.startsWith('internal_')) return false; return event; }" + } + } + } +} +``` + +**Mixing inline and package transformers:** + +```json +{ + "transformers": { + "addTimestamp": { + "code": { + "push": "$code:(event) => { event.data.processedAt = new Date().toISOString(); return event; }" + }, + "next": "validate" + }, + "validate": { + "package": "@walkeros/transformer-validator" + } + } +} +``` + ## Pipeline Integration Transformers run at two points in the pipeline: diff --git a/website/docs/getting-started/flow.mdx b/website/docs/getting-started/flow.mdx index 4f364fbd9..6f434f39c 100644 --- a/website/docs/getting-started/flow.mdx +++ b/website/docs/getting-started/flow.mdx @@ -263,19 +263,19 @@ Destinations receive processed events and send them to analytics tools, database - **`consent`** - Required consent states - **`policy`** - Processing rules -**Built-in Code Destination:** +See [Destinations documentation](/docs/destinations/) for all available options. + +### Transformers -For custom logic without external packages, use `code: true`: +Transformers process events between sources and destinations. They validate, enrich, or redact events in the pipeline. -See [Destinations documentation](/docs/destinations/) for all available options. +**Configuration options:** +- **`package`** - The npm package (required) +- **`code`** - Explicit import variable name (optional, auto-resolved) +- **`config`** - Transformer-specific configuration +- **`env`** - Environment-specific settings +- **`next`** - Next transformer in chain (omit to end chain) +- **`variables`** - Transformer-level variable overrides +- **`definitions`** - Transformer-level definitions + +**Chaining transformers:** + +Link transformers together using `next`: + + + +**Explicit chain control with arrays:** + +For explicit control over the transformer chain order, use an array instead of a string. This bypasses the automatic chain resolution: + + + +| Syntax | Behavior | +|--------|----------| +| `"next": "validate"` | Walks chain via each transformer's `next` property | +| `"next": ["validate", "enrich"]` | Uses exact order specified, ignores transformer `next` properties | + +**Connecting to sources and destinations:** + +- **Pre-collector chain**: Use `next` on a source to route events through transformers before the collector +- **Post-collector chain**: Use `before` on a destination to route events through transformers after the collector + + + +Both `next` and `before` accept arrays for explicit chain control (see "Explicit chain control with arrays" above). + +See [Transformers documentation](/docs/transformers/) for available transformers and custom transformer guide. + +### Inline Code (without packages) + +For simple one-liner logic, define sources, transformers, or destinations inline without creating a package. + +Use `code` object instead of `package`: + +**Inline Transformer:** + + ({ ...event, data: { ...event.data, timestamp: Date.now() } })" + }, + "config": {} + } + } +}`} + language="json" +/> + +**Inline Destination:** + + { context.logger.info(event.name, event.data); }" + }, + "config": {} + } + } +}`} + language="json" +/> + +**Code object properties:** +- **`push`** - The push function with `$code:` prefix (required) +- **`type`** - Optional instance type identifier +- **`init`** - Optional init function with `$code:` prefix + +**Rules:** +- Use `package` OR `code`, never both +- `config` stays separate (same as package-based) +- `$code:` prefix outputs raw JavaScript at bundle time ### Collector @@ -589,6 +727,7 @@ walkerOS uses a clear type hierarchy: │ ├── web: {} or server: {} │ │ ├── packages: { ... } │ │ ├── sources: { ... } │ +│ ├── transformers: { ... } │ │ ├── destinations: { ... } │ │ └── collector: { ... } │ └─────────────────────────────────────────────────────────────────┘ diff --git a/website/docs/sources/web/browser/index.mdx b/website/docs/sources/web/browser/index.mdx index 16fada477..0335a4c8c 100644 --- a/website/docs/sources/web/browser/index.mdx +++ b/website/docs/sources/web/browser/index.mdx @@ -15,9 +15,9 @@ Captures user interactions directly from the DOM using data attributes. ## Features * **Data attribute reading**: Extracts tracking data from `data-elb-*` attributes -* **Session management**: Automatic session detection and handling * **Pageview tracking**: Automatic or manual page view events * **DOM commands**: Enhanced `elb` function with `walker init` support +* **Trigger system**: Click, submit, load, hover, scroll, visible triggers :::info Where This Fits The Browser Source is a **source** in the walkerOS flow: @@ -39,7 +39,7 @@ It reads `data-elb-*` attributes from your HTML and generates events. The Collec Install the packages: @@ -48,18 +48,12 @@ Configure in your code: + +# Session Source + +Standalone session detection and management that can be composed with any walkerOS source. + +## Installation + + + + +Install the packages: + + + +Configure in your code: + + + + + + +Add to your `flow.json` sources: + + + +[See Bundled Mode setup →](/docs/getting-started/modes/bundled) | [CLI reference →](/docs/apps/cli) + + + + +## Configuration + + + +## Detection Methods + +The session source uses two complementary methods: + +| Method | Storage | Use Case | +|--------|---------|----------| +| **Window** | None | Privacy-first, per-page session | +| **Storage** | localStorage | Cross-page session tracking | + +### Window-based Detection + +Without storage, session detection relies on browser signals: + +**Navigation Type:** + + +**Marketing Parameters:** + +UTM and other marketing parameters trigger session start: + + + +### Storage-based Detection + +With `storage: true`, sessions persist across page loads: + + + +**Session Lifecycle:** + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Session Timeline │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ User visits Page 2 Page 3 30min idle │ +│ │ │ │ │ │ +│ ▼ ▼ ▼ ▼ │ +│ [Session 1] ─────────────────────────────► [Expires] │ +│ id: abc123 │ +│ │ +│ User returns │ +│ │ │ +│ ▼ │ +│ [Session 2] │ +│ id: def456 │ +└─────────────────────────────────────────────────────────────┘ +``` + +**Storage Keys:** + +| Key | Default | Description | +|-----|---------|-------------| +| `sessionKey` | `elbSessionId` | localStorage key for session ID | +| `deviceKey` | `elbDeviceId` | localStorage key for device ID | + +## Session Start Event + +When a new session is detected, the source pushes: + + + +## Consent Integration + +Session detection adapts to consent state: + +| Consent State | Behavior | +|---------------|----------| +| No consent | Window-only detection (no storage) | +| Consent granted | Storage-based with device ID | +| Consent revoked | Falls back to window-only | + + + +## Next Steps + +* [Browser Source](/docs/sources/web/browser) - DOM-based event tracking +* [DataLayer Source](/docs/sources/web/dataLayer) - GTM/GA4 integration +* [Consent Guide](/docs/guides/consent) - Consent management patterns diff --git a/website/docs/transformers/index.mdx b/website/docs/transformers/index.mdx index b2f010ddd..9ddcd5afb 100644 --- a/website/docs/transformers/index.mdx +++ b/website/docs/transformers/index.mdx @@ -30,13 +30,17 @@ Transformers are middleware for **validating**, **enriching**, and **redacting** ## Configuration + + + + + + +Add to your `flow.json`: + + + + + + ## Available transformers ### Validator diff --git a/website/docs/transformers/validator.mdx b/website/docs/transformers/validator.mdx index 39c3a8220..9b382c9a3 100644 --- a/website/docs/transformers/validator.mdx +++ b/website/docs/transformers/validator.mdx @@ -13,22 +13,28 @@ import { schemas } from '@walkeros/transformer-validator/dev'; Validates events using JSON Schema with two modes: **format** (structure) and **contract** (business rules). -## Installation +## Setup + + + + +Install the package: -## Setup +Configure in your code: + + + +Add to your `flow.json`: + + + +[See Bundled Mode setup →](/docs/getting-started/modes/bundled) | [CLI reference →](/docs/apps/cli) + + + + ## Validation modes | Mode | Purpose | @@ -70,10 +112,44 @@ await startFlow({ +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `format` | `boolean` | `true` | Validate full WalkerOS.Event structure | +| `contract` | `Contract` | - | Entity/action keyed validation rules | + ## Behavior -* **Invalid events**: Stop chain, log error -* **Unmatched events**: Pass through (validation is opt-in) +### When validation fails + +Invalid events are **dropped from the pipeline** and do not reach destinations. The validator logs detailed error information: + +| Validation | Log message | Includes | +|------------|-------------|----------| +| Format | `Event format invalid` | Field-level errors from AJV | +| Contract | `Contract validation failed` | Rule name + field-level errors | + +**Example log output:** + +``` +[ERROR] Event format invalid { errors: "data/name must match pattern \"^\\w+ \\w+$\"" } +[ERROR] Contract validation failed { rule: "product add", errors: "data must have required property 'id'" } +``` + +### When validation passes + +- **Format valid**: Event continues to contract validation (if configured) +- **Contract valid**: Logs debug message, event continues to next transformer or destinations +- **No matching contract**: Event passes through unchanged (validation is opt-in per entity/action) + +### Pipeline flow + +``` +Event → Format check → Contract check → Next transformer/Destination + ↓ ↓ + Invalid? Invalid? + ↓ ↓ + Drop + Log Drop + Log +```
Conditional rules diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts index 920abb1e5..229b22f2a 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -189,7 +189,20 @@ const config: Config = { }, } satisfies Preset.ThemeConfig, - plugins: [tailwindPlugin], + plugins: [ + tailwindPlugin, + [ + '@docusaurus/plugin-client-redirects', + { + redirects: [ + { + from: '/docs/sources/web/session', + to: '/docs/sources/web/session/detection', + }, + ], + }, + ], + ], }; async function tailwindPlugin() { diff --git a/website/package.json b/website/package.json index 5fac6fb64..71b322e54 100644 --- a/website/package.json +++ b/website/package.json @@ -44,6 +44,7 @@ "devDependencies": { "@docusaurus/faster": "^3.9.2", "@docusaurus/module-type-aliases": "^3.9.2", + "@docusaurus/plugin-client-redirects": "^3.9.2", "@docusaurus/tsconfig": "^3.9.2", "@docusaurus/types": "^3.9.2", "@heroicons/react": "^2.2.0", diff --git a/website/src/components/organisms/hero.tsx b/website/src/components/organisms/hero.tsx index d227b74d6..6ad0ab1e5 100644 --- a/website/src/components/organisms/hero.tsx +++ b/website/src/components/organisms/hero.tsx @@ -113,7 +113,7 @@ export default function Hero({ className="inline-flex items-center space-x-2 text-sm/6 font-medium" style={{ color: 'var(--color-base-content)' }} > - Just shipped v1.0 + Just shipped v1.1