diff --git a/.eslintrc.js b/.eslintrc.js
index 511bede27f..7749c2d86b 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -1,6 +1,6 @@
module.exports = {
root: true,
- ignorePatterns: ['node_modules', 'dist', 'templates', '**/node_modules'],
+ ignorePatterns: ['node_modules', 'dist', 'templates', 'scripts', '**/node_modules'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2019,
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 6a367de782..5ffaa510ea 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -6,11 +6,11 @@
# Actions common lib folder
-actions-shared/ @segmentio/build-experience-team
+actions-shared/ @segmentio/build-experience-team @segmentio/strategic-connections-team
# AJV utils
-ajv-human-errors/ @segmentio/build-experience-team
+ajv-human-errors/ @segmentio/build-experience-team @segmentio/strategic-connections-team
# Browser destinations
@@ -18,15 +18,15 @@ browser-destinations/ @segmentio/libraries-web-team @segmentio/strategic-connect
# CLI private libs
-cli-internal/ @segmentio/build-experience-team
+cli-internal/ @segmentio/build-experience-team @segmentio/strategic-connections-team
# CLI binary
-cli/ @segmentio/build-experience-team
+cli/ @segmentio/build-experience-team @segmentio/strategic-connections-team
# Core actions runtime
-core/ @segmentio/build-experience-team
+core/ @segmentio/build-experience-team @segmentio/strategic-connections-team
# Destination definitions and their actions
@@ -34,4 +34,4 @@ destination-actions/ @segmentio/strategic-connections-team @segmentio/build-expe
# Utilities for event payload validation against an action's subscription AST.
-destination-subscriptions/ @segmentio/build-experience-team
+destination-subscriptions/ @segmentio/build-experience-team @segmentio/strategic-connections-team
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 30292a181a..f7cda9abd8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,116 +7,178 @@ on:
pull_request:
jobs:
- test-and-build:
+ test:
+ name: Unit tests
runs-on: ubuntu-20.04
-
timeout-minutes: 30
-
strategy:
matrix:
node-version: [18.x]
steps:
- - uses: actions/checkout@v2
+ # See nx recipe: https://nx.dev/recipes/ci/monorepo-ci-github-actions
+ - uses: actions/checkout@v3
with:
persist-credentials: false
+ fetch-depth: 0 # nx recipe
- name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v2
+ uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
registry-url: 'https://registry.npmjs.org'
+ cache: yarn
- - name: Get yarn cache directory path
- id: yarn-cache-dir-path
- run: echo "::set-output name=dir::$(yarn cache dir)"
+ - name: Use Github Personal Access Token
+ run: git config --global url."https://${{ secrets.GH_PAT }}@github.com/".insteadOf ssh://git@github.com/
- - uses: actions/cache@v2
- id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
+ - uses: nrwl/nx-set-shas@v3 # nx recipe
+
+ - name: Install Dependencies
+ run: yarn install --frozen-lockfile
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+
+ - name: Build (Affected)
+ run: NODE_ENV=production yarn nx affected -t build --parallel=3 # nx recipe
+
+ - name: Test (Affected)
+ run: yarn nx affected -t test --parallel=3 # nx recipe
+
+ lint:
+ name: Lint
+ runs-on: ubuntu-20.04
+ timeout-minutes: 20
+ strategy:
+ matrix:
+ node-version: [18.x]
+
+ steps:
+ - uses: actions/checkout@v3
with:
- path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
- key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
- restore-keys: |
- ${{ runner.os }}-yarn-
+ persist-credentials: false
+ fetch-depth: 0 # nx recipe
+
+ - name: Use Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@v3
+ with:
+ node-version: ${{ matrix.node-version }}
+ registry-url: 'https://registry.npmjs.org'
+ cache: yarn
- name: Use Github Personal Access Token
run: git config --global url."https://${{ secrets.GH_PAT }}@github.com/".insteadOf ssh://git@github.com/
+ - uses: nrwl/nx-set-shas@v3 # nx recipe
+
- name: Install Dependencies
run: yarn install --frozen-lockfile
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- - name: Build
- run: NODE_ENV=production yarn build
+ - name: Build # TODO: This monorepo should be refactored so packages can be linted invidually. "affected" will not work ATM.
+ run: NODE_ENV=production yarn build # nx recipe
- name: Lint
env:
NODE_OPTIONS: '--max-old-space-size=4096'
run: yarn lint
- - name: Validate
- run: yarn validate
+ validate:
+ name: Validate
+ runs-on: ubuntu-20.04
+ timeout-minutes: 20
+ strategy:
+ matrix:
+ node-version: [18.x]
- - name: Test
- run: yarn test
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ persist-credentials: false
+ fetch-depth: 0 # nx recipe
+
+ - name: Use Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@v3
+ with:
+ node-version: ${{ matrix.node-version }}
+ registry-url: 'https://registry.npmjs.org'
+ cache: yarn
+
+ - name: Use Github Personal Access Token
+ run: git config --global url."https://${{ secrets.GH_PAT }}@github.com/".insteadOf ssh://git@github.com/
+
+ - uses: nrwl/nx-set-shas@v3 # nx recipe
+
+ - name: Install Dependencies
+ run: yarn install --frozen-lockfile
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+
+ - name: Assert yarn.lock is up-to-date
+ run: bash scripts/assert-lockfile-updated.sh
+
+ - name: Build # TODO: This monorepo should be refactored so packages can be linted invidually. "affected" will not work ATM.
+ run: NODE_ENV=production yarn build # nx recipe
+
+ - name: Validate Definitions
+ run: yarn validate
- - name: destination-subscriptions size
+ - name: Destination Subscription Size
run: |
if $(lerna changed | grep -q destination-subscriptions); then
yarn subscriptions size
fi
- # browser-tests-destination:
- # # env: # Disable saucelabs - we blew through our quota.
- # # SAUCE_USERNAME: ${{secrets.SAUCE_USERNAME}}
- # # SAUCE_ACCESS_KEY: ${{secrets.SAUCE_ACCESS_KEY}}
+ - name: Assert generated types are up-to-date
+ run: bash scripts/assert-types-updated.sh
- # runs-on: ubuntu-20.04
-
- # timeout-minutes: 20
-
- # strategy:
- # matrix:
- # node-version: [18.x]
+ browser-destination-bundle-qa:
+ name: Browser Destination Bundle QA
+ # env: # Disable saucelabs - we blew through our quota.
+ # SAUCE_USERNAME: ${{secrets.SAUCE_USERNAME}}
+ # SAUCE_ACCESS_KEY: ${{secrets.SAUCE_ACCESS_KEY}}
+ runs-on: ubuntu-20.04
+ timeout-minutes: 20
+ strategy:
+ matrix:
+ node-version: [18.x]
- # steps:
- # - uses: actions/checkout@master
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ persist-credentials: false
- # - name: Use Node.js ${{ matrix.node-version }}
- # uses: actions/setup-node@v2
- # with:
- # node-version: ${{ matrix.node-version }}
- # registry-url: 'https://registry.npmjs.org'
+ - name: Use Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@v3
+ with:
+ node-version: ${{ matrix.node-version }}
+ registry-url: 'https://registry.npmjs.org'
+ cache: yarn
- # - name: Get yarn cache directory path
- # id: yarn-cache-dir-path
- # run: echo "::set-output name=dir::$(yarn cache dir)"
+ - name: Use Github Personal Access Token
+ run: git config --global url."https://${{ secrets.GH_PAT }}@github.com/".insteadOf ssh://git@github.com/
- # - uses: actions/cache@v2
- # id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
- # with:
- # path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
- # key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
- # restore-keys: |
- # ${{ runner.os }}-yarn-
+ - name: Install Dependencies
+ run: yarn install --frozen-lockfile
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- # - name: Install Dependencies
- # run: yarn install --frozen-lockfile
- # env:
- # NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+ - name: Build
+ run: NODE_ENV=production yarn build:browser-bundles
- # - name: Build
- # run: NODE_ENV=production yarn build:browser-destinations && yarn browser build-web
+ - name: Size Limit
+ run: yarn browser size
- # - name: Run Saucelabs Tests
- # working-directory: packages/browser-destinations-integration-tests
- # shell: bash
- # run: |
- # yarn start-destination-server &
- # yarn test:sauce
+ # - name: Run Saucelabs Tests
+ # working-directory: packages/browser-destinations-integration-tests
+ # shell: bash
+ # run: |
+ # yarn start-destination-server &
+ # yarn test:sauce
browser-tests-core:
+ name: 'Browser tests: actions-core'
runs-on: ubuntu-20.04
timeout-minutes: 10
@@ -164,6 +226,7 @@ jobs:
run: yarn test-browser
snyk:
+ name: Snyk
runs-on: ubuntu-20.04
timeout-minutes: 5
diff --git a/.github/workflows/label-prs.yml b/.github/workflows/label-prs.yml
new file mode 100644
index 0000000000..8f3355effe
--- /dev/null
+++ b/.github/workflows/label-prs.yml
@@ -0,0 +1,56 @@
+# This workflow labels PRs based on the files that were changed. It uses a custom script to this
+# instead of actions/labeler as few of the tags are more than just file changes.
+
+name: Label PRs
+on:
+ pull_request_target:
+ types: [opened, synchronize, reopened]
+
+jobs:
+ pr-labeler:
+ runs-on: ubuntu-20.04
+ permissions:
+ contents: read
+ pull-requests: write
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ persist-credentials: false
+ - name: Compute Labels
+ id: compute-labels
+ uses: actions/github-script@v7
+ with:
+ # Required for the script to access team membership information.
+ # Scope: members:read and contentes:read permission on the organization.
+ github-token: ${{ secrets.GH_PAT_MEMBER_AND_PULL_REQUEST_READONLY }}
+ script: |
+ const script = require('./scripts/compute-labels.js')
+ await script({github, context, core})
+ - name: Apply Labels
+ uses: actions/github-script@v7
+ env:
+ labelsToAdd: '${{ steps.compute-labels.outputs.add }}'
+ labelsToRemove: '${{ steps.compute-labels.outputs.remove }}'
+ with:
+ script: |
+ const { labelsToAdd, labelsToRemove } = process.env
+ if(labelsToAdd.length > 0) {
+ await github.rest.issues.addLabels({
+ issue_number: context.payload.pull_request.number,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ labels: labelsToAdd.split(',')
+ });
+ }
+ if(labelsToRemove.length > 0) {
+ const requests = labelsToRemove.split(',').map(label => {
+ return github.rest.issues.removeLabel({
+ issue_number: context.payload.pull_request.number,
+ name: label,
+ owner: context.repo.owner,
+ repo: context.repo.repo
+ });
+ });
+ await Promise.all(requests);
+ }
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 715b854940..c141670881 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -56,3 +56,35 @@ jobs:
- name: Publish
run: |
yarn lerna publish from-git --yes --allowBranch=main --loglevel=verbose --dist-tag latest
+
+ - name: Generate and Push Release Tag
+ id: push-release-tag
+ run: |
+ git config user.name ${{ github.actor }}
+ git config user.email ${{ github.actor }}@users.noreply.github.com
+
+ commit=${{ github.sha }}
+ if ! n=$(git rev-list --count $commit~ --grep "Publish" --since="00:00"); then
+ echo 'failed to calculate tag'
+ exit 1
+ fi
+
+ case "$n" in
+ 0) suffix="" ;; # first commit of the day gets no suffix
+ *) suffix=".$n" ;; # subsequent commits get a suffix, starting with .1
+ esac
+
+ tag=$(printf release-$(date '+%Y-%m-%d%%s') $suffix)
+ git tag -a $tag -m "Release $tag"
+ git push origin $tag
+ echo "release-tag=$tag" >> $GITHUB_OUTPUT
+
+ - name: Create Github Release
+ id: create-github-release
+ uses: actions/github-script@v7
+ env:
+ RELEASE_TAG: ${{ steps.push-release-tag.outputs.release-tag }}
+ with:
+ script: |
+ const script = require('./scripts/create-github-release.js')
+ await script({github, context, core, exec})
diff --git a/.gitignore b/.gitignore
index 35b316e330..128e678d43 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,7 +2,7 @@ node_modules
dist
.DS_Store
*.log
-*.tsbuildinfo
+*.tsbuildinfo*
.eslintcache
package-lock.json
.env
diff --git a/.nxignore b/.nxignore
new file mode 100644
index 0000000000..ea31b7529b
--- /dev/null
+++ b/.nxignore
@@ -0,0 +1 @@
+packages/cli-internal
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 3807a2101d..61877fb64f 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -36,6 +36,17 @@
"port": 47082,
"args": ["serve"]
},
+ {
+ "type": "node",
+ "request": "launch",
+ "name": "Run CLI Serve without UI",
+ "program": "${workspaceFolder}/bin/run",
+ "restart": true,
+ "cwd": "${workspaceFolder}",
+ "console": "integratedTerminal",
+ "port": 47082,
+ "args": ["serve", "-n"]
+ },
{
"type": "node",
"request": "launch",
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 40b75bb09c..170f25e318 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -36,7 +36,7 @@ Before continuing, please make sure to read our [Code of Conduct](./CODE_OF_COND
3. Spec out the integration. If you want some guidance, you can use this [template](https://docs.google.com/document/d/1dIJxYge9N700U9Nhawapy25WMD8pUuey72S5qo3uejA/edit#heading=h.92w309fjzhti), which will prompt you to think about: whether you want to build a cloud-mode or device-mode destination, the method of authentication, the settings, and the Actions and default Field Mappings that you want to build.
-4. Join the Segment Partners Slack workspace. We’ll send you an invite. The **#dev-center-pilot** channel is the space for questions - partners can share their advice with each other, and the Segment team is there to answer any tricky questions.
+4. If you have any questions during the build process, please contact us at partner-support@segment.com.
## Build your integration
@@ -50,7 +50,7 @@ Before continuing, please make sure to read our [Code of Conduct](./CODE_OF_COND
- For cloud-mode destinations, follow these instructions: [Build & Test Cloud Destinations](./docs/testing.md).
- If you are building a device-mode destination, see the [browser-destinations README](./packages/browser-destinations/README.md).
-4. When you have questions, ask in the Segment Partners Slack workspace - use the **#dev-center-pilot** channel.
+4. When you have questions, reach out to partner-support@segment.com for assistance.
## Submit a pull request
diff --git a/README.md b/README.md
index 145bd84c09..c2ca148ddb 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,7 @@
# Action Destinations
+
Action Destinations are the new way to build streaming destinations on Segment.
Action Destinations were [launched in December 2021](https://segment.com/blog/introducing-destination-actions/) to enable customers with a customizable framework to map Segment event sources to their favorite 3rd party tools like Google Analytics.
@@ -30,6 +31,7 @@ For more detailed instruction, see the following READMEs:
- [Presets](#presets)
- [perform function](#the-perform-function)
- [Batching Requests](#batching-requests)
+- [Action Hooks](#action-hooks)
- [HTTP Requests](#http-requests)
- [Support](#support)
@@ -37,7 +39,13 @@ For more detailed instruction, see the following READMEs:
### Local development
-This is a monorepo with multiple packages leveraging [`lerna`](https://github.com/lerna/lerna) with [Yarn Workspaces](https://classic.yarnpkg.com/en/docs/workspaces):
+This is a monorepo with multiple packages leveraging:
+
+- [`lerna`](https://github.com/lerna/lerna) for publishing
+- [`nx`](https://nx.dev) for dependency-tree aware building, linting, testing, and caching (migration away from `lerna` in progress!).
+- [Yarn Workspaces](https://classic.yarnpkg.com/en/docs/workspaces) for package symlinking and hoisting.
+
+Structure:
- `packages/ajv-human-errors` - a wrapper around [AJV](https://ajv.js.org/) errors to produce more friendly validation messages
- `packages/browser-destinations` - destination definitions that run on device via Analytics 2.0
@@ -65,9 +73,8 @@ yarn login
# Requires node 18.12.1, optionally: nvm use 18.12.1
yarn --ignore-optional
-yarn bootstrap
-yarn build
yarn install
+yarn build
# Run unit tests to ensure things are working! For partners who don't have access to internal packages, you can run:
yarn test-partners
@@ -383,6 +390,74 @@ const destination = {
}
```
+## Conditional Fields
+
+Conditional fields enable a field only when a predefined list of conditions are met while the user steps through the mapping editor. This is useful when showing a field becomes unnecessary based on the value of some other field.
+
+For example, in the Salesforce destination the 'Bulk Upsert External ID' field is only relevant when the user has selected 'Operation: Upsert' and 'Enable Batching: True'. In all other cases the field will be hidden to streamline UX while setting up the mapping.
+
+To define a conditional field, the `InputField` should implement the `depends_on` property. This property lives in destination-kit and the definition can be found here: [`packages/core/src/destination-kit/types.ts`](https://github.com/segmentio/action-destinations/blame/854a9e154547a54a7323dc3d4bf95bc31d31433a/packages/core/src/destination-kit/types.ts).
+
+The above Salesforce use case is defined like this:
+
+```js
+export const bulkUpsertExternalId: InputField = {
+ // other properties skipped for brevity ...
+ depends_on: {
+ match: 'all', // match is optional and can be either 'any' or 'all'. If left undefiend it defaults to matching all conditions.
+ conditions: [
+ {
+ fieldKey: 'operation', // field keys must match some other field in the same action
+ operator: 'is',
+ value: 'upsert'
+ },
+ {
+ fieldKey: 'enable_batching',
+ operator: 'is',
+ value: true
+ }
+ ]
+ }
+}
+```
+
+Lists of values can also be included as match conditions. For example:
+
+```js
+export const recordMatcherOperator: InputField = {
+ // ...
+ depends_on: {
+ // This is interpreted as "show recordMatcherOperator if operation is (update or upsert or delete)"
+ conditions: [
+ {
+ fieldKey: 'operation',
+ operator: 'is',
+ value: ['update', 'upsert', 'delete']
+ }
+ ]
+ }
+}
+```
+
+The value can be undefined, which allows matching against empty fields or fields which contain any value. For example:
+
+```js
+export const name: InputField = {
+ // ...
+ depends_on: {
+ match: 'all',
+ // The name field will be shown only if conversionRuleId is not empty.
+ conditions: [
+ {
+ fieldKey: 'conversionRuleId',
+ operator: 'is_not',
+ value: undefined
+ }
+ ]
+ }
+}
+```
+
## Presets
Presets are pre-built use cases to enable customers to get started quickly with an action destination. They include everything needed to generate a valid subscription.
@@ -431,6 +506,7 @@ The `perform` method accepts two arguments, (1) the request client instance (ext
- `features` - The features available in the request based on the customer's sourceID. Features can only be enabled and/or used by internal Twilio/Segment employees. Features cannot be used for Partner builds.
- `statsContext` - An object, containing a `statsClient` and `tags`. Stats can only be used by internal Twilio/Segment employees. Stats cannot be used for Partner builds.
- `logger` - Logger can only be used by internal Twilio/Segment employees. Logger cannot be used for Partner builds.
+- `dataFeedCache` - DataFeedCache can only be used by internal Twilio/Segment employees. DataFeedCache cannot be used for Partner builds.
- `transactionContext` - An object, containing transaction variables and a method to update transaction variables which are required for few segment developed actions. Transaction Context cannot be used for Partner builds.
- `stateContext` - An object, containing context variables and a method to get and set context variables which are required for few segment developed actions. State Context cannot be used for Partner builds.
@@ -511,6 +587,104 @@ Keep in mind a few important things about how batching works:
Additionally, you’ll need to coordinate with Segment’s R&D team for the time being. Please reach out to us in your dedicated Slack channel!
+## Action Hooks
+
+**Note: This feature is not yet released.**
+
+Hooks allow builders to perform requests against a destination at certain points in the lifecycle of a mapping. Values can then be persisted from that request to be used later on in the action's `perform` method.
+
+**Inputs**
+
+Builders may define a set of `inputFields` that are used when performing the request to the destination.
+
+**`performHook`**
+
+Similar to the `perform` method, the `performHook` method allows builders to trigger a request to the destination whenever the criteria for that hook to be triggered is met. This method uses the `inputFields` defined as request parameters.
+
+**Outputs**
+
+Builders define the shape of the hook output with the `outputTypes` property. Successful returns from `performHook` should match the keys defined here. These values are then saved on a per-mapping basis, and can be used in the `perform` or `performBatch` methods when events are sent through the mapping.
+
+### Example (LinkedIn Conversions API)
+
+This example has been shorted for brevity. The full code can be seen in the LinkedIn Conversions API 'streamConversion' action.
+
+```js
+const action: ActionDefinition = {
+ title: 'Stream Conversion Event',
+ ...
+ hooks: {
+ 'onMappingSave': {
+ type: 'onMappingSave',
+ label: 'Create a Conversion Rule',
+ description:
+ 'When saving this mapping, we will create a conversion rule in LinkedIn using the fields you provided.',
+ inputFields: {
+ name: {
+ type: 'string',
+ label: 'Name',
+ description: 'The name of the conversion rule.',
+ required: true
+ },
+ conversionType: {
+ type: 'string',
+ label: 'Conversion Type',
+ description: 'The type of conversion rule.',
+ required: true
+ },
+ },
+ outputTypes: {
+ id: {
+ type: 'string',
+ label: 'ID',
+ description: 'The ID of the conversion rule.',
+ required: true
+ },
+ name: {
+ type: 'string',
+ label: 'Name',
+ description: 'The name of the conversion rule.',
+ required: true
+ },
+ },
+ performHook: async (request, { hookInputs }) => {
+ const { data } =
+ await request
+ ('https://api.linkedin.com/rest/conversions', {
+ method: 'post',
+ json: {
+ name: hookInputs.name,
+ type: hookInputs.conversionType
+ }
+ })
+
+ return {
+ successMessage:
+ `Conversion rule ${data.id} created successfully!`,
+ savedData: {
+ id: data.id,
+ name: data.name,
+ }
+ }
+ }
+ }
+ },
+ perform: (request, data) => {
+ return request('https://example.com', {
+ method: 'post',
+ json: {
+ conversion: data.hookOutputs?.onMappingSave?.id,
+ name: data.hookOutputs?.onMappingSave?.name
+ }
+ })
+ }
+ }
+```
+
+### `onMappingSave` hook
+
+The `onMappingSave` hook is triggered after a user clicks 'Save' on a mapping. The result of the hook is then saved to the users configuration as if it were a normal field. Builders can access the saved values in the `perform` block by referencing `data.hookOutputs?.onMappingSave?.`.
+
## Audience Support (Pilot)
In order to support audience destinations, we've introduced a type that extends regular destinations:
@@ -646,7 +820,7 @@ For any issues, please contact our support team at partner-support@segment.com.
MIT License
-Copyright (c) 2023 Segment
+Copyright (c) 2024 Segment
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/docs/authentication.md b/docs/authentication.md
index cd53517165..65dd4d9866 100644
--- a/docs/authentication.md
+++ b/docs/authentication.md
@@ -98,20 +98,25 @@ const destination = {
}
```
-## OAuth2 Authentication Scheme
+## OAuth 2.0 Managed Authentication Scheme
-_oauth authentication is not generally available to external partners as part of Developer Center Pilot. Please contact Segment team if you require it._
+OAuth 2.0 Managed Authentication scheme is the model to be used for destination APIs which support [OAuth 2.0](https://oauth.net/2/). You’ll be able to define a `refreshAccessToken` function if you want the framework to refresh expired tokens.
-OAuth2 Authentication scheme is the model to be used for destination APIs which support [OAuth 2.0](https://oauth.net/2/). You’ll be able to define a `refreshAccessToken` function if you want the framework to refresh expired tokens.
+You will have a new `auth` object available in `extendRequest` and `refreshAccessToken` which will surface your destination’s accessToken, refreshToken, clientId and clientSecret (these last two only available in `refreshAccessToken`). Most destination APIs expect the access token to be used as part of the authorization header in every request. You can use `extendRequest` to define that header.
-You will have a new `auth` object available in `extendRequest` and `refreshAccessToken` which will surface your destination’s accessToken, refreshToken, clientId and clientSecret (these last two only available in `refreshAccessToken`).
+Once your Actions code is deployed and you've received an invitation to manage the destination within our developer portal, you can then provide Segment with the following OAuth parameters.
-Most destination APIs expect the access token to be used as part of the authorization header in every request. You can use `extendRequest` to define that header.
+- Client ID
+- Client Secret
+- Authorization server URL
+ - Specify where to send users to authenticate with your API.
+- Access token server URL
+ - Enter the API endpoint URL where Segment sends the approval code on user redirect.
```
authentication: {
- scheme: 'oauth2',
+ scheme: 'oauth-managed',
fields: {
subdomain: {
type: 'string',
@@ -148,8 +153,6 @@ authentication: {
}
```
-**Note:** OAuth directly depends on the oauth providers available in Segment's internal OAuth Service. Please contact Segment if you require OAuth for your destination.
-
## Unsupported Authentication Schemes
We will explore adding built-in support for more authentication schemes when there is sufficient demand. These might include:
diff --git a/docs/testing.md b/docs/testing.md
index fe47fd8b3e..68c5d39234 100644
--- a/docs/testing.md
+++ b/docs/testing.md
@@ -42,6 +42,8 @@ The default port is set to `3000`. To use a different port, you can specify the
After running the `serve` command, select the destination you want to test locally. Once a destination is selected the server should start up.
+You can also run the serve command for a specific Destination without the Web UI being started up. For example `./bin/run serve --destination=criteo-audiences -n` will start the process for the criteo-audiences Destination, but will not start the Actions Tester web user interface.
+
### Testing an Action's perform() or performBatch() function
To test a specific destination action's perform() or performBatch() function you can send a Postman or cURL request with the following URL format: `https://localhost:/`. A list of eligible URLs will also be provided by the CLI command when the server is spun up.
@@ -325,7 +327,7 @@ export NODE_ENV=test
## Code Coverage
-Code coverage is automatically collected upon completion of `yarn test`. Results may be inspected by examining the HTML report found at `coverage/lcov-report/index.html`, or directly in your IDE if _lcov_ is supported.
+Code coverage is collected upon completion of `yarn test --coverage`. Results may be inspected by examining the HTML report found at `coverage/lcov-report/index.html`, or directly in your IDE if _lcov_ is supported.
## Post Deployment Change Testing
diff --git a/nx.json b/nx.json
new file mode 100644
index 0000000000..3fb6d6e687
--- /dev/null
+++ b/nx.json
@@ -0,0 +1,37 @@
+{
+ "$schema": "./node_modules/nx/schemas/nx-schema.json",
+ "namedInputs": {
+ "sharedGlobals": ["{workspaceRoot}/nx.json", "{workspaceRoot}/tsconfig.json"],
+ "default": ["{projectRoot}/**/*", "sharedGlobals"],
+ "production": [
+ "default",
+ "{projectRoot}/tsconfig.json",
+ "{projectRoot}/tsconfig.build.json",
+ "{projectRoot}/webpack.config*",
+ "{projectRoot}/babel.config*",
+ "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
+ "!{projectRoot}/**/test/**/*"
+ ]
+ },
+ "tasksRunnerOptions": {
+ "default": {
+ "runner": "nx/tasks-runners/default",
+ "options": {
+ "cacheableOperations": ["build", "test"]
+ }
+ }
+ },
+ "targetDefaults": {
+ "build": {
+ "inputs": ["production", "^production"],
+ "dependsOn": ["^build"]
+ },
+ "test": {
+ "inputs": ["default", "^production"],
+ "dependsOn": ["build"]
+ }
+ },
+ "affected": {
+ "defaultBase": "main"
+ }
+}
diff --git a/package.json b/package.json
index 919d3caead..042bfd9c5b 100644
--- a/package.json
+++ b/package.json
@@ -18,13 +18,13 @@
"cli-internal": "yarn workspace @segment/actions-cli-internal",
"core": "yarn workspace @segment/actions-core",
"bootstrap": "lerna bootstrap",
- "build": "./bin/run generate:types && lerna run build --concurrency 1 --stream --ignore @segment/actions-cli-internal && yarn browser build-web",
- "build:browser-destinations": "yarn lerna run build --concurrency 1 --scope=@segment/destinations-manifest --include-dependencies --stream && yarn browser build-web",
+ "build": "nx run-many -t build && yarn build:browser-bundles",
+ "build:browser-bundles": "nx build @segment/destinations-manifest && nx build-web @segment/browser-destinations",
"types": "./bin/run generate:types",
"validate": "./bin/run validate",
"lint": "ls -d ./packages/* | xargs -I {} eslint '{}/**/*.ts' --cache",
"subscriptions": "yarn workspace @segment/destination-subscriptions",
- "test": "lerna run test --stream",
+ "test": "nx run-many -t test",
"test-partners": "lerna run test --stream --ignore @segment/actions-core --ignore @segment/actions-cli --ignore @segment/ajv-human-errors",
"test-browser": "bash scripts/test-browser.sh",
"typecheck": "lerna run typecheck --stream",
@@ -68,7 +68,7 @@
"prettier": "^2.4.1",
"process": "^0.11.10",
"timers-browserify": "^2.0.12",
- "ts-jest": "^27.0.0",
+ "ts-jest": "^27.0.7",
"ts-node": "^9.1.1",
"typescript": "4.3.5",
"ws": "^8.5.0"
diff --git a/packages/actions-shared/README.md b/packages/actions-shared/README.md
index 15f413080d..53ac1334f5 100644
--- a/packages/actions-shared/README.md
+++ b/packages/actions-shared/README.md
@@ -6,7 +6,7 @@ Shared definitions and utilities
MIT License
-Copyright (c) 2023 Segment
+Copyright (c) 2024 Segment
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/packages/actions-shared/package.json b/packages/actions-shared/package.json
index 0b7290b9c7..a464e9a0b8 100644
--- a/packages/actions-shared/package.json
+++ b/packages/actions-shared/package.json
@@ -1,7 +1,7 @@
{
"name": "@segment/actions-shared",
"description": "Shared destination action methods and definitions.",
- "version": "1.65.0",
+ "version": "1.84.0",
"repository": {
"type": "git",
"url": "https://github.com/segmentio/action-destinations",
@@ -23,7 +23,7 @@
"registry": "https://registry.npmjs.org"
},
"scripts": {
- "build": "yarn clean && yarn tsc -b tsconfig.build.json",
+ "build": "yarn tsc -b tsconfig.build.json",
"clean": "tsc -b tsconfig.build.json --clean",
"postclean": "rm -rf dist",
"prepublishOnly": "yarn build",
@@ -37,7 +37,7 @@
},
"dependencies": {
"@amplitude/ua-parser-js": "^0.7.25",
- "@segment/actions-core": "^3.83.0",
+ "@segment/actions-core": "^3.103.0",
"cheerio": "^1.0.0-rc.10",
"dayjs": "^1.10.7",
"escape-goat": "^3",
@@ -46,6 +46,11 @@
},
"jest": {
"preset": "ts-jest",
+ "globals": {
+ "ts-jest": {
+ "isolatedModules": true
+ }
+ },
"testEnvironment": "node",
"modulePathIgnorePatterns": [
"/dist/"
diff --git a/packages/ajv-human-errors/README.md b/packages/ajv-human-errors/README.md
index 64cccbf30d..d61c221fbe 100644
--- a/packages/ajv-human-errors/README.md
+++ b/packages/ajv-human-errors/README.md
@@ -202,7 +202,7 @@ Returns this error message when validating a non-string object:
MIT License
-Copyright (c) 2023 Segment
+Copyright (c) 2024 Segment
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/packages/ajv-human-errors/package.json b/packages/ajv-human-errors/package.json
index 811f31a97a..f81911b264 100644
--- a/packages/ajv-human-errors/package.json
+++ b/packages/ajv-human-errors/package.json
@@ -1,6 +1,6 @@
{
"name": "@segment/ajv-human-errors",
- "version": "2.11.3",
+ "version": "2.12.0",
"description": "Human-readable error messages for Ajv (Another JSON Schema Validator).",
"repository": {
"type": "git",
diff --git a/packages/browser-destination-runtime/package.json b/packages/browser-destination-runtime/package.json
index e02c841693..adc0d9196c 100644
--- a/packages/browser-destination-runtime/package.json
+++ b/packages/browser-destination-runtime/package.json
@@ -1,15 +1,17 @@
{
"name": "@segment/browser-destination-runtime",
- "version": "1.14.0",
+ "version": "1.33.0",
"license": "MIT",
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org"
},
"scripts": {
- "build": "yarn build:esm && yarn build:cjs",
- "build:esm": "tsc --outDir ./dist/esm",
- "build:cjs": "tsc --module commonjs --outDir ./dist/cjs"
+ "build-ts": "yarn tsc -p tsconfig.build.json",
+ "build": "yarn build-ts && yarn build:esm && yarn build:cjs",
+ "build:esm": "tsc -p tsconfig.build.json --outDir ./dist/esm",
+ "build:cjs": "tsc -p tsconfig.build.json --module commonjs --outDir ./dist/cjs",
+ "clean": "tsc -b tsconfig.build.json --clean"
},
"exports": {
".": {
@@ -60,12 +62,29 @@
}
},
"dependencies": {
- "@segment/actions-core": "^3.83.0"
+ "@segment/actions-core": "^3.103.0"
},
"devDependencies": {
"@segment/analytics-next": "*"
},
"peerDependencies": {
"@segment/analytics-next": "*"
+ },
+ "jest": {
+ "preset": "ts-jest",
+ "globals": {
+ "ts-jest": {
+ "isolatedModules": true
+ }
+ },
+ "testEnvironment": "node",
+ "modulePathIgnorePatterns": [
+ "/dist/"
+ ],
+ "moduleNameMapper": {
+ "@segment/ajv-human-errors": "/../ajv-human-errors/src",
+ "@segment/actions-core": "/../core/src",
+ "@segment/destination-subscriptions": "/../destination-subscriptions/src"
+ }
}
}
diff --git a/packages/browser-destination-runtime/tsconfig.build.json b/packages/browser-destination-runtime/tsconfig.build.json
new file mode 100644
index 0000000000..8c38965829
--- /dev/null
+++ b/packages/browser-destination-runtime/tsconfig.build.json
@@ -0,0 +1,22 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "module": "esnext",
+ "composite": true,
+ "outDir": "dist",
+ "rootDir": "src",
+ "allowJs": true,
+ "importHelpers": true,
+ "lib": ["es2020", "dom"],
+ "baseUrl": ".",
+ "paths": {
+ "@segment/actions-core/*": ["../core/src/*"]
+ }
+ },
+ "exclude": ["**/__tests__/**/*.ts"],
+ "include": ["src"],
+ "references": [
+ { "path": "../core/tsconfig.build.json" },
+ { "path": "../destination-subscriptions/tsconfig.build.json" }
+ ]
+}
diff --git a/packages/browser-destination-runtime/tsconfig.json b/packages/browser-destination-runtime/tsconfig.json
index b311e107d5..81a9959f93 100644
--- a/packages/browser-destination-runtime/tsconfig.json
+++ b/packages/browser-destination-runtime/tsconfig.json
@@ -3,7 +3,14 @@
"compilerOptions": {
"module": "esnext",
"removeComments": false,
- "baseUrl": "."
+ "baseUrl": ".",
+ "paths": {
+ "@segment/actions-core": ["../core/src"],
+ "@segment/actions-core/*": ["../core/src/*"],
+ "@segment/actions-shared": ["../actions-shared/src"],
+ "@segment/action-destinations": ["../destination-actions/src"],
+ "@segment/destination-subscriptions": ["../destination-subscriptions/src"]
+ }
},
"exclude": ["dist"]
-}
\ No newline at end of file
+}
diff --git a/packages/browser-destinations-integration-tests/package.json b/packages/browser-destinations-integration-tests/package.json
index 3843b78a24..17f02fab8a 100644
--- a/packages/browser-destinations-integration-tests/package.json
+++ b/packages/browser-destinations-integration-tests/package.json
@@ -12,7 +12,7 @@
"test:sauce": "wdio wdio.conf.sauce.ts",
"test:local": "wdio wdio.conf.local.ts",
"start-destination-server": "yarn ts-node src/server/start-destination-server.ts",
- "browser-destinations:build": "NODE_ENV=production yarn lerna run build --scope=@segment/browser-destinations --include-dependencies --stream"
+ "browser-destinations:build": "NODE_ENV=production yarn nx build-web @segment/browser-destinations"
},
"devDependencies": {
"@wdio/cli": "^7.26.0",
diff --git a/packages/browser-destinations/README.md b/packages/browser-destinations/README.md
index 3c863e17f2..4a3ef03f32 100644
--- a/packages/browser-destinations/README.md
+++ b/packages/browser-destinations/README.md
@@ -56,7 +56,7 @@ Coming Soon
MIT License
-Copyright (c) 2023 Segment
+Copyright (c) 2024 Segment
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/packages/browser-destinations/__tests__/window.test.ts b/packages/browser-destinations/__tests__/window.test.ts
new file mode 100644
index 0000000000..b0fdea83f8
--- /dev/null
+++ b/packages/browser-destinations/__tests__/window.test.ts
@@ -0,0 +1,28 @@
+import { Analytics, Context } from '@segment/analytics-next'
+import segmentUtilitiesDestination from '../destinations/segment-utilities-web/src/index'
+
+it('window object shouldnt be changed by actions core', async () => {
+ const windowBefore = Object.keys(window)
+
+ // load a plugin that doesn't alter window object
+ const [plugin] = await segmentUtilitiesDestination({
+ throttleWindow: 3000,
+ passThroughCount: 1,
+ subscriptions: [
+ {
+ partnerAction: 'throttle',
+ name: 'Throttle',
+ enabled: true,
+ subscribe: 'type = "track"',
+ mapping: {}
+ }
+ ]
+ })
+
+ await plugin.load(Context.system(), {} as Analytics)
+
+ const windowAfter = Object.keys(window)
+
+ // window object shouldn't change as long as actions-core isn't changing it
+ expect(windowBefore.sort()).toEqual(windowAfter.sort())
+})
diff --git a/packages/browser-destinations/destinations/1flow/README.md b/packages/browser-destinations/destinations/1flow/README.md
new file mode 100644
index 0000000000..d1f3ce5580
--- /dev/null
+++ b/packages/browser-destinations/destinations/1flow/README.md
@@ -0,0 +1,31 @@
+# @segment/analytics-browser-actions-1flow
+
+The 1Flow browser action destination for use with @segment/analytics-next.
+
+## License
+
+MIT License
+
+Copyright (c) 2024 Segment
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+## Contributing
+
+All third party contributors acknowledge that any contributions they provide will be made under the same open source license that the open source project is provided under.
diff --git a/packages/browser-destinations/destinations/1flow/package.json b/packages/browser-destinations/destinations/1flow/package.json
new file mode 100644
index 0000000000..eac6a2e696
--- /dev/null
+++ b/packages/browser-destinations/destinations/1flow/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "@segment/analytics-browser-actions-1flow",
+ "version": "1.16.0",
+ "license": "MIT",
+ "publishConfig": {
+ "access": "public",
+ "registry": "https://registry.npmjs.org"
+ },
+ "main": "./dist/cjs",
+ "module": "./dist/esm",
+ "scripts": {
+ "build": "yarn build:esm && yarn build:cjs",
+ "build:cjs": "tsc --module commonjs --outDir ./dist/cjs",
+ "build:esm": "tsc --outDir ./dist/esm"
+ },
+ "typings": "./dist/esm",
+ "dependencies": {
+ "@segment/browser-destination-runtime": "^1.33.0"
+ },
+ "peerDependencies": {
+ "@segment/analytics-next": ">=1.55.0"
+ }
+}
diff --git a/packages/browser-destinations/destinations/1flow/src/1flow.ts b/packages/browser-destinations/destinations/1flow/src/1flow.ts
new file mode 100644
index 0000000000..a7e07e105c
--- /dev/null
+++ b/packages/browser-destinations/destinations/1flow/src/1flow.ts
@@ -0,0 +1,22 @@
+/* eslint-disable */
+// @ts-nocheck
+
+export function initScript({ projectApiKey }) {
+ //Set your APP_ID
+ const apiKey = projectApiKey
+
+ const autoURLTracking = false
+ ;(function (w, o, s, t, k, a, r) {
+ ;(w._1flow = function (e, d, v) {
+ s(function () {
+ w._1flow(e, d, !v ? {} : v)
+ }, 5)
+ }),
+ (a = o.getElementsByTagName('head')[0])
+ r = o.createElement('script')
+ r.async = 1
+ r.setAttribute('data-api-key', k)
+ r.src = t
+ a.appendChild(r)
+ })(window, document, setTimeout, 'https://1flow.app/js/1flow.js', apiKey)
+}
diff --git a/packages/browser-destinations/destinations/1flow/src/__tests__/index.test.ts b/packages/browser-destinations/destinations/1flow/src/__tests__/index.test.ts
new file mode 100644
index 0000000000..cd839e0ed5
--- /dev/null
+++ b/packages/browser-destinations/destinations/1flow/src/__tests__/index.test.ts
@@ -0,0 +1,14 @@
+import _1FlowDestination from '../index'
+import { _1Flow } from '../api'
+
+describe('_1Flow', () => {
+ beforeAll(() => {
+ jest.mock('@segment/browser-destination-runtime/load-script', () => ({
+ loadScript: (_src: any, _attributes: any) => {}
+ }))
+ jest.mock('@segment/browser-destination-runtime/resolve-when', () => ({
+ resolveWhen: (_fn: any, _timeout: any) => {}
+ }))
+ })
+ test('it maps event parameters correctly to identify function ', async () => {})
+})
diff --git a/packages/browser-destinations/destinations/1flow/src/api.ts b/packages/browser-destinations/destinations/1flow/src/api.ts
new file mode 100644
index 0000000000..b8279d0f4e
--- /dev/null
+++ b/packages/browser-destinations/destinations/1flow/src/api.ts
@@ -0,0 +1,11 @@
+type method = 'track' | 'identify'
+
+type _1FlowApi = {
+ richLinkProperties: string[] | undefined
+ activator: string | undefined
+ projectApiKey: string
+}
+
+type _1FlowFunction = (method: method, ...args: unknown[]) => void
+
+export type _1Flow = _1FlowFunction & _1FlowApi
diff --git a/packages/browser-destinations/destinations/1flow/src/generated-types.ts b/packages/browser-destinations/destinations/1flow/src/generated-types.ts
new file mode 100644
index 0000000000..ad85489880
--- /dev/null
+++ b/packages/browser-destinations/destinations/1flow/src/generated-types.ts
@@ -0,0 +1,8 @@
+// Generated file. DO NOT MODIFY IT BY HAND.
+
+export interface Settings {
+ /**
+ * This is the unique app_id for your 1Flow application, serving as the identifier for data storage and retrieval. This field is mandatory.
+ */
+ projectApiKey: string
+}
diff --git a/packages/browser-destinations/destinations/1flow/src/identifyUser/__tests__/index.test.ts b/packages/browser-destinations/destinations/1flow/src/identifyUser/__tests__/index.test.ts
new file mode 100644
index 0000000000..f080cf0579
--- /dev/null
+++ b/packages/browser-destinations/destinations/1flow/src/identifyUser/__tests__/index.test.ts
@@ -0,0 +1,14 @@
+import _1FlowDestination from '../../index'
+
+describe('identify', () => {
+ beforeAll(() => {
+ jest.mock('@segment/browser-destination-runtime/load-script', () => ({
+ loadScript: (_src: any, _attributes: any) => {}
+ }))
+ jest.mock('@segment/browser-destination-runtime/resolve-when', () => ({
+ resolveWhen: (_fn: any, _timeout: any) => {}
+ }))
+ })
+
+ test('it maps event parameters correctly to identify function ', async () => {})
+})
diff --git a/packages/browser-destinations/destinations/1flow/src/identifyUser/generated-types.ts b/packages/browser-destinations/destinations/1flow/src/identifyUser/generated-types.ts
new file mode 100644
index 0000000000..f5b1336f24
--- /dev/null
+++ b/packages/browser-destinations/destinations/1flow/src/identifyUser/generated-types.ts
@@ -0,0 +1,34 @@
+// Generated file. DO NOT MODIFY IT BY HAND.
+
+export interface Payload {
+ /**
+ * A unique identifier for the user.
+ */
+ userId?: string
+ /**
+ * An anonymous identifier for the user.
+ */
+ anonymousId?: string
+ /**
+ * The user's custom attributes.
+ */
+ traits?: {
+ [k: string]: unknown
+ }
+ /**
+ * The user's first name.
+ */
+ first_name?: string
+ /**
+ * The user's last name.
+ */
+ last_name?: string
+ /**
+ * The user's phone number.
+ */
+ phone?: string
+ /**
+ * The user's email address.
+ */
+ email?: string
+}
diff --git a/packages/browser-destinations/destinations/1flow/src/identifyUser/index.ts b/packages/browser-destinations/destinations/1flow/src/identifyUser/index.ts
new file mode 100644
index 0000000000..80401e35a7
--- /dev/null
+++ b/packages/browser-destinations/destinations/1flow/src/identifyUser/index.ts
@@ -0,0 +1,90 @@
+import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
+import { _1Flow } from '../api'
+import type { Settings } from '../generated-types'
+import type { Payload } from './generated-types'
+
+const action: BrowserActionDefinition = {
+ title: 'Identify User',
+ description: 'Create or update a user in 1Flow.',
+ defaultSubscription: 'type = "identify"',
+ platform: 'web',
+ fields: {
+ userId: {
+ description: 'A unique identifier for the user.',
+ label: 'User ID',
+ type: 'string',
+ required: false,
+ default: {
+ '@path': '$.userId'
+ }
+ },
+ anonymousId: {
+ description: 'An anonymous identifier for the user.',
+ label: 'Anonymous ID',
+ type: 'string',
+ required: false,
+ default: {
+ '@path': '$.anonymousId'
+ }
+ },
+ traits: {
+ description: "The user's custom attributes.",
+ label: 'Custom Attributes',
+ type: 'object',
+ required: false,
+ defaultObjectUI: 'keyvalue',
+ default: {
+ '@path': '$.traits'
+ }
+ },
+ first_name: {
+ description: "The user's first name.",
+ label: 'First Name',
+ type: 'string',
+ required: false,
+ default: {
+ '@path': '$.traits.first_name'
+ }
+ },
+ last_name: {
+ description: "The user's last name.",
+ label: 'First Name',
+ type: 'string',
+ required: false,
+ default: {
+ '@path': '$.traits.last_name'
+ }
+ },
+ phone: {
+ description: "The user's phone number.",
+ label: 'Phone Number',
+ type: 'string',
+ required: false,
+ default: {
+ '@path': '$.traits.phone'
+ }
+ },
+
+ email: {
+ description: "The user's email address.",
+ label: 'Email Address',
+ type: 'string',
+ required: false,
+ default: {
+ '@path': '$.traits.email'
+ }
+ }
+ },
+ perform: (_1Flow, event) => {
+ const { userId, anonymousId, traits, first_name, last_name, phone, email } = event.payload
+ _1Flow('identify', userId, anonymousId, {
+ ...traits,
+ first_name: first_name,
+ last_name: last_name,
+ phone: phone,
+ email: email
+ })
+ }
+}
+
+export default action
diff --git a/packages/browser-destinations/destinations/1flow/src/index.ts b/packages/browser-destinations/destinations/1flow/src/index.ts
new file mode 100644
index 0000000000..59edc58318
--- /dev/null
+++ b/packages/browser-destinations/destinations/1flow/src/index.ts
@@ -0,0 +1,58 @@
+import type { Settings } from './generated-types'
+import type { BrowserDestinationDefinition } from '@segment/browser-destination-runtime/types'
+import { browserDestination } from '@segment/browser-destination-runtime/shim'
+import trackEvent from './trackEvent'
+import { initScript } from './1flow'
+import { _1Flow } from './api'
+import identifyUser from './identifyUser'
+import { defaultValues } from '@segment/actions-core'
+declare global {
+ interface Window {
+ _1Flow: _1Flow
+ }
+}
+
+export const destination: BrowserDestinationDefinition = {
+ name: '1Flow Web (Actions)',
+ slug: 'actions-1flow',
+ mode: 'device',
+ description: 'Send analytics from Segment to 1Flow',
+ settings: {
+ projectApiKey: {
+ description:
+ 'This is the unique app_id for your 1Flow application, serving as the identifier for data storage and retrieval. This field is mandatory.',
+ label: 'Project API Key',
+ type: 'string',
+ required: true
+ }
+ },
+ presets: [
+ {
+ name: 'Track Event',
+ subscribe: 'type = "track"',
+ partnerAction: 'trackEvent',
+ mapping: defaultValues(trackEvent.fields),
+ type: 'automatic'
+ },
+ {
+ name: 'Identify User',
+ subscribe: 'type = "identify"',
+ partnerAction: 'identifyUser',
+ mapping: defaultValues(identifyUser.fields),
+ type: 'automatic'
+ }
+ ],
+
+ initialize: async ({ settings }, deps) => {
+ const projectApiKey = settings.projectApiKey
+ initScript({ projectApiKey })
+ await deps.resolveWhen(() => Object.prototype.hasOwnProperty.call(window, '_1Flow'), 100)
+ return window._1Flow
+ },
+ actions: {
+ trackEvent,
+ identifyUser
+ }
+}
+
+export default browserDestination(destination)
diff --git a/packages/browser-destinations/destinations/1flow/src/trackEvent/__tests__/index.test.ts b/packages/browser-destinations/destinations/1flow/src/trackEvent/__tests__/index.test.ts
new file mode 100644
index 0000000000..8564c31fea
--- /dev/null
+++ b/packages/browser-destinations/destinations/1flow/src/trackEvent/__tests__/index.test.ts
@@ -0,0 +1,14 @@
+import _1flowDestination from '../../index'
+
+describe('track', () => {
+ beforeAll(() => {
+ jest.mock('@segment/browser-destination-runtime/load-script', () => ({
+ loadScript: (_src: any, _attributes: any) => {}
+ }))
+ jest.mock('@segment/browser-destination-runtime/resolve-when', () => ({
+ resolveWhen: (_fn: any, _timeout: any) => {}
+ }))
+ })
+
+ test('it maps event parameters correctly to track function', async () => {})
+})
diff --git a/packages/browser-destinations/destinations/1flow/src/trackEvent/generated-types.ts b/packages/browser-destinations/destinations/1flow/src/trackEvent/generated-types.ts
new file mode 100644
index 0000000000..5c7fd1c746
--- /dev/null
+++ b/packages/browser-destinations/destinations/1flow/src/trackEvent/generated-types.ts
@@ -0,0 +1,22 @@
+// Generated file. DO NOT MODIFY IT BY HAND.
+
+export interface Payload {
+ /**
+ * The name of the event.
+ */
+ event_name: string
+ /**
+ * A unique identifier for the user.
+ */
+ userId?: string
+ /**
+ * An anonymous identifier for the user.
+ */
+ anonymousId?: string
+ /**
+ * Information associated with the event
+ */
+ properties?: {
+ [k: string]: unknown
+ }
+}
diff --git a/packages/browser-destinations/destinations/1flow/src/trackEvent/index.ts b/packages/browser-destinations/destinations/1flow/src/trackEvent/index.ts
new file mode 100644
index 0000000000..0fec9ca043
--- /dev/null
+++ b/packages/browser-destinations/destinations/1flow/src/trackEvent/index.ts
@@ -0,0 +1,59 @@
+import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
+import { _1Flow } from '../api'
+import type { Settings } from '../generated-types'
+import type { Payload } from './generated-types'
+
+const action: BrowserActionDefinition = {
+ title: 'Track Event',
+ description: 'Submit an event to 1Flow.',
+ defaultSubscription: 'type = "track"',
+ platform: 'web',
+ fields: {
+ event_name: {
+ description: 'The name of the event.',
+ label: 'Event Name',
+ type: 'string',
+ required: true,
+ default: {
+ '@path': '$.event'
+ }
+ },
+ userId: {
+ description: 'A unique identifier for the user.',
+ label: 'User ID',
+ type: 'string',
+ required: false,
+ default: {
+ '@path': '$.userId'
+ }
+ },
+ anonymousId: {
+ description: 'An anonymous identifier for the user.',
+ label: 'Anonymous ID',
+ type: 'string',
+ required: false,
+ default: {
+ '@path': '$.anonymousId'
+ }
+ },
+ properties: {
+ description: 'Information associated with the event',
+ label: 'Event Properties',
+ type: 'object',
+ required: false,
+ default: {
+ '@path': '$.properties'
+ }
+ }
+ },
+ perform: (_1Flow, event) => {
+ const { event_name, userId, anonymousId, properties } = event.payload
+ _1Flow('track', event_name, {
+ userId: userId,
+ anonymousId: anonymousId,
+ properties: properties
+ })
+ }
+}
+
+export default action
diff --git a/packages/browser-destinations/destinations/1flow/tsconfig.json b/packages/browser-destinations/destinations/1flow/tsconfig.json
new file mode 100644
index 0000000000..c2a7897afd
--- /dev/null
+++ b/packages/browser-destinations/destinations/1flow/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.build.json",
+ "compilerOptions": {
+ "rootDir": "./src",
+ "baseUrl": "."
+ },
+ "include": ["src"],
+ "exclude": ["dist", "**/__tests__"]
+}
diff --git a/packages/browser-destinations/destinations/adobe-target/package.json b/packages/browser-destinations/destinations/adobe-target/package.json
index 78b0f2144f..90d0c7942a 100644
--- a/packages/browser-destinations/destinations/adobe-target/package.json
+++ b/packages/browser-destinations/destinations/adobe-target/package.json
@@ -1,6 +1,6 @@
{
"name": "@segment/analytics-browser-actions-adobe-target",
- "version": "1.15.0",
+ "version": "1.34.0",
"license": "MIT",
"publishConfig": {
"access": "public",
@@ -16,7 +16,7 @@
},
"typings": "./dist/esm",
"dependencies": {
- "@segment/browser-destination-runtime": "^1.14.0"
+ "@segment/browser-destination-runtime": "^1.33.0"
},
"peerDependencies": {
"@segment/analytics-next": ">=1.55.0"
diff --git a/packages/browser-destinations/destinations/algolia-plugins/README.md b/packages/browser-destinations/destinations/algolia-plugins/README.md
new file mode 100644
index 0000000000..a6d1391fd6
--- /dev/null
+++ b/packages/browser-destinations/destinations/algolia-plugins/README.md
@@ -0,0 +1,31 @@
+# @segment/analytics-browser-actions-algolia-plugins
+
+The Algolia Plugins browser action destination for use with @segment/analytics-next.
+
+## License
+
+MIT License
+
+Copyright (c) 2023 Segment
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+## Contributing
+
+All third party contributors acknowledge that any contributions they provide will be made under the same open source license that the open source project is provided under.
diff --git a/packages/browser-destinations/destinations/algolia-plugins/package.json b/packages/browser-destinations/destinations/algolia-plugins/package.json
new file mode 100644
index 0000000000..11371cb1d5
--- /dev/null
+++ b/packages/browser-destinations/destinations/algolia-plugins/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "@segment/analytics-browser-actions-algolia-plugins",
+ "version": "1.11.0",
+ "license": "MIT",
+ "publishConfig": {
+ "access": "public",
+ "registry": "https://registry.npmjs.org"
+ },
+ "main": "./dist/cjs",
+ "module": "./dist/esm",
+ "scripts": {
+ "build": "yarn build:esm && yarn build:cjs",
+ "build:cjs": "tsc --module commonjs --outDir ./dist/cjs",
+ "build:esm": "tsc --outDir ./dist/esm"
+ },
+ "typings": "./dist/esm",
+ "dependencies": {
+ "@segment/browser-destination-runtime": "^1.33.0"
+ },
+ "peerDependencies": {
+ "@segment/analytics-next": ">=1.55.0"
+ }
+}
diff --git a/packages/browser-destinations/destinations/algolia-plugins/src/__tests__/index.test.ts b/packages/browser-destinations/destinations/algolia-plugins/src/__tests__/index.test.ts
new file mode 100644
index 0000000000..0ce3cffc05
--- /dev/null
+++ b/packages/browser-destinations/destinations/algolia-plugins/src/__tests__/index.test.ts
@@ -0,0 +1,52 @@
+import { Analytics, Context, Plugin } from '@segment/analytics-next'
+import { Subscription } from '@segment/browser-destination-runtime/types'
+import browserPluginsDestination from '../'
+import { queryIdIntegrationFieldName } from '../utils'
+
+const example: Subscription[] = [
+ {
+ partnerAction: 'algoliaPlugin',
+ name: 'Algolia Plugin',
+ enabled: true,
+ subscribe: 'type = "track"',
+ mapping: {}
+ }
+]
+
+let browserActions: Plugin[]
+let algoliaPlugin: Plugin
+let ajs: Analytics
+
+beforeEach(async () => {
+ browserActions = await browserPluginsDestination({ subscriptions: example })
+ algoliaPlugin = browserActions[0]
+
+ ajs = new Analytics({
+ writeKey: 'w_123'
+ })
+ Object.defineProperty(window, 'location', {
+ value: {
+ search: 'queryID=1234567'
+ },
+ writable: true
+ })
+})
+
+describe('ajs-integration', () => {
+ test('updates the original event with an Algolia query ID', async () => {
+ await algoliaPlugin.load(Context.system(), ajs)
+
+ const ctx = new Context({
+ type: 'track',
+ event: 'Test Event',
+ properties: {
+ greeting: 'Yo!'
+ }
+ })
+
+ const updatedCtx = await algoliaPlugin.track?.(ctx)
+
+ const algoliaIntegrationsObj = updatedCtx?.event?.integrations['Algolia Insights (Actions)']
+ expect(algoliaIntegrationsObj[queryIdIntegrationFieldName]).toEqual('1234567')
+ })
+})
diff --git a/packages/browser-destinations/destinations/algolia-plugins/src/algoliaPlugin/generated-types.ts b/packages/browser-destinations/destinations/algolia-plugins/src/algoliaPlugin/generated-types.ts
new file mode 100644
index 0000000000..944d22b085
--- /dev/null
+++ b/packages/browser-destinations/destinations/algolia-plugins/src/algoliaPlugin/generated-types.ts
@@ -0,0 +1,3 @@
+// Generated file. DO NOT MODIFY IT BY HAND.
+
+export interface Payload {}
diff --git a/packages/browser-destinations/destinations/algolia-plugins/src/algoliaPlugin/index.ts b/packages/browser-destinations/destinations/algolia-plugins/src/algoliaPlugin/index.ts
new file mode 100644
index 0000000000..86e8accaa8
--- /dev/null
+++ b/packages/browser-destinations/destinations/algolia-plugins/src/algoliaPlugin/index.ts
@@ -0,0 +1,30 @@
+import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
+import type { Settings } from '../generated-types'
+import type { Payload } from './generated-types'
+import { UniversalStorage } from '@segment/analytics-next'
+import { storageFallback, storageQueryIdKey, queryIdIntegrationFieldName } from '../utils'
+
+const action: BrowserActionDefinition = {
+ title: 'Algolia Browser Plugin',
+ description: 'Enriches all Segment payloads with the Algolia query_id value',
+ platform: 'web',
+ hidden: false,
+ defaultSubscription: 'type = "track" or type = "identify" or type = "page" or type = "group" or type = "alias"',
+ fields: {},
+ lifecycleHook: 'enrichment',
+ perform: (_, { context, analytics }) => {
+ const storage = (analytics.storage as UniversalStorage>) ?? storageFallback
+
+ const query_id: string | null = storage.get(storageQueryIdKey)
+
+ if (query_id && (context.event.integrations?.All !== false || context.event.integrations['Algolia Insights (Actions)'])) {
+ const integrationsData: Record = {}
+ integrationsData[queryIdIntegrationFieldName] = query_id
+ context.updateEvent(`integrations.Algolia Insights (Actions)`, integrationsData)
+ }
+
+ return
+ }
+}
+
+export default action
diff --git a/packages/browser-destinations/destinations/algolia-plugins/src/generated-types.ts b/packages/browser-destinations/destinations/algolia-plugins/src/generated-types.ts
new file mode 100644
index 0000000000..1ceb7fa3b9
--- /dev/null
+++ b/packages/browser-destinations/destinations/algolia-plugins/src/generated-types.ts
@@ -0,0 +1,8 @@
+// Generated file. DO NOT MODIFY IT BY HAND.
+
+export interface Settings {
+ /**
+ * QueryString name you use for when storing the Algolia QueryID in a page URL.
+ */
+ queryIdQueryStringName?: string
+}
diff --git a/packages/browser-destinations/destinations/algolia-plugins/src/index.ts b/packages/browser-destinations/destinations/algolia-plugins/src/index.ts
new file mode 100644
index 0000000000..12ac5288f6
--- /dev/null
+++ b/packages/browser-destinations/destinations/algolia-plugins/src/index.ts
@@ -0,0 +1,42 @@
+import type { Settings } from './generated-types'
+import type { BrowserDestinationDefinition } from '@segment/browser-destination-runtime/types'
+import { browserDestination } from '@segment/browser-destination-runtime/shim'
+import { UniversalStorage } from '@segment/analytics-next'
+import { storageFallback, storageQueryIdKey, queryIdQueryStringNameDefault } from './utils'
+
+import algoliaPlugin from './algoliaPlugin'
+
+// Switch from unknown to the partner SDK client types
+export const destination: BrowserDestinationDefinition = {
+ name: 'Algolia Plugins',
+ slug: 'actions-algolia-plugins',
+ mode: 'device',
+ settings: {
+ queryIdQueryStringName: {
+ label: 'QueryID QueryString Name',
+ description: 'QueryString name you use for when storing the Algolia QueryID in a page URL.',
+ type: 'string',
+ default: queryIdQueryStringNameDefault,
+ required: false
+ }
+ },
+ initialize: async ({ analytics, settings }) => {
+ const storage = (analytics.storage as UniversalStorage>) ?? storageFallback
+
+ const urlParams = new URLSearchParams(window.location.search)
+
+ const queryId: string | null =
+ urlParams.get(settings.queryIdQueryStringName ?? queryIdQueryStringNameDefault) || null
+
+ if (queryId) {
+ storage.set(storageQueryIdKey, queryId)
+ }
+
+ return {}
+ },
+ actions: {
+ algoliaPlugin
+ }
+}
+
+export default browserDestination(destination)
diff --git a/packages/browser-destinations/destinations/algolia-plugins/src/utils.ts b/packages/browser-destinations/destinations/algolia-plugins/src/utils.ts
new file mode 100644
index 0000000000..bb9ddb4b62
--- /dev/null
+++ b/packages/browser-destinations/destinations/algolia-plugins/src/utils.ts
@@ -0,0 +1,17 @@
+// The name of the storage location where we'll cache the Query ID value
+export const storageQueryIdKey = 'analytics_algolia_query_id'
+
+export const queryIdQueryStringNameDefault = 'queryID'
+
+// The field name to include for the Algolia query_id in 'context.integrations.Algolia Insights (Actions)'
+export const queryIdIntegrationFieldName = 'query_id'
+
+export const storageFallback = {
+ get: (key: string) => {
+ const data = window.localStorage.getItem(key)
+ return data
+ },
+ set: (key: string, value: string) => {
+ return window.localStorage.setItem(key, value)
+ }
+}
diff --git a/packages/browser-destinations/destinations/algolia-plugins/tsconfig.json b/packages/browser-destinations/destinations/algolia-plugins/tsconfig.json
new file mode 100644
index 0000000000..c2a7897afd
--- /dev/null
+++ b/packages/browser-destinations/destinations/algolia-plugins/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.build.json",
+ "compilerOptions": {
+ "rootDir": "./src",
+ "baseUrl": "."
+ },
+ "include": ["src"],
+ "exclude": ["dist", "**/__tests__"]
+}
diff --git a/packages/browser-destinations/destinations/amplitude-plugins/package.json b/packages/browser-destinations/destinations/amplitude-plugins/package.json
index 5090f61975..9822177965 100644
--- a/packages/browser-destinations/destinations/amplitude-plugins/package.json
+++ b/packages/browser-destinations/destinations/amplitude-plugins/package.json
@@ -1,6 +1,6 @@
{
"name": "@segment/analytics-browser-actions-amplitude-plugins",
- "version": "1.15.0",
+ "version": "1.34.0",
"license": "MIT",
"publishConfig": {
"access": "public",
@@ -15,7 +15,7 @@
},
"typings": "./dist/esm",
"dependencies": {
- "@segment/browser-destination-runtime": "^1.14.0"
+ "@segment/browser-destination-runtime": "^1.33.0"
},
"peerDependencies": {
"@segment/analytics-next": ">=1.55.0"
diff --git a/packages/browser-destinations/destinations/braze-cloud-plugins/package.json b/packages/browser-destinations/destinations/braze-cloud-plugins/package.json
index 84fd76e262..50c54fbb7c 100644
--- a/packages/browser-destinations/destinations/braze-cloud-plugins/package.json
+++ b/packages/browser-destinations/destinations/braze-cloud-plugins/package.json
@@ -1,6 +1,6 @@
{
"name": "@segment/analytics-browser-actions-braze-cloud-plugins",
- "version": "1.16.0",
+ "version": "1.37.0",
"license": "MIT",
"publishConfig": {
"access": "public",
@@ -15,8 +15,8 @@
},
"typings": "./dist/esm",
"dependencies": {
- "@segment/analytics-browser-actions-braze": "^1.16.0",
- "@segment/browser-destination-runtime": "^1.14.0"
+ "@segment/analytics-browser-actions-braze": "^1.37.0",
+ "@segment/browser-destination-runtime": "^1.33.0"
},
"peerDependencies": {
"@segment/analytics-next": ">=1.55.0"
diff --git a/packages/browser-destinations/destinations/braze/package.json b/packages/browser-destinations/destinations/braze/package.json
index 57584cc39a..dbe08afedb 100644
--- a/packages/browser-destinations/destinations/braze/package.json
+++ b/packages/browser-destinations/destinations/braze/package.json
@@ -1,6 +1,6 @@
{
"name": "@segment/analytics-browser-actions-braze",
- "version": "1.16.0",
+ "version": "1.37.0",
"license": "MIT",
"publishConfig": {
"access": "public",
@@ -35,8 +35,8 @@
"dependencies": {
"@braze/web-sdk": "npm:@braze/web-sdk@^4.1.0",
"@braze/web-sdk-v3": "npm:@braze/web-sdk@^3.5.1",
- "@segment/actions-core": "^3.83.0",
- "@segment/browser-destination-runtime": "^1.14.0"
+ "@segment/actions-core": "^3.103.0",
+ "@segment/browser-destination-runtime": "^1.33.0"
},
"peerDependencies": {
"@segment/analytics-next": ">=1.55.0"
diff --git a/packages/browser-destinations/destinations/braze/src/updateUserProfile/index.ts b/packages/browser-destinations/destinations/braze/src/updateUserProfile/index.ts
index 7edf1f64c7..a45ccc5414 100644
--- a/packages/browser-destinations/destinations/braze/src/updateUserProfile/index.ts
+++ b/packages/browser-destinations/destinations/braze/src/updateUserProfile/index.ts
@@ -79,12 +79,7 @@ const action: BrowserActionDefinition
email_subscribe: {
label: 'Email Subscribe',
description: `The user's email subscription preference: “opted_in” (explicitly registered to receive email messages), “unsubscribed” (explicitly opted out of email messages), and “subscribed” (neither opted in nor out).`,
- type: 'string',
- choices: [
- { label: 'OTPED_IN', value: 'opted_in' },
- { label: 'SUBSCRIBED', value: 'subscribed' },
- { label: 'UNSUBSCRIBED', value: 'unsubscribed' }
- ]
+ type: 'string'
},
first_name: {
label: 'First Name',
diff --git a/packages/browser-destinations/destinations/bucket/package.json b/packages/browser-destinations/destinations/bucket/package.json
new file mode 100644
index 0000000000..171c32a74a
--- /dev/null
+++ b/packages/browser-destinations/destinations/bucket/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "@segment/analytics-browser-actions-bucket",
+ "version": "1.14.0",
+ "license": "MIT",
+ "publishConfig": {
+ "access": "public",
+ "registry": "https://registry.npmjs.org"
+ },
+ "main": "./dist/cjs",
+ "module": "./dist/esm",
+ "scripts": {
+ "build": "yarn build:esm && yarn build:cjs",
+ "build:cjs": "tsc --module commonjs --outDir ./dist/cjs",
+ "build:esm": "tsc --outDir ./dist/esm"
+ },
+ "typings": "./dist/esm",
+ "dependencies": {
+ "@bucketco/tracking-sdk": "^2.0.0",
+ "@segment/actions-core": "^3.103.0",
+ "@segment/browser-destination-runtime": "^1.33.0"
+ },
+ "peerDependencies": {
+ "@segment/analytics-next": ">=1.55.0"
+ }
+}
diff --git a/packages/browser-destinations/destinations/bucket/src/__tests__/index.test.ts b/packages/browser-destinations/destinations/bucket/src/__tests__/index.test.ts
new file mode 100644
index 0000000000..b47472f8a4
--- /dev/null
+++ b/packages/browser-destinations/destinations/bucket/src/__tests__/index.test.ts
@@ -0,0 +1,160 @@
+import { Analytics, Context, User } from '@segment/analytics-next'
+import bucketWebDestination, { destination } from '../index'
+import { Subscription } from '@segment/browser-destination-runtime/types'
+import { JSONArray } from '@segment/actions-core/*'
+import { bucketTestHooks, getBucketCallLog } from '../test-utils'
+
+const subscriptions: Subscription[] = [
+ {
+ partnerAction: 'trackEvent',
+ name: 'Track Event',
+ enabled: true,
+ subscribe: 'type = "track"',
+ mapping: {
+ name: {
+ '@path': '$.name'
+ }
+ }
+ }
+]
+
+describe('Bucket', () => {
+ bucketTestHooks()
+
+ it('loads the Bucket SDK', async () => {
+ const [instance] = await bucketWebDestination({
+ trackingKey: 'testTrackingKey',
+ subscriptions: subscriptions as unknown as JSONArray
+ })
+
+ jest.spyOn(destination, 'initialize')
+
+ const analyticsInstance = new Analytics({ writeKey: 'test-writekey' })
+
+ await instance.load(Context.system(), analyticsInstance)
+ expect(destination.initialize).toHaveBeenCalled()
+
+ const scripts = Array.from(window.document.querySelectorAll('script'))
+ expect(scripts).toMatchInlineSnapshot(`
+ Array [
+ ,
+ ,
+ ]
+ `)
+
+ expect(window.bucket).toMatchObject({
+ init: expect.any(Function),
+ user: expect.any(Function),
+ company: expect.any(Function),
+ track: expect.any(Function),
+ reset: expect.any(Function)
+ })
+ })
+
+ it('resets the Bucket SDK', async () => {
+ const [instance] = await bucketWebDestination({
+ trackingKey: 'testTrackingKey',
+ subscriptions: subscriptions as unknown as JSONArray
+ })
+
+ const analyticsInstance = new Analytics({ writeKey: 'test-writekey' })
+
+ await instance.load(Context.system(), analyticsInstance)
+
+ analyticsInstance.reset()
+
+ expect(getBucketCallLog()).toStrictEqual([
+ { method: 'init', args: ['testTrackingKey', {}] },
+ { method: 'reset', args: [] }
+ ])
+ })
+
+ it('passes options to bucket.init()', async () => {
+ const [instance] = await bucketWebDestination({
+ trackingKey: 'testTrackingKey',
+ host: 'http://localhost:3200',
+ subscriptions: subscriptions as unknown as JSONArray
+ })
+
+ const analyticsInstance = new Analytics({ writeKey: 'test-writekey' })
+
+ await instance.load(Context.system(), analyticsInstance)
+
+ expect(getBucketCallLog()).toStrictEqual([
+ { method: 'init', args: ['testTrackingKey', { host: 'http://localhost:3200' }] }
+ ])
+ })
+
+ it('allows sdkVersion override', async () => {
+ const [instance] = await bucketWebDestination({
+ trackingKey: 'testTrackingKey',
+ sdkVersion: 'latest',
+ subscriptions: subscriptions as unknown as JSONArray
+ })
+
+ const analyticsInstance = new Analytics({ writeKey: 'test-writekey' })
+
+ await instance.load(Context.system(), analyticsInstance)
+
+ const scripts = Array.from(window.document.querySelectorAll('script'))
+ expect(scripts).toMatchInlineSnapshot(`
+ Array [
+ ,
+ ,
+ ]
+ `)
+
+ expect(getBucketCallLog()).toStrictEqual([{ method: 'init', args: ['testTrackingKey', {}] }])
+ })
+
+ describe('when not logged in', () => {
+ it('initializes Bucket SDK', async () => {
+ const [instance] = await bucketWebDestination({
+ trackingKey: 'testTrackingKey',
+ subscriptions: subscriptions as unknown as JSONArray
+ })
+
+ const analyticsInstance = new Analytics({ writeKey: 'test-writekey' })
+
+ await instance.load(Context.system(), analyticsInstance)
+
+ expect(getBucketCallLog()).toStrictEqual([{ method: 'init', args: ['testTrackingKey', {}] }])
+ })
+ })
+
+ describe('when logged in', () => {
+ it('initializes Bucket SDK and registers user', async () => {
+ const [instance] = await bucketWebDestination({
+ trackingKey: 'testTrackingKey',
+ subscriptions: subscriptions as unknown as JSONArray
+ })
+
+ const analyticsInstance = new Analytics({ writeKey: 'test-writekey' })
+ jest.spyOn(analyticsInstance, 'user').mockImplementation(
+ () =>
+ ({
+ id: () => 'test-user-id-1'
+ } as User)
+ )
+
+ await instance.load(Context.system(), analyticsInstance)
+
+ expect(getBucketCallLog()).toStrictEqual([
+ { method: 'init', args: ['testTrackingKey', {}] },
+ { method: 'user', args: ['test-user-id-1', {}, { active: false }] }
+ ])
+ })
+ })
+})
diff --git a/packages/browser-destinations/destinations/bucket/src/generated-types.ts b/packages/browser-destinations/destinations/bucket/src/generated-types.ts
new file mode 100644
index 0000000000..ed3d76cd3b
--- /dev/null
+++ b/packages/browser-destinations/destinations/bucket/src/generated-types.ts
@@ -0,0 +1,8 @@
+// Generated file. DO NOT MODIFY IT BY HAND.
+
+export interface Settings {
+ /**
+ * Your Bucket App tracking key, found on the tracking page.
+ */
+ trackingKey: string
+}
diff --git a/packages/browser-destinations/destinations/bucket/src/group/__tests__/index.test.ts b/packages/browser-destinations/destinations/bucket/src/group/__tests__/index.test.ts
new file mode 100644
index 0000000000..fbba8d8467
--- /dev/null
+++ b/packages/browser-destinations/destinations/bucket/src/group/__tests__/index.test.ts
@@ -0,0 +1,186 @@
+import { Analytics, Context, User } from '@segment/analytics-next'
+import bucketWebDestination, { destination } from '../../index'
+import { Subscription } from '@segment/browser-destination-runtime/types'
+import { JSONArray } from '@segment/actions-core/*'
+import { bucketTestHooks, getBucketCallLog } from '../../test-utils'
+
+const subscriptions: Subscription[] = [
+ {
+ partnerAction: 'group',
+ name: 'Identify Company',
+ enabled: true,
+ subscribe: 'type = "group"',
+ mapping: {
+ groupId: {
+ '@path': '$.groupId'
+ },
+ userId: {
+ '@path': '$.userId'
+ },
+ traits: {
+ '@path': '$.traits'
+ }
+ }
+ }
+]
+
+describe('Bucket.company', () => {
+ bucketTestHooks()
+
+ describe('when logged in', () => {
+ describe('from analytics.js previous session', () => {
+ it('maps parameters correctly to Bucket', async () => {
+ const [bucketPlugin] = await bucketWebDestination({
+ trackingKey: 'testTrackingKey',
+ subscriptions: subscriptions as unknown as JSONArray
+ })
+
+ const analyticsInstance = new Analytics({ writeKey: 'test-writekey' })
+ jest.spyOn(analyticsInstance, 'user').mockImplementation(
+ () =>
+ ({
+ id: () => 'user-id-1'
+ } as User)
+ )
+ await bucketPlugin.load(Context.system(), analyticsInstance)
+
+ jest.spyOn(destination.actions.group, 'perform')
+
+ await bucketPlugin.group?.(
+ new Context({
+ type: 'group',
+ userId: 'user-id-1',
+ groupId: 'group-id-1',
+ traits: {
+ name: 'ACME INC'
+ }
+ })
+ )
+
+ expect(destination.actions.group.perform).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({
+ payload: {
+ userId: 'user-id-1',
+ groupId: 'group-id-1',
+ traits: {
+ name: 'ACME INC'
+ }
+ }
+ })
+ )
+
+ expect(getBucketCallLog()).toStrictEqual([
+ { method: 'init', args: ['testTrackingKey', {}] },
+ {
+ method: 'user',
+ args: ['user-id-1', {}, { active: false }]
+ },
+ {
+ method: 'company',
+ args: [
+ 'group-id-1',
+ {
+ name: 'ACME INC'
+ },
+ 'user-id-1'
+ ]
+ }
+ ])
+ })
+ })
+
+ describe('from am identify call', () => {
+ it('maps parameters correctly to Bucket', async () => {
+ const [bucketPlugin] = await bucketWebDestination({
+ trackingKey: 'testTrackingKey',
+ subscriptions: subscriptions as unknown as JSONArray
+ })
+
+ await bucketPlugin.load(Context.system(), new Analytics({ writeKey: 'test-writekey' }))
+
+ jest.spyOn(destination.actions.group, 'perform')
+
+ // Bucket rejects group calls without previous identify calls
+ await window.bucket.user('user-id-1')
+
+ await bucketPlugin.group?.(
+ new Context({
+ type: 'group',
+ userId: 'user-id-1',
+ groupId: 'group-id-1',
+ traits: {
+ name: 'ACME INC'
+ }
+ })
+ )
+
+ expect(destination.actions.group.perform).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({
+ payload: {
+ userId: 'user-id-1',
+ groupId: 'group-id-1',
+ traits: {
+ name: 'ACME INC'
+ }
+ }
+ })
+ )
+
+ expect(getBucketCallLog()).toStrictEqual([
+ { method: 'init', args: ['testTrackingKey', {}] },
+ {
+ method: 'user',
+ args: ['user-id-1']
+ },
+ {
+ method: 'company',
+ args: [
+ 'group-id-1',
+ {
+ name: 'ACME INC'
+ },
+ 'user-id-1'
+ ]
+ }
+ ])
+ })
+ })
+ })
+
+ describe('when not logged in', () => {
+ it('should not call Bucket.group', async () => {
+ const [bucketPlugin] = await bucketWebDestination({
+ trackingKey: 'testTrackingKey',
+ subscriptions: subscriptions as unknown as JSONArray
+ })
+
+ const analyticsInstance = new Analytics({ writeKey: 'test-writekey' })
+ await bucketPlugin.load(Context.system(), analyticsInstance)
+
+ jest.spyOn(destination.actions.group, 'perform')
+
+ // Manually mimicking a group call without a userId.
+ // The analytics client will probably never do this if
+ // userId doesn't exist, since the subscription marks it as required
+ await bucketPlugin.group?.(
+ new Context({
+ type: 'group',
+ anonymousId: 'anonymous-id-1',
+ groupId: 'group-id-1',
+ traits: {
+ name: 'ACME INC'
+ }
+ })
+ )
+
+ // TODO: Ideally we should be able to assert that the destination action was never
+ // called, but couldn't figure out how to create an anlytics instance with the plugin
+ // and then trigger the full flow trhough analytics.group() with only an anonymous ID
+ // expect(destination.actions.group.perform).not.toHaveBeenCalled()
+
+ expect(getBucketCallLog()).toStrictEqual([{ method: 'init', args: ['testTrackingKey', {}] }])
+ })
+ })
+})
diff --git a/packages/browser-destinations/destinations/bucket/src/group/generated-types.ts b/packages/browser-destinations/destinations/bucket/src/group/generated-types.ts
new file mode 100644
index 0000000000..ff9f16892c
--- /dev/null
+++ b/packages/browser-destinations/destinations/bucket/src/group/generated-types.ts
@@ -0,0 +1,18 @@
+// Generated file. DO NOT MODIFY IT BY HAND.
+
+export interface Payload {
+ /**
+ * Unique identifier for the company
+ */
+ groupId: string
+ /**
+ * Unique identifier for the user
+ */
+ userId: string
+ /**
+ * Additional information to associate with the Company in Bucket
+ */
+ traits?: {
+ [k: string]: unknown
+ }
+}
diff --git a/packages/browser-destinations/destinations/bucket/src/group/index.ts b/packages/browser-destinations/destinations/bucket/src/group/index.ts
new file mode 100644
index 0000000000..a4162da1e1
--- /dev/null
+++ b/packages/browser-destinations/destinations/bucket/src/group/index.ts
@@ -0,0 +1,49 @@
+import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
+import type { Settings } from '../generated-types'
+import type { Payload } from './generated-types'
+import type { Bucket } from '../types'
+
+const action: BrowserActionDefinition = {
+ title: 'Identify Company',
+ description: 'Creates or updates a Company in Bucket and associates the user with it',
+ platform: 'web',
+ defaultSubscription: 'type = "group"',
+ fields: {
+ groupId: {
+ type: 'string',
+ required: true,
+ description: 'Unique identifier for the company',
+ label: 'Company ID',
+ default: {
+ '@path': '$.groupId'
+ }
+ },
+ userId: {
+ type: 'string',
+ required: true,
+ allowNull: false,
+ description: 'Unique identifier for the user',
+ label: 'User ID',
+ default: {
+ '@path': '$.userId'
+ }
+ },
+ traits: {
+ type: 'object',
+ required: false,
+ description: 'Additional information to associate with the Company in Bucket',
+ label: 'Company Attributes',
+ default: {
+ '@path': '$.traits'
+ }
+ }
+ },
+ perform: (bucket, { payload }) => {
+ // Ensure we never call Bucket.company() without a user ID
+ if (payload.userId) {
+ void bucket.company(payload.groupId, payload.traits, payload.userId)
+ }
+ }
+}
+
+export default action
diff --git a/packages/browser-destinations/destinations/bucket/src/identifyUser/__tests__/index.test.ts b/packages/browser-destinations/destinations/bucket/src/identifyUser/__tests__/index.test.ts
new file mode 100644
index 0000000000..51a7e58239
--- /dev/null
+++ b/packages/browser-destinations/destinations/bucket/src/identifyUser/__tests__/index.test.ts
@@ -0,0 +1,78 @@
+import { Analytics, Context } from '@segment/analytics-next'
+import bucketWebDestination, { destination } from '../../index'
+import { Subscription } from '@segment/browser-destination-runtime/types'
+import { JSONArray } from '@segment/actions-core/*'
+import { bucketTestHooks, getBucketCallLog } from '../../test-utils'
+
+const subscriptions: Subscription[] = [
+ {
+ partnerAction: 'identifyUser',
+ name: 'Identify User',
+ enabled: true,
+ subscribe: 'type = "identify"',
+ mapping: {
+ userId: {
+ '@path': '$.userId'
+ },
+ traits: {
+ '@path': '$.traits'
+ }
+ }
+ }
+]
+
+describe('Bucket.user', () => {
+ bucketTestHooks()
+
+ test('it maps event parameters correctly to bucket.user', async () => {
+ const [identifyEvent] = await bucketWebDestination({
+ trackingKey: 'testTrackingKey',
+ subscriptions: subscriptions as unknown as JSONArray
+ })
+
+ await identifyEvent.load(Context.system(), new Analytics({ writeKey: 'test-writekey' }))
+
+ jest.spyOn(destination.actions.identifyUser, 'perform')
+
+ await identifyEvent.identify?.(
+ new Context({
+ type: 'identify',
+ userId: 'user-id-1',
+ traits: {
+ name: 'John Doe',
+ email: 'test-email-2@gmail.com',
+ age: 42
+ }
+ })
+ )
+
+ expect(destination.actions.identifyUser.perform).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({
+ payload: {
+ userId: 'user-id-1',
+ traits: {
+ name: 'John Doe',
+ email: 'test-email-2@gmail.com',
+ age: 42
+ }
+ }
+ })
+ )
+
+ expect(getBucketCallLog()).toStrictEqual([
+ { method: 'init', args: ['testTrackingKey', {}] },
+ {
+ method: 'user',
+ args: [
+ 'user-id-1',
+ {
+ name: 'John Doe',
+ email: 'test-email-2@gmail.com',
+ age: 42
+ }
+ ]
+ }
+ ])
+ })
+})
diff --git a/packages/browser-destinations/destinations/bucket/src/identifyUser/generated-types.ts b/packages/browser-destinations/destinations/bucket/src/identifyUser/generated-types.ts
new file mode 100644
index 0000000000..2703e2cb3c
--- /dev/null
+++ b/packages/browser-destinations/destinations/bucket/src/identifyUser/generated-types.ts
@@ -0,0 +1,14 @@
+// Generated file. DO NOT MODIFY IT BY HAND.
+
+export interface Payload {
+ /**
+ * Unique identifier for the User
+ */
+ userId: string
+ /**
+ * Additional information to associate with the User in Bucket
+ */
+ traits?: {
+ [k: string]: unknown
+ }
+}
diff --git a/packages/browser-destinations/destinations/bucket/src/identifyUser/index.ts b/packages/browser-destinations/destinations/bucket/src/identifyUser/index.ts
new file mode 100644
index 0000000000..4472b52cc9
--- /dev/null
+++ b/packages/browser-destinations/destinations/bucket/src/identifyUser/index.ts
@@ -0,0 +1,36 @@
+import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
+import type { Settings } from '../generated-types'
+import type { Payload } from './generated-types'
+import type { Bucket } from '../types'
+
+const action: BrowserActionDefinition = {
+ title: 'Identify User',
+ description: 'Creates or updates a user profile in Bucket. Also initializes Live Satisfaction',
+ platform: 'web',
+ defaultSubscription: 'type = "identify"',
+ fields: {
+ userId: {
+ type: 'string',
+ required: true,
+ description: 'Unique identifier for the User',
+ label: 'User ID',
+ default: {
+ '@path': '$.userId'
+ }
+ },
+ traits: {
+ type: 'object',
+ required: false,
+ description: 'Additional information to associate with the User in Bucket',
+ label: 'User Attributes',
+ default: {
+ '@path': '$.traits'
+ }
+ }
+ },
+ perform: (bucket, { payload }) => {
+ void bucket.user(payload.userId, payload.traits)
+ }
+}
+
+export default action
diff --git a/packages/browser-destinations/destinations/bucket/src/index.ts b/packages/browser-destinations/destinations/bucket/src/index.ts
new file mode 100644
index 0000000000..067679a67c
--- /dev/null
+++ b/packages/browser-destinations/destinations/bucket/src/index.ts
@@ -0,0 +1,96 @@
+import type { Settings } from './generated-types'
+import type { BrowserDestinationDefinition } from '@segment/browser-destination-runtime/types'
+import { browserDestination } from '@segment/browser-destination-runtime/shim'
+import { Bucket } from './types'
+import identifyUser from './identifyUser'
+import trackEvent from './trackEvent'
+import { defaultValues } from '@segment/actions-core'
+import group from './group'
+
+declare global {
+ interface Window {
+ bucket: Bucket
+ }
+}
+
+export const destination: BrowserDestinationDefinition = {
+ name: 'Bucket Web (Actions)',
+ description:
+ 'Loads the Bucket browser SDK, maps identify(), group() and track() events and enables LiveSatisfaction connections',
+ slug: 'bucket-web',
+ mode: 'device',
+
+ presets: [
+ {
+ name: 'Identify User',
+ subscribe: 'type = "identify"',
+ partnerAction: 'identifyUser',
+ mapping: defaultValues(identifyUser.fields),
+ type: 'automatic'
+ },
+ {
+ name: 'Group',
+ subscribe: 'type = "group"',
+ partnerAction: 'group',
+ mapping: defaultValues(group.fields),
+ type: 'automatic'
+ },
+ {
+ name: 'Track Event',
+ subscribe: 'type = "track"',
+ partnerAction: 'trackEvent',
+ mapping: defaultValues(trackEvent.fields),
+ type: 'automatic'
+ }
+ ],
+
+ settings: {
+ trackingKey: {
+ description: 'Your Bucket App tracking key, found on the tracking page.',
+ label: 'Tracking Key',
+ type: 'string',
+ required: true
+ }
+ },
+
+ actions: {
+ identifyUser,
+ group,
+ trackEvent
+ },
+
+ initialize: async ({ settings, analytics }, deps) => {
+ const {
+ // @ts-expect-error versionSettings is not part of the settings object but they are injected by Analytics 2.0, making Braze SDK raise a warning when we initialize it.
+ versionSettings,
+ // @ts-expect-error same as above.
+ subscriptions,
+
+ trackingKey,
+ // @ts-expect-error Code-only SDK version override. Can be set via analytics.load() integrations overrides
+ sdkVersion = '2',
+ ...options
+ } = settings
+ await deps.loadScript(`https://cdn.jsdelivr.net/npm/@bucketco/tracking-sdk@${sdkVersion}`)
+ await deps.resolveWhen(() => window.bucket != undefined, 100)
+
+ window.bucket.init(settings.trackingKey, options)
+
+ // If the analytics client already has a logged in user from a
+ // previous session or page, consider the user logged in.
+ // In this case we need to call `bucket.user()` to set the persisted
+ // user id in bucket and initialize Live Satisfaction
+ const segmentPersistedUserId = analytics.user().id()
+ if (segmentPersistedUserId) {
+ void window.bucket.user(segmentPersistedUserId, {}, { active: false })
+ }
+
+ analytics.on('reset', () => {
+ window.bucket.reset()
+ })
+
+ return window.bucket
+ }
+}
+
+export default browserDestination(destination)
diff --git a/packages/browser-destinations/destinations/bucket/src/test-utils.ts b/packages/browser-destinations/destinations/bucket/src/test-utils.ts
new file mode 100644
index 0000000000..baabbb6e6d
--- /dev/null
+++ b/packages/browser-destinations/destinations/bucket/src/test-utils.ts
@@ -0,0 +1,63 @@
+import nock from 'nock'
+import { Bucket } from 'src/types'
+
+const bucketTestMock = `
+(() => {
+ const noop = () => {};
+
+ const bucketTestInterface = {
+ init: noop,
+ user: noop,
+ company: noop,
+ track: noop,
+ reset: noop
+ };
+
+ const callLog = [];
+
+ window.bucket = new Proxy(bucketTestInterface, {
+ get(bucket, property) {
+ if (typeof bucket[property] === 'function') {
+ return (...args) => {
+ callLog.push({ method: property, args })
+ return bucket[property](...args)
+ }
+ }
+
+ if (property === 'callLog') {
+ return callLog;
+ }
+ }
+ });
+})();
+`
+
+export function bucketTestHooks() {
+ beforeAll(() => {
+ nock.disableNetConnect()
+ })
+
+ beforeEach(() => {
+ nock('https://cdn.jsdelivr.net')
+ .get((uri) => uri.startsWith('/npm/@bucketco/tracking-sdk@'))
+ .reply(200, bucketTestMock)
+ })
+
+ afterEach(function () {
+ if (!nock.isDone()) {
+ // @ts-expect-error no-unsafe-call
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
+ this.test.error(new Error('Not all nock interceptors were used!'))
+ }
+
+ nock.cleanAll()
+ })
+
+ afterAll(() => {
+ nock.enableNetConnect()
+ })
+}
+
+export function getBucketCallLog() {
+ return (window.bucket as unknown as { callLog: Array<{ method: keyof Bucket; args: Array }> }).callLog
+}
diff --git a/packages/browser-destinations/destinations/bucket/src/trackEvent/__tests__/index.test.ts b/packages/browser-destinations/destinations/bucket/src/trackEvent/__tests__/index.test.ts
new file mode 100644
index 0000000000..5c94ef5aab
--- /dev/null
+++ b/packages/browser-destinations/destinations/bucket/src/trackEvent/__tests__/index.test.ts
@@ -0,0 +1,159 @@
+import { Analytics, Context, User } from '@segment/analytics-next'
+import bucketWebDestination, { destination } from '../../index'
+import { Subscription } from '@segment/browser-destination-runtime/types'
+import { JSONArray } from '@segment/actions-core/*'
+import { bucketTestHooks, getBucketCallLog } from '../../test-utils'
+
+const subscriptions: Subscription[] = [
+ {
+ partnerAction: 'trackEvent',
+ name: 'Track Event',
+ enabled: true,
+ subscribe: 'type = "track"',
+ mapping: {
+ name: {
+ '@path': '$.name'
+ },
+ userId: {
+ '@path': '$.userId'
+ },
+ properties: {
+ '@path': '$.properties'
+ }
+ }
+ }
+]
+
+describe('trackEvent', () => {
+ bucketTestHooks()
+
+ describe('when logged in', () => {
+ describe('from analytics.js previous session', () => {
+ it('maps parameters correctly to Bucket', async () => {
+ const [bucketPlugin] = await bucketWebDestination({
+ trackingKey: 'testTrackingKey',
+ subscriptions: subscriptions as unknown as JSONArray
+ })
+
+ const analyticsInstance = new Analytics({ writeKey: 'test-writekey' })
+ jest.spyOn(analyticsInstance, 'user').mockImplementation(
+ () =>
+ ({
+ id: () => 'user-id-1'
+ } as User)
+ )
+ await bucketPlugin.load(Context.system(), analyticsInstance)
+
+ jest.spyOn(destination.actions.trackEvent, 'perform')
+
+ const properties = { property1: 'value1', property2: false }
+ await bucketPlugin.track?.(
+ new Context({
+ type: 'track',
+ name: 'Button Clicked',
+ userId: 'user-id-1',
+ properties
+ })
+ )
+
+ expect(destination.actions.trackEvent.perform).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({
+ payload: { name: 'Button Clicked', userId: 'user-id-1', properties }
+ })
+ )
+
+ expect(getBucketCallLog()).toStrictEqual([
+ { method: 'init', args: ['testTrackingKey', {}] },
+ {
+ method: 'user',
+ args: ['user-id-1', {}, { active: false }]
+ },
+ {
+ method: 'track',
+ args: ['Button Clicked', properties, 'user-id-1']
+ }
+ ])
+ })
+ })
+
+ describe('from am identify call', () => {
+ it('maps parameters correctly to Bucket', async () => {
+ const [bucketPlugin] = await bucketWebDestination({
+ trackingKey: 'testTrackingKey',
+ subscriptions: subscriptions as unknown as JSONArray
+ })
+
+ await bucketPlugin.load(Context.system(), new Analytics({ writeKey: 'test-writekey' }))
+
+ jest.spyOn(destination.actions.trackEvent, 'perform')
+
+ // Bucket rejects group calls without previous identify calls
+ await window.bucket.user('user-id-1')
+
+ const properties = { property1: 'value1', property2: false }
+ await bucketPlugin.track?.(
+ new Context({
+ type: 'track',
+ name: 'Button Clicked',
+ userId: 'user-id-1',
+ properties
+ })
+ )
+
+ expect(destination.actions.trackEvent.perform).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({
+ payload: { name: 'Button Clicked', userId: 'user-id-1', properties }
+ })
+ )
+
+ expect(getBucketCallLog()).toStrictEqual([
+ { method: 'init', args: ['testTrackingKey', {}] },
+ {
+ method: 'user',
+ args: ['user-id-1']
+ },
+ {
+ method: 'track',
+ args: ['Button Clicked', properties, 'user-id-1']
+ }
+ ])
+ })
+ })
+ })
+
+ describe('when not logged in', () => {
+ it('should not call Bucket.group', async () => {
+ const [bucketPlugin] = await bucketWebDestination({
+ trackingKey: 'testTrackingKey',
+ subscriptions: subscriptions as unknown as JSONArray
+ })
+
+ const analyticsInstance = new Analytics({ writeKey: 'test-writekey' })
+ await bucketPlugin.load(Context.system(), analyticsInstance)
+
+ jest.spyOn(destination.actions.trackEvent, 'perform')
+
+ // Manually mimicking a track call without a userId.
+ // The analytics client will probably never do this if
+ // userId doesn't exist, since the subscription marks it as required
+ const properties = { property1: 'value1', property2: false }
+ await bucketPlugin.track?.(
+ new Context({
+ type: 'track',
+ name: 'Button Clicked',
+ anonymousId: 'user-id-1',
+ properties
+ })
+ )
+
+ // TODO: Ideally we should be able to assert that the destination action was never
+ // called, but couldn't figure out how to create an anlytics instance with the plugin
+ // and then trigger the full flow trhough analytics.track() with only an anonymous ID
+ // expect(destination.actions.trackEvent.perform).not.toHaveBeenCalled()
+
+ expect(getBucketCallLog()).toStrictEqual([{ method: 'init', args: ['testTrackingKey', {}] }])
+ })
+ })
+})
diff --git a/packages/browser-destinations/destinations/bucket/src/trackEvent/generated-types.ts b/packages/browser-destinations/destinations/bucket/src/trackEvent/generated-types.ts
new file mode 100644
index 0000000000..2d7e5e4aed
--- /dev/null
+++ b/packages/browser-destinations/destinations/bucket/src/trackEvent/generated-types.ts
@@ -0,0 +1,18 @@
+// Generated file. DO NOT MODIFY IT BY HAND.
+
+export interface Payload {
+ /**
+ * The event name
+ */
+ name: string
+ /**
+ * Unique identifier for the user
+ */
+ userId: string
+ /**
+ * Object containing the properties of the event
+ */
+ properties?: {
+ [k: string]: unknown
+ }
+}
diff --git a/packages/browser-destinations/destinations/bucket/src/trackEvent/index.ts b/packages/browser-destinations/destinations/bucket/src/trackEvent/index.ts
new file mode 100644
index 0000000000..29bf9a30fb
--- /dev/null
+++ b/packages/browser-destinations/destinations/bucket/src/trackEvent/index.ts
@@ -0,0 +1,49 @@
+import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
+import type { Settings } from '../generated-types'
+import type { Payload } from './generated-types'
+import type { Bucket } from '../types'
+
+const action: BrowserActionDefinition = {
+ title: 'Track Event',
+ description: 'Map a Segment track() event to Bucket',
+ platform: 'web',
+ defaultSubscription: 'type = "track"',
+ fields: {
+ name: {
+ description: 'The event name',
+ label: 'Event name',
+ required: true,
+ type: 'string',
+ default: {
+ '@path': '$.event'
+ }
+ },
+ userId: {
+ type: 'string',
+ required: true,
+ allowNull: false,
+ description: 'Unique identifier for the user',
+ label: 'User ID',
+ default: {
+ '@path': '$.userId'
+ }
+ },
+ properties: {
+ type: 'object',
+ required: false,
+ description: 'Object containing the properties of the event',
+ label: 'Event Properties',
+ default: {
+ '@path': '$.properties'
+ }
+ }
+ },
+ perform: (bucket, { payload }) => {
+ // Ensure we never call Bucket.track() without a user ID
+ if (payload.userId) {
+ void bucket.track(payload.name, payload.properties, payload.userId)
+ }
+ }
+}
+
+export default action
diff --git a/packages/browser-destinations/destinations/bucket/src/types.ts b/packages/browser-destinations/destinations/bucket/src/types.ts
new file mode 100644
index 0000000000..4307622bca
--- /dev/null
+++ b/packages/browser-destinations/destinations/bucket/src/types.ts
@@ -0,0 +1,3 @@
+import type bucket from '@bucketco/tracking-sdk'
+
+export type Bucket = typeof bucket
diff --git a/packages/browser-destinations/destinations/bucket/tsconfig.json b/packages/browser-destinations/destinations/bucket/tsconfig.json
new file mode 100644
index 0000000000..c2a7897afd
--- /dev/null
+++ b/packages/browser-destinations/destinations/bucket/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.build.json",
+ "compilerOptions": {
+ "rootDir": "./src",
+ "baseUrl": "."
+ },
+ "include": ["src"],
+ "exclude": ["dist", "**/__tests__"]
+}
diff --git a/packages/browser-destinations/destinations/cdpresolution/README.md b/packages/browser-destinations/destinations/cdpresolution/README.md
index fa3b1c68eb..7450e7d182 100644
--- a/packages/browser-destinations/destinations/cdpresolution/README.md
+++ b/packages/browser-destinations/destinations/cdpresolution/README.md
@@ -6,7 +6,7 @@ The Cdpresolution browser action destination for use with @segment/analytics-nex
MIT License
-Copyright (c) 2023 Segment
+Copyright (c) 2024 Segment
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/packages/browser-destinations/destinations/cdpresolution/package.json b/packages/browser-destinations/destinations/cdpresolution/package.json
index a47bef769e..7af5dd75c1 100644
--- a/packages/browser-destinations/destinations/cdpresolution/package.json
+++ b/packages/browser-destinations/destinations/cdpresolution/package.json
@@ -1,6 +1,6 @@
{
"name": "@segment/analytics-browser-actions-cdpresolution",
- "version": "1.2.0",
+ "version": "1.21.0",
"license": "MIT",
"publishConfig": {
"access": "public",
@@ -15,7 +15,7 @@
},
"typings": "./dist/esm",
"dependencies": {
- "@segment/browser-destination-runtime": "^1.14.0"
+ "@segment/browser-destination-runtime": "^1.33.0"
},
"peerDependencies": {
"@segment/analytics-next": ">=1.55.0"
diff --git a/packages/browser-destinations/destinations/cdpresolution/src/index.ts b/packages/browser-destinations/destinations/cdpresolution/src/index.ts
index 6ae95119c4..c5cf2356f4 100644
--- a/packages/browser-destinations/destinations/cdpresolution/src/index.ts
+++ b/packages/browser-destinations/destinations/cdpresolution/src/index.ts
@@ -50,9 +50,9 @@ export const destination: BrowserDestinationDefinition
}
},
- initialize: async () => {
+ initialize: async (_, deps) => {
window.cdpResolution = {
- sync: (endpoint: string, clientIdentifier: string, anonymousId: string): void => {
+ sync: async (endpoint: string, clientIdentifier: string, anonymousId: string): Promise => {
let cdpcookieset = ''
const name = 'cdpresolutionset' + '='
const ca = document.cookie.split(';')
@@ -82,7 +82,7 @@ export const destination: BrowserDestinationDefinition
if (cdpcookieset == '') {
document.cookie = 'cdpresolutionset=true'
- void fetch(endpointUrl, { mode: 'no-cors' })
+ await deps.loadScript(endpointUrl)
return
}
}
diff --git a/packages/browser-destinations/destinations/commandbar/package.json b/packages/browser-destinations/destinations/commandbar/package.json
index 45394db32c..cbfc8039f9 100644
--- a/packages/browser-destinations/destinations/commandbar/package.json
+++ b/packages/browser-destinations/destinations/commandbar/package.json
@@ -1,6 +1,6 @@
{
"name": "@segment/analytics-browser-actions-commandbar",
- "version": "1.15.0",
+ "version": "1.34.0",
"license": "MIT",
"publishConfig": {
"access": "public",
@@ -15,8 +15,8 @@
},
"typings": "./dist/esm",
"dependencies": {
- "@segment/actions-core": "^3.83.0",
- "@segment/browser-destination-runtime": "^1.14.0"
+ "@segment/actions-core": "^3.103.0",
+ "@segment/browser-destination-runtime": "^1.33.0"
},
"peerDependencies": {
"@segment/analytics-next": ">=1.55.0"
diff --git a/packages/browser-destinations/destinations/devrev/README.md b/packages/browser-destinations/destinations/devrev/README.md
index ac6991638b..26a134bd28 100644
--- a/packages/browser-destinations/destinations/devrev/README.md
+++ b/packages/browser-destinations/destinations/devrev/README.md
@@ -6,7 +6,7 @@ The Devrev browser action destination for use with @segment/analytics-next.
MIT License
-Copyright (c) 2023 Segment
+Copyright (c) 2024 Segment
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/packages/browser-destinations/destinations/devrev/package.json b/packages/browser-destinations/destinations/devrev/package.json
index d834e446b1..ab11cdbcec 100644
--- a/packages/browser-destinations/destinations/devrev/package.json
+++ b/packages/browser-destinations/destinations/devrev/package.json
@@ -1,6 +1,6 @@
{
"name": "@segment/analytics-browser-actions-devrev",
- "version": "1.2.0",
+ "version": "1.21.0",
"license": "MIT",
"publishConfig": {
"access": "public",
@@ -15,7 +15,7 @@
},
"typings": "./dist/esm",
"dependencies": {
- "@segment/browser-destination-runtime": "^1.14.0"
+ "@segment/browser-destination-runtime": "^1.33.0"
},
"peerDependencies": {
"@segment/analytics-next": ">=1.55.0"
diff --git a/packages/browser-destinations/destinations/friendbuy/package.json b/packages/browser-destinations/destinations/friendbuy/package.json
index 94f6853bb6..37f45db0fe 100644
--- a/packages/browser-destinations/destinations/friendbuy/package.json
+++ b/packages/browser-destinations/destinations/friendbuy/package.json
@@ -1,6 +1,6 @@
{
"name": "@segment/analytics-browser-actions-friendbuy",
- "version": "1.15.0",
+ "version": "1.34.0",
"license": "MIT",
"publishConfig": {
"access": "public",
@@ -15,9 +15,9 @@
},
"typings": "./dist/esm",
"dependencies": {
- "@segment/actions-core": "^3.83.0",
- "@segment/actions-shared": "^1.65.0",
- "@segment/browser-destination-runtime": "^1.14.0"
+ "@segment/actions-core": "^3.103.0",
+ "@segment/actions-shared": "^1.84.0",
+ "@segment/browser-destination-runtime": "^1.33.0"
},
"peerDependencies": {
"@segment/analytics-next": ">=1.55.0"
diff --git a/packages/browser-destinations/destinations/fullstory/package.json b/packages/browser-destinations/destinations/fullstory/package.json
index 53a147afa9..1e1ee52bec 100644
--- a/packages/browser-destinations/destinations/fullstory/package.json
+++ b/packages/browser-destinations/destinations/fullstory/package.json
@@ -1,6 +1,6 @@
{
"name": "@segment/analytics-browser-actions-fullstory",
- "version": "1.16.0",
+ "version": "1.36.0",
"license": "MIT",
"publishConfig": {
"access": "public",
@@ -15,9 +15,9 @@
},
"typings": "./dist/esm",
"dependencies": {
- "@fullstory/browser": "^1.4.9",
- "@segment/actions-core": "^3.83.0",
- "@segment/browser-destination-runtime": "^1.14.0"
+ "@fullstory/browser": "^2.0.3",
+ "@segment/actions-core": "^3.103.0",
+ "@segment/browser-destination-runtime": "^1.33.0"
},
"peerDependencies": {
"@segment/analytics-next": ">=1.55.0"
diff --git a/packages/browser-destinations/destinations/fullstory/src/__tests__/fullstory.test.ts b/packages/browser-destinations/destinations/fullstory/src/__tests__/fullstory.test.ts
index c3cf7dff89..861614e765 100644
--- a/packages/browser-destinations/destinations/fullstory/src/__tests__/fullstory.test.ts
+++ b/packages/browser-destinations/destinations/fullstory/src/__tests__/fullstory.test.ts
@@ -1,6 +1,12 @@
import { Analytics, Context } from '@segment/analytics-next'
-import fullstory, { destination } from '..'
+import fullstory from '..'
+import trackEvent from '../trackEvent'
+import identifyUser from '../identifyUser'
+import viewedPage from '../viewedPage'
import { Subscription } from '@segment/browser-destination-runtime/types'
+import { defaultValues } from '@segment/actions-core/*'
+
+const FakeOrgId = 'asdf-qwer'
const example: Subscription[] = [
{
@@ -8,82 +14,36 @@ const example: Subscription[] = [
name: 'Track Event',
enabled: true,
subscribe: 'type = "track"',
- mapping: {
- name: {
- '@path': '$.name'
- },
- properties: {
- '@path': '$.properties'
- }
- }
+ mapping: defaultValues(trackEvent.fields)
},
{
partnerAction: 'identifyUser',
name: 'Identify User',
enabled: true,
subscribe: 'type = "identify"',
- mapping: {
- anonymousId: {
- '@path': '$.anonymousId'
- },
- userId: {
- '@path': '$.userId'
- },
- email: {
- '@path': '$.traits.email'
- },
- traits: {
- '@path': '$.traits'
- },
- displayName: {
- '@path': '$.traits.name'
- }
- }
+ mapping: defaultValues(identifyUser.fields)
+ },
+ {
+ partnerAction: 'viewedPage',
+ name: 'Viewed Page',
+ enabled: true,
+ subscribe: 'type = "page"',
+ mapping: defaultValues(viewedPage.fields)
}
]
-test('can load fullstory', async () => {
- const [event] = await fullstory({
- orgId: 'thefullstory.com',
- subscriptions: example
- })
-
- jest.spyOn(destination.actions.trackEvent, 'perform')
- jest.spyOn(destination, 'initialize')
-
- await event.load(Context.system(), {} as Analytics)
- expect(destination.initialize).toHaveBeenCalled()
-
- const ctx = await event.track?.(
- new Context({
- type: 'track',
- properties: {
- banana: '📞'
- }
- })
- )
-
- expect(destination.actions.trackEvent.perform).toHaveBeenCalled()
- expect(ctx).not.toBeUndefined()
-
- const scripts = window.document.querySelectorAll('script')
- expect(scripts).toMatchInlineSnapshot(`
- NodeList [
- ,
- ,
- ]
- `)
+beforeEach(() => {
+ delete window._fs_initialized
+ if (window._fs_namespace) {
+ delete window[window._fs_namespace]
+ delete window._fs_namespace
+ }
})
describe('#track', () => {
it('sends record events to fullstory on "event"', async () => {
const [event] = await fullstory({
- orgId: 'thefullstory.com',
+ orgId: FakeOrgId,
subscriptions: example
})
@@ -93,7 +53,7 @@ describe('#track', () => {
await event.track?.(
new Context({
type: 'track',
- name: 'hello!',
+ event: 'hello!',
properties: {
banana: '📞'
}
@@ -112,16 +72,16 @@ describe('#track', () => {
describe('#identify', () => {
it('should default to anonymousId', async () => {
- const [_, identifyUser] = await fullstory({
- orgId: 'thefullstory.com',
+ const [_, identify] = await fullstory({
+ orgId: FakeOrgId,
subscriptions: example
})
- await identifyUser.load(Context.system(), {} as Analytics)
+ await identify.load(Context.system(), {} as Analytics)
const fs = jest.spyOn(window.FS, 'setUserVars')
const fsId = jest.spyOn(window.FS, 'identify')
- await identifyUser.identify?.(
+ await identify.identify?.(
new Context({
type: 'identify',
anonymousId: 'anon',
@@ -137,7 +97,7 @@ describe('#identify', () => {
}),
it('should send an id', async () => {
const [_, identifyUser] = await fullstory({
- orgId: 'thefullstory.com',
+ orgId: FakeOrgId,
subscriptions: example
})
await identifyUser.load(Context.system(), {} as Analytics)
@@ -147,14 +107,14 @@ describe('#identify', () => {
expect(fsId).toHaveBeenCalledWith('id', {}, 'segment-browser-actions')
}),
it('should camelCase custom traits', async () => {
- const [_, identifyUser] = await fullstory({
- orgId: 'thefullstory.com',
+ const [_, identify] = await fullstory({
+ orgId: FakeOrgId,
subscriptions: example
})
- await identifyUser.load(Context.system(), {} as Analytics)
+ await identify.load(Context.system(), {} as Analytics)
const fsId = jest.spyOn(window.FS, 'identify')
- await identifyUser.identify?.(
+ await identify.identify?.(
new Context({
type: 'identify',
userId: 'id',
@@ -173,15 +133,15 @@ describe('#identify', () => {
})
it('can set user vars', async () => {
- const [_, identifyUser] = await fullstory({
- orgId: 'thefullstory.com',
+ const [_, identify] = await fullstory({
+ orgId: FakeOrgId,
subscriptions: example
})
- await identifyUser.load(Context.system(), {} as Analytics)
+ await identify.load(Context.system(), {} as Analytics)
const fs = jest.spyOn(window.FS, 'setUserVars')
- await identifyUser.identify?.(
+ await identify.identify?.(
new Context({
type: 'identify',
traits: {
@@ -204,15 +164,15 @@ describe('#identify', () => {
})
it('should set displayName correctly', async () => {
- const [_, identifyUser] = await fullstory({
- orgId: 'thefullstory.com',
+ const [_, identify] = await fullstory({
+ orgId: FakeOrgId,
subscriptions: example
})
- await identifyUser.load(Context.system(), {} as Analytics)
+ await identify.load(Context.system(), {} as Analytics)
const fs = jest.spyOn(window.FS, 'identify')
- await identifyUser.identify?.(
+ await identify.identify?.(
new Context({
type: 'identify',
userId: 'userId',
@@ -236,3 +196,93 @@ describe('#identify', () => {
)
})
})
+
+describe('#page', () => {
+ it('sends page events to fullstory on "page" (category edition)', async () => {
+ const [, , viewed] = await fullstory({
+ orgId: FakeOrgId,
+ subscriptions: example
+ })
+
+ await viewed.load(Context.system(), {} as Analytics)
+ const fs = jest.spyOn(window.FS, 'setVars')
+
+ await viewed.page?.(
+ new Context({
+ type: 'page',
+ category: 'Walruses',
+ name: 'Walrus Page',
+ properties: {
+ banana: '📞'
+ }
+ })
+ )
+
+ expect(fs).toHaveBeenCalledWith(
+ 'page',
+ {
+ pageName: 'Walruses',
+ banana: '📞'
+ },
+ 'segment-browser-actions'
+ )
+ })
+
+ it('sends page events to fullstory on "page" (name edition)', async () => {
+ const [, , viewed] = await fullstory({
+ orgId: FakeOrgId,
+ subscriptions: example
+ })
+
+ await viewed.load(Context.system(), {} as Analytics)
+ const fs = jest.spyOn(window.FS, 'setVars')
+
+ await viewed.page?.(
+ new Context({
+ type: 'page',
+ name: 'Walrus Page',
+ properties: {
+ banana: '📞'
+ }
+ })
+ )
+
+ expect(fs).toHaveBeenCalledWith(
+ 'page',
+ {
+ pageName: 'Walrus Page',
+ banana: '📞'
+ },
+ 'segment-browser-actions'
+ )
+ })
+
+ it('sends page events to fullstory on "page" (no pageName edition)', async () => {
+ const [, , viewed] = await fullstory({
+ orgId: FakeOrgId,
+ subscriptions: example
+ })
+
+ await viewed.load(Context.system(), {} as Analytics)
+ const fs = jest.spyOn(window.FS, 'setVars')
+
+ await viewed.page?.(
+ new Context({
+ type: 'page',
+ properties: {
+ banana: '📞',
+ keys: '🗝🔑'
+ }
+ })
+ )
+
+ expect(fs).toHaveBeenCalledWith(
+ 'page',
+ {
+ banana: '📞',
+ keys: '🗝🔑'
+ },
+ 'segment-browser-actions'
+ )
+ })
+})
diff --git a/packages/browser-destinations/destinations/fullstory/src/__tests__/fullstoryV2.test.ts b/packages/browser-destinations/destinations/fullstory/src/__tests__/fullstoryV2.test.ts
new file mode 100644
index 0000000000..2399ce96c4
--- /dev/null
+++ b/packages/browser-destinations/destinations/fullstory/src/__tests__/fullstoryV2.test.ts
@@ -0,0 +1,277 @@
+import { Analytics, Context } from '@segment/analytics-next'
+import fullstory from '..'
+import trackEventV2 from '../trackEventV2'
+import identifyUserV2 from '../identifyUserV2'
+import viewedPageV2 from '../viewedPageV2'
+import { FS as FSApi } from '../types'
+import { Subscription } from '@segment/browser-destination-runtime/types'
+import { defaultValues } from '@segment/actions-core/*'
+
+jest.mock('@fullstory/browser', () => ({
+ ...jest.requireActual('@fullstory/browser'),
+ init: () => {
+ window.FS = jest.fn() as unknown as FSApi
+ }
+}))
+
+const FakeOrgId = 'asdf-qwer'
+
+const example: Subscription[] = [
+ {
+ partnerAction: 'trackEventV2',
+ name: 'Track Event',
+ enabled: true,
+ subscribe: 'type = "track"',
+ mapping: defaultValues(trackEventV2.fields)
+ },
+ {
+ partnerAction: 'identifyUserV2',
+ name: 'Identify User',
+ enabled: true,
+ subscribe: 'type = "identify"',
+ mapping: defaultValues(identifyUserV2.fields)
+ },
+ {
+ partnerAction: 'viewedPageV2',
+ name: 'Viewed Page',
+ enabled: true,
+ subscribe: 'type = "page"',
+ mapping: defaultValues(viewedPageV2.fields)
+ }
+]
+
+describe('#track', () => {
+ it('sends record events to fullstory on "event"', async () => {
+ const [event] = await fullstory({
+ orgId: FakeOrgId,
+ subscriptions: example
+ })
+
+ await event.load(Context.system(), {} as Analytics)
+
+ await event.track?.(
+ new Context({
+ type: 'track',
+ event: 'hello!',
+ properties: {
+ banana: '📞'
+ }
+ })
+ )
+
+ expect(window.FS).toHaveBeenCalledWith(
+ 'trackEvent',
+ {
+ name: 'hello!',
+ properties: {
+ banana: '📞'
+ }
+ },
+ 'segment-browser-actions'
+ )
+ })
+})
+
+describe('#identify', () => {
+ it('should default to anonymousId', async () => {
+ const [_, identifyUser] = await fullstory({
+ orgId: FakeOrgId,
+ subscriptions: example
+ })
+
+ await identifyUser.load(Context.system(), {} as Analytics)
+
+ await identifyUser.identify?.(
+ new Context({
+ type: 'identify',
+ anonymousId: 'anon',
+ traits: {
+ testProp: false
+ }
+ })
+ )
+
+ expect(window.FS).toHaveBeenCalledTimes(1)
+ expect(window.FS).toHaveBeenCalledWith(
+ 'setProperties',
+ { type: 'user', properties: { segmentAnonymousId: 'anon', testProp: false } },
+ 'segment-browser-actions'
+ )
+ })
+
+ it('should send an id', async () => {
+ const [_, identifyUser] = await fullstory({
+ orgId: FakeOrgId,
+ subscriptions: example
+ })
+ await identifyUser.load(Context.system(), {} as Analytics)
+
+ await identifyUser.identify?.(new Context({ type: 'identify', userId: 'id' }))
+ expect(window.FS).toHaveBeenCalledWith('setIdentity', { uid: 'id', properties: {} }, 'segment-browser-actions')
+ })
+
+ it('can set user vars', async () => {
+ const [_, identifyUser] = await fullstory({
+ orgId: FakeOrgId,
+ subscriptions: example
+ })
+
+ await identifyUser.load(Context.system(), {} as Analytics)
+
+ await identifyUser.identify?.(
+ new Context({
+ type: 'identify',
+ traits: {
+ name: 'Hasbulla',
+ email: 'thegoat@world',
+ height: '50cm'
+ }
+ })
+ )
+
+ expect(window.FS).toHaveBeenCalledWith(
+ 'setProperties',
+ {
+ type: 'user',
+ properties: {
+ displayName: 'Hasbulla',
+ email: 'thegoat@world',
+ height: '50cm',
+ name: 'Hasbulla'
+ }
+ },
+ 'segment-browser-actions'
+ )
+ })
+
+ it('should set displayName correctly', async () => {
+ const [_, identifyUser] = await fullstory({
+ orgId: FakeOrgId,
+ subscriptions: example
+ })
+
+ await identifyUser.load(Context.system(), {} as Analytics)
+
+ await identifyUser.identify?.(
+ new Context({
+ type: 'identify',
+ userId: 'userId',
+ traits: {
+ name: 'Hasbulla',
+ email: 'thegoat@world',
+ height: '50cm'
+ }
+ })
+ )
+
+ expect(window.FS).toHaveBeenCalledWith(
+ 'setIdentity',
+ {
+ uid: 'userId',
+ properties: {
+ displayName: 'Hasbulla',
+ email: 'thegoat@world',
+ height: '50cm',
+ name: 'Hasbulla'
+ }
+ },
+ 'segment-browser-actions'
+ )
+ })
+})
+
+describe('#page', () => {
+ it('sends page events to fullstory on "page" (category edition)', async () => {
+ const [, , viewed] = await fullstory({
+ orgId: FakeOrgId,
+ subscriptions: example
+ })
+
+ await viewed.load(Context.system(), {} as Analytics)
+
+ await viewed.page?.(
+ new Context({
+ type: 'page',
+ category: 'Walruses',
+ name: 'Walrus Page',
+ properties: {
+ banana: '📞'
+ }
+ })
+ )
+
+ expect(window.FS).toHaveBeenCalledWith(
+ 'setProperties',
+ {
+ type: 'page',
+ properties: {
+ pageName: 'Walruses',
+ banana: '📞'
+ }
+ },
+ 'segment-browser-actions'
+ )
+ })
+
+ it('sends page events to fullstory on "page" (name edition)', async () => {
+ const [, , viewed] = await fullstory({
+ orgId: FakeOrgId,
+ subscriptions: example
+ })
+
+ await viewed.load(Context.system(), {} as Analytics)
+
+ await viewed.page?.(
+ new Context({
+ type: 'page',
+ name: 'Walrus Page',
+ properties: {
+ banana: '📞'
+ }
+ })
+ )
+
+ expect(window.FS).toHaveBeenCalledWith(
+ 'setProperties',
+ {
+ type: 'page',
+ properties: {
+ pageName: 'Walrus Page',
+ banana: '📞'
+ }
+ },
+ 'segment-browser-actions'
+ )
+ })
+
+ it('sends page events to fullstory on "page" (no pageName edition)', async () => {
+ const [, , viewed] = await fullstory({
+ orgId: FakeOrgId,
+ subscriptions: example
+ })
+
+ await viewed.load(Context.system(), {} as Analytics)
+
+ await viewed.page?.(
+ new Context({
+ type: 'page',
+ properties: {
+ banana: '📞',
+ keys: '🗝🔑'
+ }
+ })
+ )
+
+ expect(window.FS).toHaveBeenCalledWith(
+ 'setProperties',
+ {
+ type: 'page',
+ properties: {
+ banana: '📞',
+ keys: '🗝🔑'
+ }
+ },
+ 'segment-browser-actions'
+ )
+ })
+})
diff --git a/packages/browser-destinations/destinations/fullstory/src/__tests__/initialization.test.ts b/packages/browser-destinations/destinations/fullstory/src/__tests__/initialization.test.ts
new file mode 100644
index 0000000000..9f0274bc96
--- /dev/null
+++ b/packages/browser-destinations/destinations/fullstory/src/__tests__/initialization.test.ts
@@ -0,0 +1,81 @@
+import { Analytics, Context } from '@segment/analytics-next'
+import fullstory, { destination } from '..'
+import { Subscription } from '@segment/browser-destination-runtime/types'
+
+const example: Subscription[] = [
+ {
+ partnerAction: 'trackEvent',
+ name: 'Track Event',
+ enabled: true,
+ subscribe: 'type = "track"',
+ mapping: {
+ name: {
+ '@path': '$.name'
+ },
+ properties: {
+ '@path': '$.properties'
+ }
+ }
+ },
+ {
+ partnerAction: 'identifyUser',
+ name: 'Identify User',
+ enabled: true,
+ subscribe: 'type = "identify"',
+ mapping: {
+ anonymousId: {
+ '@path': '$.anonymousId'
+ },
+ userId: {
+ '@path': '$.userId'
+ },
+ email: {
+ '@path': '$.traits.email'
+ },
+ traits: {
+ '@path': '$.traits'
+ },
+ displayName: {
+ '@path': '$.traits.name'
+ }
+ }
+ }
+]
+
+test('can load fullstory', async () => {
+ const [event] = await fullstory({
+ orgId: 'thefullstory.com',
+ subscriptions: example
+ })
+
+ jest.spyOn(destination.actions.trackEvent, 'perform')
+ jest.spyOn(destination, 'initialize')
+
+ await event.load(Context.system(), {} as Analytics)
+ expect(destination.initialize).toHaveBeenCalled()
+
+ const ctx = await event.track?.(
+ new Context({
+ type: 'track',
+ properties: {
+ banana: '📞'
+ }
+ })
+ )
+
+ expect(destination.actions.trackEvent.perform).toHaveBeenCalled()
+ expect(ctx).not.toBeUndefined()
+
+ const scripts = window.document.querySelectorAll('script')
+ expect(scripts).toMatchInlineSnapshot(`
+ NodeList [
+ ,
+ ,
+ ]
+ `)
+})
diff --git a/packages/browser-destinations/destinations/fullstory/src/identifyUserV2/generated-types.ts b/packages/browser-destinations/destinations/fullstory/src/identifyUserV2/generated-types.ts
new file mode 100644
index 0000000000..e325fbc445
--- /dev/null
+++ b/packages/browser-destinations/destinations/fullstory/src/identifyUserV2/generated-types.ts
@@ -0,0 +1,26 @@
+// Generated file. DO NOT MODIFY IT BY HAND.
+
+export interface Payload {
+ /**
+ * The user's id
+ */
+ userId?: string
+ /**
+ * The user's anonymous id
+ */
+ anonymousId?: string
+ /**
+ * The user's display name
+ */
+ displayName?: string
+ /**
+ * The user's email
+ */
+ email?: string
+ /**
+ * The Segment traits to be forwarded to FullStory
+ */
+ traits?: {
+ [k: string]: unknown
+ }
+}
diff --git a/packages/browser-destinations/destinations/fullstory/src/identifyUserV2/index.ts b/packages/browser-destinations/destinations/fullstory/src/identifyUserV2/index.ts
new file mode 100644
index 0000000000..97e7db1e7f
--- /dev/null
+++ b/packages/browser-destinations/destinations/fullstory/src/identifyUserV2/index.ts
@@ -0,0 +1,95 @@
+import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
+import type { Settings } from '../generated-types'
+import type { Payload } from './generated-types'
+import type { FS } from '../types'
+import { segmentEventSource } from '..'
+
+// Change from unknown to the partner SDK types
+const action: BrowserActionDefinition = {
+ title: 'Identify User V2',
+ description: 'Sets user identity properties',
+ platform: 'web',
+ defaultSubscription: 'type = "identify"',
+ fields: {
+ userId: {
+ type: 'string',
+ required: false,
+ description: "The user's id",
+ label: 'User ID',
+ default: {
+ '@path': '$.userId'
+ }
+ },
+ anonymousId: {
+ type: 'string',
+ required: false,
+ description: "The user's anonymous id",
+ label: 'Anonymous ID',
+ default: {
+ '@path': '$.anonymousId'
+ }
+ },
+ displayName: {
+ type: 'string',
+ required: false,
+ description: "The user's display name",
+ label: 'Display Name',
+ default: {
+ '@path': '$.traits.name'
+ }
+ },
+ email: {
+ type: 'string',
+ required: false,
+ description: "The user's email",
+ label: 'Email',
+ default: {
+ '@path': '$.traits.email'
+ }
+ },
+ traits: {
+ type: 'object',
+ required: false,
+ description: 'The Segment traits to be forwarded to FullStory',
+ label: 'Traits',
+ default: {
+ '@path': '$.traits'
+ }
+ }
+ },
+ perform: (FS, event) => {
+ const newTraits: Record = event.payload.traits || {}
+
+ if (event.payload.anonymousId) {
+ newTraits.segmentAnonymousId = event.payload.anonymousId
+ }
+
+ const userProperties = {
+ ...newTraits,
+ ...(event.payload.email !== undefined && { email: event.payload.email }),
+ ...(event.payload.displayName !== undefined && { displayName: event.payload.displayName })
+ }
+
+ if (event.payload.userId) {
+ FS(
+ 'setIdentity',
+ {
+ uid: event.payload.userId,
+ properties: userProperties
+ },
+ segmentEventSource
+ )
+ } else {
+ FS(
+ 'setProperties',
+ {
+ type: 'user',
+ properties: userProperties
+ },
+ segmentEventSource
+ )
+ }
+ }
+}
+
+export default action
diff --git a/packages/browser-destinations/destinations/fullstory/src/index.ts b/packages/browser-destinations/destinations/fullstory/src/index.ts
index c7494cabe4..01b1ac4d75 100644
--- a/packages/browser-destinations/destinations/fullstory/src/index.ts
+++ b/packages/browser-destinations/destinations/fullstory/src/index.ts
@@ -1,11 +1,14 @@
import type { FS } from './types'
import type { BrowserDestinationDefinition } from '@segment/browser-destination-runtime/types'
-import { FSPackage } from './types'
+import { initFullStory } from './types'
import { browserDestination } from '@segment/browser-destination-runtime/shim'
import type { Settings } from './generated-types'
import trackEvent from './trackEvent'
+import trackEventV2 from './trackEventV2'
import identifyUser from './identifyUser'
+import identifyUserV2 from './identifyUserV2'
import viewedPage from './viewedPage'
+import viewedPageV2 from './viewedPageV2'
import { defaultValues } from '@segment/actions-core'
declare global {
@@ -24,15 +27,22 @@ export const destination: BrowserDestinationDefinition = {
{
name: 'Track Event',
subscribe: 'type = "track"',
- partnerAction: 'trackEvent',
- mapping: defaultValues(trackEvent.fields),
+ partnerAction: 'trackEventV2',
+ mapping: defaultValues(trackEventV2.fields),
type: 'automatic'
},
{
name: 'Identify User',
subscribe: 'type = "identify"',
- partnerAction: 'identifyUser',
- mapping: defaultValues(identifyUser.fields),
+ partnerAction: 'identifyUserV2',
+ mapping: defaultValues(identifyUserV2.fields),
+ type: 'automatic'
+ },
+ {
+ name: 'Viewed Page',
+ subscribe: 'type = "page"',
+ partnerAction: 'viewedPageV2',
+ mapping: defaultValues(viewedPageV2.fields),
type: 'automatic'
}
],
@@ -60,11 +70,14 @@ export const destination: BrowserDestinationDefinition = {
},
actions: {
trackEvent,
+ trackEventV2,
identifyUser,
- viewedPage
+ identifyUserV2,
+ viewedPage,
+ viewedPageV2
},
initialize: async ({ settings }, dependencies) => {
- FSPackage.init(settings)
+ initFullStory(settings)
await dependencies.resolveWhen(() => Object.prototype.hasOwnProperty.call(window, 'FS'), 100)
return window.FS
}
diff --git a/packages/browser-destinations/destinations/fullstory/src/trackEventV2/generated-types.ts b/packages/browser-destinations/destinations/fullstory/src/trackEventV2/generated-types.ts
new file mode 100644
index 0000000000..6ec21ec140
--- /dev/null
+++ b/packages/browser-destinations/destinations/fullstory/src/trackEventV2/generated-types.ts
@@ -0,0 +1,14 @@
+// Generated file. DO NOT MODIFY IT BY HAND.
+
+export interface Payload {
+ /**
+ * The name of the event.
+ */
+ name: string
+ /**
+ * A JSON object containing additional information about the event that will be indexed by FullStory.
+ */
+ properties?: {
+ [k: string]: unknown
+ }
+}
diff --git a/packages/browser-destinations/destinations/fullstory/src/trackEventV2/index.ts b/packages/browser-destinations/destinations/fullstory/src/trackEventV2/index.ts
new file mode 100644
index 0000000000..50f2f7b2c0
--- /dev/null
+++ b/packages/browser-destinations/destinations/fullstory/src/trackEventV2/index.ts
@@ -0,0 +1,44 @@
+import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
+import type { Settings } from '../generated-types'
+import type { Payload } from './generated-types'
+import type { FS } from '../types'
+import { segmentEventSource } from '..'
+
+const action: BrowserActionDefinition = {
+ title: 'Track Event V2',
+ description: 'Track events',
+ platform: 'web',
+ defaultSubscription: 'type = "track"',
+ fields: {
+ name: {
+ description: 'The name of the event.',
+ label: 'Name',
+ required: true,
+ type: 'string',
+ default: {
+ '@path': '$.event'
+ }
+ },
+ properties: {
+ description: 'A JSON object containing additional information about the event that will be indexed by FullStory.',
+ label: 'Properties',
+ required: false,
+ type: 'object',
+ default: {
+ '@path': '$.properties'
+ }
+ }
+ },
+ perform: (FS, event) => {
+ FS(
+ 'trackEvent',
+ {
+ name: event.payload.name,
+ properties: event.payload.properties ?? {}
+ },
+ segmentEventSource
+ )
+ }
+}
+
+export default action
diff --git a/packages/browser-destinations/destinations/fullstory/src/types.ts b/packages/browser-destinations/destinations/fullstory/src/types.ts
index fa6f59e129..7a23e1a0e5 100644
--- a/packages/browser-destinations/destinations/fullstory/src/types.ts
+++ b/packages/browser-destinations/destinations/fullstory/src/types.ts
@@ -1,10 +1,4 @@
-import * as FullStory from '@fullstory/browser'
+import { FullStory, init as initFullStory } from '@fullstory/browser'
-export const FSPackage = FullStory
-export type FS = typeof FullStory & {
- // setVars is not available on the FS client yet.
- setVars: (eventName: string, eventProperties: object, source: string) => {}
- setUserVars: (eventProperties: object, source: string) => void
- event: (eventName: string, eventProperties: { [key: string]: unknown }, source: string) => void
- identify: (uid: string, customVars: FullStory.UserVars, source: string) => void
-}
+export type FS = typeof FullStory
+export { FullStory, initFullStory }
diff --git a/packages/browser-destinations/destinations/fullstory/src/viewedPageV2/generated-types.ts b/packages/browser-destinations/destinations/fullstory/src/viewedPageV2/generated-types.ts
new file mode 100644
index 0000000000..fa90a6ee12
--- /dev/null
+++ b/packages/browser-destinations/destinations/fullstory/src/viewedPageV2/generated-types.ts
@@ -0,0 +1,14 @@
+// Generated file. DO NOT MODIFY IT BY HAND.
+
+export interface Payload {
+ /**
+ * The name of the page that was viewed.
+ */
+ pageName?: string
+ /**
+ * The properties of the page that was viewed.
+ */
+ properties?: {
+ [k: string]: unknown
+ }
+}
diff --git a/packages/browser-destinations/destinations/fullstory/src/viewedPageV2/index.ts b/packages/browser-destinations/destinations/fullstory/src/viewedPageV2/index.ts
new file mode 100644
index 0000000000..a531c0df22
--- /dev/null
+++ b/packages/browser-destinations/destinations/fullstory/src/viewedPageV2/index.ts
@@ -0,0 +1,52 @@
+import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
+import type { Settings } from '../generated-types'
+import type { Payload } from './generated-types'
+import type { FS } from '../types'
+import { segmentEventSource } from '..'
+
+const action: BrowserActionDefinition = {
+ title: 'Viewed Page V2',
+ description: 'Sets page properties',
+ defaultSubscription: 'type = "page"',
+ platform: 'web',
+ fields: {
+ pageName: {
+ type: 'string',
+ required: false,
+ description: 'The name of the page that was viewed.',
+ label: 'Page Name',
+ default: {
+ '@if': {
+ exists: { '@path': '$.category' },
+ then: { '@path': '$.category' },
+ else: { '@path': '$.name' }
+ }
+ }
+ },
+ properties: {
+ type: 'object',
+ required: false,
+ description: 'The properties of the page that was viewed.',
+ label: 'Properties',
+ default: {
+ '@path': '$.properties'
+ }
+ }
+ },
+ perform: (FS, event) => {
+ const properties: object = event.payload.pageName
+ ? { pageName: event.payload.pageName, ...event.payload.properties }
+ : { ...event.payload.properties }
+
+ FS(
+ 'setProperties',
+ {
+ type: 'page',
+ properties
+ },
+ segmentEventSource
+ )
+ }
+}
+
+export default action
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/package.json b/packages/browser-destinations/destinations/google-analytics-4-web/package.json
index 1f3bca6926..35b30c824e 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/package.json
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/package.json
@@ -1,6 +1,6 @@
{
"name": "@segment/analytics-browser-actions-google-analytics-4",
- "version": "1.19.0",
+ "version": "1.40.0",
"license": "MIT",
"publishConfig": {
"access": "public",
@@ -15,8 +15,8 @@
},
"typings": "./dist/esm",
"dependencies": {
- "@segment/actions-core": "^3.83.0",
- "@segment/browser-destination-runtime": "^1.14.0"
+ "@segment/actions-core": "^3.103.0",
+ "@segment/browser-destination-runtime": "^1.33.0"
},
"peerDependencies": {
"@segment/analytics-next": ">=1.55.0"
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/addPaymentInfo.test.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/addPaymentInfo.test.ts
index 01a93f084a..9a25125aa5 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/addPaymentInfo.test.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/addPaymentInfo.test.ts
@@ -18,6 +18,9 @@ const subscriptions: Subscription[] = [
coupon: {
'@path': '$.properties.coupon'
},
+ send_to: {
+ '@path': '$.properties.send_to'
+ },
items: [
{
item_name: {
@@ -64,7 +67,41 @@ describe('GoogleAnalytics4Web.addPaymentInfo', () => {
await trackEventPlugin.load(Context.system(), {} as Analytics)
})
- test('GA4 addPaymentInfo Event', async () => {
+ test('GA4 addPaymentInfo Event when send to is false', async () => {
+ const context = new Context({
+ event: 'Payment Info Entered',
+ type: 'track',
+ properties: {
+ currency: 'USD',
+ value: 10,
+ coupon: 'SUMMER_123',
+ payment_method: 'Credit Card',
+ send_to: false,
+ products: [
+ {
+ product_id: '12345',
+ name: 'Monopoly: 3rd Edition',
+ currency: 'USD'
+ }
+ ]
+ }
+ })
+ await addPaymentInfoEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('add_payment_info'),
+ expect.objectContaining({
+ coupon: 'SUMMER_123',
+ currency: 'USD',
+ items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
+ value: 10,
+ send_to: 'default'
+ })
+ )
+ })
+
+ test('GA4 addPaymentInfo Event when send to is true', async () => {
const context = new Context({
event: 'Payment Info Entered',
type: 'track',
@@ -73,6 +110,7 @@ describe('GoogleAnalytics4Web.addPaymentInfo', () => {
value: 10,
coupon: 'SUMMER_123',
payment_method: 'Credit Card',
+ send_to: true,
products: [
{
product_id: '12345',
@@ -91,7 +129,41 @@ describe('GoogleAnalytics4Web.addPaymentInfo', () => {
coupon: 'SUMMER_123',
currency: 'USD',
items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
- value: 10
+ value: 10,
+ send_to: settings.measurementID
+ })
+ )
+ })
+
+ test('GA4 addPaymentInfo Event when send to is undefined', async () => {
+ const context = new Context({
+ event: 'Payment Info Entered',
+ type: 'track',
+ properties: {
+ currency: 'USD',
+ value: 10,
+ coupon: 'SUMMER_123',
+ payment_method: 'Credit Card',
+ products: [
+ {
+ product_id: '12345',
+ name: 'Monopoly: 3rd Edition',
+ currency: 'USD'
+ }
+ ]
+ }
+ })
+ await addPaymentInfoEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('add_payment_info'),
+ expect.objectContaining({
+ coupon: 'SUMMER_123',
+ currency: 'USD',
+ items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
+ value: 10,
+ send_to: 'default'
})
)
})
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/addToCart.test.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/addToCart.test.ts
index dd2852d7aa..024f34742c 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/addToCart.test.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/addToCart.test.ts
@@ -15,6 +15,9 @@ const subscriptions: Subscription[] = [
value: {
'@path': '$.properties.value'
},
+ send_to: {
+ '@path': '$.properties.send_to'
+ },
items: [
{
item_name: {
@@ -61,13 +64,14 @@ describe('GoogleAnalytics4Web.addToCart', () => {
await trackEventPlugin.load(Context.system(), {} as Analytics)
})
- test('GA4 addToCart Event', async () => {
+ test('GA4 addToCart Event when send to is false', async () => {
const context = new Context({
event: 'Add To Cart',
type: 'track',
properties: {
currency: 'USD',
value: 10,
+ send_to: false,
products: [
{
product_id: '12345',
@@ -85,7 +89,68 @@ describe('GoogleAnalytics4Web.addToCart', () => {
expect.objectContaining({
currency: 'USD',
items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
- value: 10
+ value: 10,
+ send_to: 'default'
+ })
+ )
+ })
+ test('GA4 addToCart Event when send to is true', async () => {
+ const context = new Context({
+ event: 'Add To Cart',
+ type: 'track',
+ properties: {
+ currency: 'USD',
+ value: 10,
+ send_to: true,
+ products: [
+ {
+ product_id: '12345',
+ name: 'Monopoly: 3rd Edition',
+ currency: 'USD'
+ }
+ ]
+ }
+ })
+ await addToCartEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('add_to_cart'),
+ expect.objectContaining({
+ currency: 'USD',
+ items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
+ value: 10,
+ send_to: settings.measurementID
+ })
+ )
+ })
+
+ test('GA4 addToCart Event when send to is undefined', async () => {
+ const context = new Context({
+ event: 'Add To Cart',
+ type: 'track',
+ properties: {
+ currency: 'USD',
+ value: 10,
+ products: [
+ {
+ product_id: '12345',
+ name: 'Monopoly: 3rd Edition',
+ currency: 'USD'
+ }
+ ]
+ }
+ })
+ await addToCartEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('add_to_cart'),
+ expect.objectContaining({
+ currency: 'USD',
+ items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
+ value: 10,
+ send_to: 'default'
})
)
})
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/addToWishlist.test.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/addToWishlist.test.ts
index 4dfc53432a..fdd99f0664 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/addToWishlist.test.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/addToWishlist.test.ts
@@ -15,6 +15,9 @@ const subscriptions: Subscription[] = [
value: {
'@path': '$.properties.value'
},
+ send_to: {
+ '@path': '$.properties.send_to'
+ },
items: [
{
item_name: {
@@ -61,13 +64,45 @@ describe('GoogleAnalytics4Web.addToWishlist', () => {
await trackEventPlugin.load(Context.system(), {} as Analytics)
})
- test('Track call without parameters', async () => {
+ test('Track call without parameters when send to is false', async () => {
+ const context = new Context({
+ event: 'Add To Wishlist',
+ type: 'track',
+ properties: {
+ currency: 'USD',
+ value: 10,
+ send_to: false,
+ products: [
+ {
+ product_id: '12345',
+ name: 'Monopoly: 3rd Edition',
+ currency: 'USD'
+ }
+ ]
+ }
+ })
+ await addToWishlistEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('add_to_wishlist'),
+ expect.objectContaining({
+ currency: 'USD',
+ items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
+ value: 10,
+ send_to: 'default'
+ })
+ )
+ })
+
+ test('Track call without parameters when send to is true', async () => {
const context = new Context({
event: 'Add To Wishlist',
type: 'track',
properties: {
currency: 'USD',
value: 10,
+ send_to: true,
products: [
{
product_id: '12345',
@@ -85,7 +120,38 @@ describe('GoogleAnalytics4Web.addToWishlist', () => {
expect.objectContaining({
currency: 'USD',
items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
- value: 10
+ value: 10,
+ send_to: settings.measurementID
+ })
+ )
+ })
+
+ test('Track call without parameters when send to is undefined', async () => {
+ const context = new Context({
+ event: 'Add To Wishlist',
+ type: 'track',
+ properties: {
+ currency: 'USD',
+ value: 10,
+ products: [
+ {
+ product_id: '12345',
+ name: 'Monopoly: 3rd Edition',
+ currency: 'USD'
+ }
+ ]
+ }
+ })
+ await addToWishlistEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('add_to_wishlist'),
+ expect.objectContaining({
+ currency: 'USD',
+ items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
+ value: 10,
+ send_to: 'default'
})
)
})
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/beginCheckout.test.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/beginCheckout.test.ts
index f59339b1d7..23d72e68e1 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/beginCheckout.test.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/beginCheckout.test.ts
@@ -18,6 +18,9 @@ const subscriptions: Subscription[] = [
coupon: {
'@path': '$.properties.coupon'
},
+ send_to: {
+ '@path': '$.properties.send_to'
+ },
items: [
{
item_name: {
@@ -64,7 +67,73 @@ describe('GoogleAnalytics4Web.beginCheckout', () => {
await trackEventPlugin.load(Context.system(), {} as Analytics)
})
- test('GA4 beginCheckout Event', async () => {
+ test('GA4 beginCheckout Event when send to is false', async () => {
+ const context = new Context({
+ event: 'Begin Checkout',
+ type: 'track',
+ properties: {
+ currency: 'USD',
+ value: 10,
+ coupon: 'SUMMER_123',
+ payment_method: 'Credit Card',
+ send_to: false,
+ products: [
+ {
+ product_id: '12345',
+ name: 'Monopoly: 3rd Edition',
+ currency: 'USD'
+ }
+ ]
+ }
+ })
+ await beginCheckoutEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('begin_checkout'),
+ expect.objectContaining({
+ coupon: 'SUMMER_123',
+ currency: 'USD',
+ items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
+ value: 10,
+ send_to: 'default'
+ })
+ )
+ })
+ test('GA4 beginCheckout Event when send to is true', async () => {
+ const context = new Context({
+ event: 'Begin Checkout',
+ type: 'track',
+ properties: {
+ currency: 'USD',
+ value: 10,
+ coupon: 'SUMMER_123',
+ payment_method: 'Credit Card',
+ send_to: true,
+ products: [
+ {
+ product_id: '12345',
+ name: 'Monopoly: 3rd Edition',
+ currency: 'USD'
+ }
+ ]
+ }
+ })
+ await beginCheckoutEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('begin_checkout'),
+ expect.objectContaining({
+ coupon: 'SUMMER_123',
+ currency: 'USD',
+ items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
+ value: 10,
+ send_to: settings.measurementID
+ })
+ )
+ })
+ test('GA4 beginCheckout Event when send to is undefined', async () => {
const context = new Context({
event: 'Begin Checkout',
type: 'track',
@@ -91,7 +160,8 @@ describe('GoogleAnalytics4Web.beginCheckout', () => {
coupon: 'SUMMER_123',
currency: 'USD',
items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
- value: 10
+ value: 10,
+ send_to: 'default'
})
)
})
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/customEvent.test.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/customEvent.test.ts
index 32c263736e..c155cbcc89 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/customEvent.test.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/customEvent.test.ts
@@ -12,6 +12,9 @@ const subscriptions: Subscription[] = [
name: {
'@path': '$.event'
},
+ send_to: {
+ '@path': '$.properties.send_to'
+ },
params: {
'@path': '$.properties.params'
}
@@ -42,7 +45,34 @@ describe('GoogleAnalytics4Web.customEvent', () => {
await trackEventPlugin.load(Context.system(), {} as Analytics)
})
- test('GA4 customEvent Event', async () => {
+ test('GA4 customEvent Event when send_to is false', async () => {
+ const context = new Context({
+ event: 'Custom Event',
+ type: 'track',
+ properties: {
+ send_to: false,
+ params: [
+ {
+ paramOne: 'test123',
+ paramTwo: 'test123',
+ paramThree: 123
+ }
+ ]
+ }
+ })
+ await customEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('Custom_Event'),
+ expect.objectContaining({
+ send_to: 'default',
+ ...[{ paramOne: 'test123', paramThree: 123, paramTwo: 'test123' }]
+ })
+ )
+ })
+
+ test('GA4 customEvent Event when send_to is undefined', async () => {
const context = new Context({
event: 'Custom Event',
type: 'track',
@@ -61,7 +91,37 @@ describe('GoogleAnalytics4Web.customEvent', () => {
expect(mockGA4).toHaveBeenCalledWith(
expect.anything(),
expect.stringContaining('Custom_Event'),
- expect.objectContaining([{ paramOne: 'test123', paramThree: 123, paramTwo: 'test123' }])
+ expect.objectContaining({
+ send_to: 'default',
+ ...[{ paramOne: 'test123', paramThree: 123, paramTwo: 'test123' }]
+ })
+ )
+ })
+
+ test('GA4 customEvent Event when send_to is true', async () => {
+ const context = new Context({
+ event: 'Custom Event',
+ type: 'track',
+ properties: {
+ params: [
+ {
+ paramOne: 'test123',
+ paramTwo: 'test123',
+ paramThree: 123
+ }
+ ],
+ send_to: true
+ }
+ })
+ await customEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('Custom_Event'),
+ expect.objectContaining({
+ send_to: settings.measurementID,
+ ...[{ paramOne: 'test123', paramThree: 123, paramTwo: 'test123' }]
+ })
)
})
})
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/generateLead.test.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/generateLead.test.ts
index cc7cebb251..f334f4df38 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/generateLead.test.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/generateLead.test.ts
@@ -14,6 +14,9 @@ const subscriptions: Subscription[] = [
},
value: {
'@path': '$.properties.value'
+ },
+ send_to: {
+ '@path': '$.properties.send_to'
}
}
}
@@ -42,13 +45,36 @@ describe('GoogleAnalytics4Web.generateLead', () => {
await trackEventPlugin.load(Context.system(), {} as Analytics)
})
- test('GA4 generateLead Event', async () => {
+ test('GA4 generateLead Event when send to is false', async () => {
const context = new Context({
event: 'Generate Lead',
type: 'track',
properties: {
currency: 'USD',
- value: 10
+ value: 10,
+ send_to: false
+ }
+ })
+ await generateLeadEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('generate_lead'),
+ expect.objectContaining({
+ currency: 'USD',
+ value: 10,
+ send_to: 'default'
+ })
+ )
+ })
+ test('GA4 generateLead Event when send to is true', async () => {
+ const context = new Context({
+ event: 'Generate Lead',
+ type: 'track',
+ properties: {
+ currency: 'USD',
+ value: 10,
+ send_to: true
}
})
await generateLeadEvent.track?.(context)
@@ -57,8 +83,30 @@ describe('GoogleAnalytics4Web.generateLead', () => {
expect.anything(),
expect.stringContaining('generate_lead'),
expect.objectContaining({
+ currency: 'USD',
+ value: 10,
+ send_to: settings.measurementID
+ })
+ )
+ })
+ test('GA4 generateLead Event when send to is undefined', async () => {
+ const context = new Context({
+ event: 'Generate Lead',
+ type: 'track',
+ properties: {
currency: 'USD',
value: 10
+ }
+ })
+ await generateLeadEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('generate_lead'),
+ expect.objectContaining({
+ currency: 'USD',
+ value: 10,
+ send_to: 'default'
})
)
})
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/login.test.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/login.test.ts
index 3aadda5fcb..b50577a251 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/login.test.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/login.test.ts
@@ -11,6 +11,9 @@ const subscriptions: Subscription[] = [
mapping: {
method: {
'@path': '$.properties.method'
+ },
+ send_to: {
+ '@path': '$.properties.send_to'
}
}
}
@@ -39,12 +42,33 @@ describe('GoogleAnalytics4Web.login', () => {
await trackEventPlugin.load(Context.system(), {} as Analytics)
})
- test('GA4 login Event', async () => {
+ test('GA4 login Event when send to is false', async () => {
const context = new Context({
event: 'Login',
type: 'track',
properties: {
- method: 'Google'
+ method: 'Google',
+ send_to: false
+ }
+ })
+ await loginEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('login'),
+ expect.objectContaining({
+ method: 'Google',
+ send_to: 'default'
+ })
+ )
+ })
+ test('GA4 login Event when send to is true', async () => {
+ const context = new Context({
+ event: 'Login',
+ type: 'track',
+ properties: {
+ method: 'Google',
+ send_to: true
}
})
await loginEvent.track?.(context)
@@ -53,7 +77,28 @@ describe('GoogleAnalytics4Web.login', () => {
expect.anything(),
expect.stringContaining('login'),
expect.objectContaining({
+ method: 'Google',
+ send_to: settings.measurementID
+ })
+ )
+ })
+
+ test('GA4 login Event when send to is undefined', async () => {
+ const context = new Context({
+ event: 'Login',
+ type: 'track',
+ properties: {
method: 'Google'
+ }
+ })
+ await loginEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('login'),
+ expect.objectContaining({
+ method: 'Google',
+ send_to: 'default'
})
)
})
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/purchase.test.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/purchase.test.ts
index d231073f1d..37d2d0aec7 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/purchase.test.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/purchase.test.ts
@@ -21,6 +21,9 @@ const subscriptions: Subscription[] = [
transaction_id: {
'@path': '$.properties.transaction_id'
},
+ send_to: {
+ '@path': '$.properties.send_to'
+ },
items: [
{
item_name: {
@@ -67,7 +70,7 @@ describe('GoogleAnalytics4Web.purchase', () => {
await trackEventPlugin.load(Context.system(), {} as Analytics)
})
- test('GA4 purchase Event', async () => {
+ test('GA4 purchase Event when send to is false', async () => {
const context = new Context({
event: 'Purchase',
type: 'track',
@@ -75,6 +78,7 @@ describe('GoogleAnalytics4Web.purchase', () => {
currency: 'USD',
value: 10,
transaction_id: 12321,
+ send_to: false,
products: [
{
product_id: '12345',
@@ -93,7 +97,72 @@ describe('GoogleAnalytics4Web.purchase', () => {
currency: 'USD',
transaction_id: 12321,
items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
- value: 10
+ value: 10,
+ send_to: 'default'
+ })
+ )
+ })
+ test('GA4 purchase Event when send to is true', async () => {
+ const context = new Context({
+ event: 'Purchase',
+ type: 'track',
+ properties: {
+ currency: 'USD',
+ value: 10,
+ transaction_id: 12321,
+ send_to: true,
+ products: [
+ {
+ product_id: '12345',
+ name: 'Monopoly: 3rd Edition',
+ currency: 'USD'
+ }
+ ]
+ }
+ })
+ await purchaseEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('purchase'),
+ expect.objectContaining({
+ currency: 'USD',
+ transaction_id: 12321,
+ items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
+ value: 10,
+ send_to: settings.measurementID
+ })
+ )
+ })
+
+ test('GA4 purchase Event when send to is undefined', async () => {
+ const context = new Context({
+ event: 'Purchase',
+ type: 'track',
+ properties: {
+ currency: 'USD',
+ value: 10,
+ transaction_id: 12321,
+ products: [
+ {
+ product_id: '12345',
+ name: 'Monopoly: 3rd Edition',
+ currency: 'USD'
+ }
+ ]
+ }
+ })
+ await purchaseEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('purchase'),
+ expect.objectContaining({
+ currency: 'USD',
+ transaction_id: 12321,
+ items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
+ value: 10,
+ send_to: 'default'
})
)
})
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/refund.test.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/refund.test.ts
index feba1a6a9e..3ebf3d5fa1 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/refund.test.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/refund.test.ts
@@ -15,6 +15,9 @@ const subscriptions: Subscription[] = [
value: {
'@path': '$.properties.value'
},
+ send_to: {
+ '@path': '$.properties.send_to'
+ },
coupon: {
'@path': '$.properties.coupon'
},
@@ -67,7 +70,71 @@ describe('GoogleAnalytics4Web.refund', () => {
await trackEventPlugin.load(Context.system(), {} as Analytics)
})
- test('GA4 Refund Event', async () => {
+ test('GA4 Refund Event when send to is false', async () => {
+ const context = new Context({
+ event: 'Refund',
+ type: 'track',
+ properties: {
+ currency: 'USD',
+ value: 10,
+ transaction_id: 12321,
+ send_to: false,
+ products: [
+ {
+ product_id: '12345',
+ name: 'Monopoly: 3rd Edition',
+ currency: 'USD'
+ }
+ ]
+ }
+ })
+ await refundEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('refund'),
+ expect.objectContaining({
+ currency: 'USD',
+ transaction_id: 12321,
+ items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
+ value: 10,
+ send_to: 'default'
+ })
+ )
+ })
+ test('GA4 Refund Event when send to is true', async () => {
+ const context = new Context({
+ event: 'Refund',
+ type: 'track',
+ properties: {
+ currency: 'USD',
+ value: 10,
+ transaction_id: 12321,
+ send_to: true,
+ products: [
+ {
+ product_id: '12345',
+ name: 'Monopoly: 3rd Edition',
+ currency: 'USD'
+ }
+ ]
+ }
+ })
+ await refundEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('refund'),
+ expect.objectContaining({
+ currency: 'USD',
+ transaction_id: 12321,
+ items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
+ value: 10,
+ send_to: settings.measurementID
+ })
+ )
+ })
+ test('GA4 Refund Event when send to is undefined', async () => {
const context = new Context({
event: 'Refund',
type: 'track',
@@ -93,7 +160,8 @@ describe('GoogleAnalytics4Web.refund', () => {
currency: 'USD',
transaction_id: 12321,
items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
- value: 10
+ value: 10,
+ send_to: 'default'
})
)
})
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/removeFromCart.test.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/removeFromCart.test.ts
index 31a938a121..2ecdaa5f20 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/removeFromCart.test.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/removeFromCart.test.ts
@@ -18,6 +18,9 @@ const subscriptions: Subscription[] = [
coupon: {
'@path': '$.properties.coupon'
},
+ send_to: {
+ '@path': '$.properties.send_to'
+ },
items: [
{
item_name: {
@@ -64,13 +67,45 @@ describe('GoogleAnalytics4Web.removeFromCart', () => {
await trackEventPlugin.load(Context.system(), {} as Analytics)
})
- test('GA4 removeFromCart Event', async () => {
+ test('GA4 removeFromCart Event when send to is false', async () => {
+ const context = new Context({
+ event: 'Remove from Cart',
+ type: 'track',
+ properties: {
+ currency: 'USD',
+ value: 10,
+ send_to: false,
+ products: [
+ {
+ product_id: '12345',
+ name: 'Monopoly: 3rd Edition',
+ currency: 'USD'
+ }
+ ]
+ }
+ })
+
+ await removeFromCartEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('remove_from_cart'),
+ expect.objectContaining({
+ currency: 'USD',
+ items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
+ value: 10,
+ send_to: 'default'
+ })
+ )
+ })
+ test('GA4 removeFromCart Event when send to is true', async () => {
const context = new Context({
event: 'Remove from Cart',
type: 'track',
properties: {
currency: 'USD',
value: 10,
+ send_to: true,
products: [
{
product_id: '12345',
@@ -89,7 +124,39 @@ describe('GoogleAnalytics4Web.removeFromCart', () => {
expect.objectContaining({
currency: 'USD',
items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
- value: 10
+ value: 10,
+ send_to: settings.measurementID
+ })
+ )
+ })
+
+ test('GA4 removeFromCart Event when send to is undefined', async () => {
+ const context = new Context({
+ event: 'Remove from Cart',
+ type: 'track',
+ properties: {
+ currency: 'USD',
+ value: 10,
+ products: [
+ {
+ product_id: '12345',
+ name: 'Monopoly: 3rd Edition',
+ currency: 'USD'
+ }
+ ]
+ }
+ })
+
+ await removeFromCartEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('remove_from_cart'),
+ expect.objectContaining({
+ currency: 'USD',
+ items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
+ value: 10,
+ send_to: 'default'
})
)
})
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/search.test.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/search.test.ts
index 902006f63f..a7b1c19c33 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/search.test.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/search.test.ts
@@ -11,6 +11,9 @@ const subscriptions: Subscription[] = [
mapping: {
search_term: {
'@path': '$.properties.search_term'
+ },
+ send_to: {
+ '@path': '$.properties.send_to'
}
}
}
@@ -39,12 +42,34 @@ describe('GoogleAnalytics4Web.search', () => {
await trackEventPlugin.load(Context.system(), {} as Analytics)
})
- test('GA4 search Event', async () => {
+ test('GA4 search Event when send to is false', async () => {
const context = new Context({
event: 'search',
type: 'track',
properties: {
- search_term: 'Monopoly: 3rd Edition'
+ search_term: 'Monopoly: 3rd Edition',
+ send_to: false
+ }
+ })
+
+ await searchEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('search'),
+ expect.objectContaining({
+ search_term: 'Monopoly: 3rd Edition',
+ send_to: 'default'
+ })
+ )
+ })
+ test('GA4 search Event when send to is true', async () => {
+ const context = new Context({
+ event: 'search',
+ type: 'track',
+ properties: {
+ search_term: 'Monopoly: 3rd Edition',
+ send_to: true
}
})
@@ -54,7 +79,28 @@ describe('GoogleAnalytics4Web.search', () => {
expect.anything(),
expect.stringContaining('search'),
expect.objectContaining({
+ search_term: 'Monopoly: 3rd Edition',
+ send_to: settings.measurementID
+ })
+ )
+ })
+ test('GA4 search Event when send to is undefined', async () => {
+ const context = new Context({
+ event: 'search',
+ type: 'track',
+ properties: {
search_term: 'Monopoly: 3rd Edition'
+ }
+ })
+
+ await searchEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('search'),
+ expect.objectContaining({
+ search_term: 'Monopoly: 3rd Edition',
+ send_to: 'default'
})
)
})
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/selectItem.test.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/selectItem.test.ts
index b2eeedcd38..345a2884ce 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/selectItem.test.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/selectItem.test.ts
@@ -15,6 +15,9 @@ const subscriptions: Subscription[] = [
item_list_name: {
'@path': '$.properties.item_list_name'
},
+ send_to: {
+ '@path': '$.properties.send_to'
+ },
items: [
{
item_name: {
@@ -61,7 +64,70 @@ describe('GoogleAnalytics4Web.selectItem', () => {
await trackEventPlugin.load(Context.system(), {} as Analytics)
})
- test('GA4 selectItem Event', async () => {
+ test('GA4 selectItem Event when send to is false', async () => {
+ const context = new Context({
+ event: 'Select Item',
+ type: 'track',
+ properties: {
+ item_list_id: 12321,
+ item_list_name: 'Monopoly: 3rd Edition',
+ send_to: false,
+ products: [
+ {
+ product_id: '12345',
+ name: 'Monopoly: 3rd Edition',
+ currency: 'USD'
+ }
+ ]
+ }
+ })
+
+ await selectItemEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('select_item'),
+ expect.objectContaining({
+ item_list_id: 12321,
+ item_list_name: 'Monopoly: 3rd Edition',
+ items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
+ send_to: 'default'
+ })
+ )
+ })
+ test('GA4 selectItem Event when send to is true', async () => {
+ const context = new Context({
+ event: 'Select Item',
+ type: 'track',
+ properties: {
+ item_list_id: 12321,
+ item_list_name: 'Monopoly: 3rd Edition',
+ send_to: true,
+ products: [
+ {
+ product_id: '12345',
+ name: 'Monopoly: 3rd Edition',
+ currency: 'USD'
+ }
+ ]
+ }
+ })
+
+ await selectItemEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('select_item'),
+ expect.objectContaining({
+ item_list_id: 12321,
+ item_list_name: 'Monopoly: 3rd Edition',
+ items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
+ send_to: settings.measurementID
+ })
+ )
+ })
+
+ test('GA4 selectItem Event when send to is undefined', async () => {
const context = new Context({
event: 'Select Item',
type: 'track',
@@ -86,7 +152,8 @@ describe('GoogleAnalytics4Web.selectItem', () => {
expect.objectContaining({
item_list_id: 12321,
item_list_name: 'Monopoly: 3rd Edition',
- items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }]
+ items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
+ send_to: 'default'
})
)
})
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/selectPromotion.test.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/selectPromotion.test.ts
index a5d4b3446f..c35a910668 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/selectPromotion.test.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/selectPromotion.test.ts
@@ -24,6 +24,9 @@ const subscriptions: Subscription[] = [
promotion_name: {
'@path': '$.properties.promotion_name'
},
+ send_to: {
+ '@path': '$.properties.send_to'
+ },
items: [
{
item_name: {
@@ -70,7 +73,82 @@ describe('GoogleAnalytics4Web.selectPromotion', () => {
await trackEventPlugin.load(Context.system(), {} as Analytics)
})
- test('GA4 selectPromotion Event', async () => {
+ test('GA4 selectPromotion Event when send to is false', async () => {
+ const context = new Context({
+ event: 'Select Promotion',
+ type: 'track',
+ properties: {
+ creative_name: 'summer_banner2',
+ creative_slot: 'featured_app_1',
+ location_id: 'ChIJIQBpAG2ahYAR_6128GcTUEo',
+ promotion_id: 'P_12345',
+ promotion_name: 'Summer Sale',
+ send_to: false,
+ products: [
+ {
+ product_id: '12345',
+ name: 'Monopoly: 3rd Edition',
+ currency: 'USD'
+ }
+ ]
+ }
+ })
+
+ await selectPromotionEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('select_promotion'),
+ expect.objectContaining({
+ creative_name: 'summer_banner2',
+ creative_slot: 'featured_app_1',
+ location_id: 'ChIJIQBpAG2ahYAR_6128GcTUEo',
+ promotion_id: 'P_12345',
+ promotion_name: 'Summer Sale',
+ items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
+ send_to: 'default'
+ })
+ )
+ })
+ test('GA4 selectPromotion Event when send to is true', async () => {
+ const context = new Context({
+ event: 'Select Promotion',
+ type: 'track',
+ properties: {
+ creative_name: 'summer_banner2',
+ creative_slot: 'featured_app_1',
+ location_id: 'ChIJIQBpAG2ahYAR_6128GcTUEo',
+ promotion_id: 'P_12345',
+ promotion_name: 'Summer Sale',
+ send_to: true,
+ products: [
+ {
+ product_id: '12345',
+ name: 'Monopoly: 3rd Edition',
+ currency: 'USD'
+ }
+ ]
+ }
+ })
+
+ await selectPromotionEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('select_promotion'),
+ expect.objectContaining({
+ creative_name: 'summer_banner2',
+ creative_slot: 'featured_app_1',
+ location_id: 'ChIJIQBpAG2ahYAR_6128GcTUEo',
+ promotion_id: 'P_12345',
+ promotion_name: 'Summer Sale',
+ items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
+ send_to: settings.measurementID
+ })
+ )
+ })
+
+ test('GA4 selectPromotion Event when send to is undefined', async () => {
const context = new Context({
event: 'Select Promotion',
type: 'track',
@@ -101,7 +179,8 @@ describe('GoogleAnalytics4Web.selectPromotion', () => {
location_id: 'ChIJIQBpAG2ahYAR_6128GcTUEo',
promotion_id: 'P_12345',
promotion_name: 'Summer Sale',
- items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }]
+ items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
+ send_to: 'default'
})
)
})
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/setConfigurationFields.test.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/setConfigurationFields.test.ts
new file mode 100644
index 0000000000..feca9da7d3
--- /dev/null
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/setConfigurationFields.test.ts
@@ -0,0 +1,809 @@
+import googleAnalytics4Web, { destination } from '../index'
+import { Analytics, Context } from '@segment/analytics-next'
+import { Subscription } from '@segment/browser-destination-runtime/types'
+import type { Settings } from '../generated-types'
+
+let mockGtag: GA
+let setConfigurationEvent: any
+const subscriptions: Subscription[] = [
+ {
+ partnerAction: 'setConfigurationFields',
+ name: 'Set Configuration Fields',
+ enabled: true,
+ subscribe: 'type = "page"',
+ mapping: {
+ ads_storage_consent_state: {
+ '@path': '$.properties.ads_storage_consent_state'
+ },
+ analytics_storage_consent_state: {
+ '@path': '$.properties.analytics_storage_consent_state'
+ },
+ screen_resolution: {
+ '@path': '$.properties.screen_resolution'
+ },
+ user_id: {
+ '@path': '$.properties.user_id'
+ },
+ page_title: {
+ '@path': '$.properties.page_title'
+ },
+ page_referrer: {
+ '@path': '$.properties.page_referrer'
+ },
+ language: {
+ '@path': '$.properties.language'
+ },
+ content_group: {
+ '@path': '$.properties.content_group'
+ },
+ campaign_content: {
+ '@path': '$.properties.campaign_content'
+ },
+ campaign_id: {
+ '@path': '$.properties.campaign_id'
+ },
+ campaign_medium: {
+ '@path': '$.properties.campaign_medium'
+ },
+ campaign_name: {
+ '@path': '$.properties.campaign_name'
+ },
+ campaign_source: {
+ '@path': '$.properties.campaign_source'
+ },
+ campaign_term: {
+ '@path': '$.properties.campaign_term'
+ },
+ ad_user_data_consent_state: {
+ '@path': '$.properties.ad_user_data_consent_state'
+ },
+ ad_personalization_consent_state: {
+ '@path': '$.properties.ad_personalization_consent_state'
+ },
+ send_page_view: {
+ '@path': '$.properties.send_page_view'
+ }
+ }
+ }
+]
+
+describe('Set Configuration Fields action', () => {
+ const defaultSettings: Settings = {
+ enableConsentMode: false,
+ measurementID: 'G-XXXXXXXXXX',
+ allowAdPersonalizationSignals: false,
+ allowGoogleSignals: false,
+ cookieDomain: 'auto',
+ cookieExpirationInSeconds: 63072000,
+ cookieUpdate: true,
+ pageView: true
+ }
+ beforeEach(async () => {
+ jest.restoreAllMocks()
+
+ const [setConfigurationEventPlugin] = await googleAnalytics4Web({
+ ...defaultSettings,
+ subscriptions
+ })
+ setConfigurationEvent = setConfigurationEventPlugin
+
+ jest.spyOn(destination, 'initialize').mockImplementation(() => {
+ mockGtag = jest.fn()
+ return Promise.resolve(mockGtag)
+ })
+ await setConfigurationEventPlugin.load(Context.system(), {} as Analytics)
+ })
+
+ it('should configure consent when consent mode is enabled', async () => {
+ defaultSettings.enableConsentMode = true
+
+ const [setConfigurationEventPlugin] = await googleAnalytics4Web({
+ ...defaultSettings,
+ subscriptions
+ })
+ setConfigurationEvent = setConfigurationEventPlugin
+ await setConfigurationEventPlugin.load(Context.system(), {} as Analytics)
+
+ const context = new Context({
+ event: 'setConfigurationFields',
+ type: 'page',
+ properties: {
+ ads_storage_consent_state: 'granted',
+ analytics_storage_consent_state: 'denied'
+ }
+ })
+
+ setConfigurationEvent.page?.(context)
+
+ expect(mockGtag).toHaveBeenCalledWith('consent', 'update', {
+ ad_storage: 'granted',
+ analytics_storage: 'denied'
+ })
+ expect(mockGtag).toHaveBeenCalledWith('config', 'G-XXXXXXXXXX', {
+ allow_ad_personalization_signals: false,
+ allow_google_signals: false,
+ send_page_view: true
+ })
+ })
+ it('should configure cookie expiry time other then default value', async () => {
+ const settings = {
+ ...defaultSettings,
+ cookieExpirationInSeconds: 500
+ }
+ const [setConfigurationEventPlugin] = await googleAnalytics4Web({
+ ...settings,
+ subscriptions
+ })
+ setConfigurationEvent = setConfigurationEventPlugin
+ await setConfigurationEventPlugin.load(Context.system(), {} as Analytics)
+
+ const context = new Context({
+ event: 'setConfigurationFields',
+ type: 'page',
+ properties: {}
+ })
+
+ setConfigurationEvent.page?.(context)
+
+ expect(mockGtag).toHaveBeenCalledWith('config', 'G-XXXXXXXXXX', {
+ allow_ad_personalization_signals: false,
+ allow_google_signals: false,
+ send_page_view: true,
+ cookie_expires: 500
+ })
+ })
+ it('should configure cookie domain other then default value', async () => {
+ const settings = {
+ ...defaultSettings,
+ cookieDomain: 'example.com'
+ }
+
+ const [setConfigurationEventPlugin] = await googleAnalytics4Web({
+ ...settings,
+ subscriptions
+ })
+ setConfigurationEvent = setConfigurationEventPlugin
+ await setConfigurationEventPlugin.load(Context.system(), {} as Analytics)
+
+ const context = new Context({
+ event: 'setConfigurationFields',
+ type: 'page',
+ properties: {}
+ })
+
+ setConfigurationEvent.page?.(context)
+
+ expect(mockGtag).toHaveBeenCalledWith('config', 'G-XXXXXXXXXX', {
+ allow_ad_personalization_signals: false,
+ allow_google_signals: false,
+ send_page_view: true,
+ cookie_domain: 'example.com'
+ })
+ })
+ it('should configure cookie prefix other then default value', async () => {
+ const settings = {
+ ...defaultSettings,
+ cookiePrefix: 'stage'
+ }
+ const [setConfigurationEventPlugin] = await googleAnalytics4Web({
+ ...settings,
+ subscriptions
+ })
+ setConfigurationEvent = setConfigurationEventPlugin
+ await setConfigurationEventPlugin.load(Context.system(), {} as Analytics)
+
+ const context = new Context({
+ event: 'setConfigurationFields',
+ type: 'page',
+ properties: {}
+ })
+
+ setConfigurationEvent.page?.(context)
+
+ expect(mockGtag).toHaveBeenCalledWith('config', 'G-XXXXXXXXXX', {
+ allow_ad_personalization_signals: false,
+ allow_google_signals: false,
+ send_page_view: true,
+ cookie_prefix: 'stage'
+ })
+ })
+ it('should configure cookie path other then default value', async () => {
+ const settings = {
+ ...defaultSettings,
+ cookiePath: '/home'
+ }
+
+ const [setConfigurationEventPlugin] = await googleAnalytics4Web({
+ ...settings,
+ subscriptions
+ })
+ setConfigurationEvent = setConfigurationEventPlugin
+ await setConfigurationEventPlugin.load(Context.system(), {} as Analytics)
+
+ const context = new Context({
+ event: 'setConfigurationFields',
+ type: 'page',
+ properties: {}
+ })
+
+ setConfigurationEvent.page?.(context)
+
+ expect(mockGtag).toHaveBeenCalledWith('config', 'G-XXXXXXXXXX', {
+ allow_ad_personalization_signals: false,
+ allow_google_signals: false,
+ send_page_view: true,
+ cookie_path: '/home'
+ })
+ })
+ it('should configure cookie update other then default value', async () => {
+ const settings = {
+ ...defaultSettings,
+ cookieUpdate: false
+ }
+ const [setConfigurationEventPlugin] = await googleAnalytics4Web({
+ ...settings,
+ subscriptions
+ })
+ setConfigurationEvent = setConfigurationEventPlugin
+ await setConfigurationEventPlugin.load(Context.system(), {} as Analytics)
+
+ const context = new Context({
+ event: 'setConfigurationFields',
+ type: 'page',
+ properties: {}
+ })
+
+ setConfigurationEvent.page?.(context)
+
+ expect(mockGtag).toHaveBeenCalledWith('config', 'G-XXXXXXXXXX', {
+ allow_ad_personalization_signals: false,
+ allow_google_signals: false,
+ send_page_view: true,
+ cookie_update: false
+ })
+ })
+ it('should not configure consent when consent mode is disabled', async () => {
+ const settings = {
+ ...defaultSettings,
+ enableConsentMode: false
+ }
+ const [setConfigurationEventPlugin] = await googleAnalytics4Web({
+ ...settings,
+ subscriptions
+ })
+ setConfigurationEvent = setConfigurationEventPlugin
+ await setConfigurationEventPlugin.load(Context.system(), {} as Analytics)
+
+ const context = new Context({
+ event: 'setConfigurationFields',
+ type: 'page',
+ properties: {}
+ })
+
+ setConfigurationEvent.page?.(context)
+ expect(mockGtag).toHaveBeenCalledWith('config', 'G-XXXXXXXXXX', {
+ allow_ad_personalization_signals: false,
+ allow_google_signals: false,
+ send_page_view: true
+ })
+ })
+ it('should update config if payload has screen resolution', () => {
+ const context = new Context({
+ event: 'setConfigurationFields',
+ type: 'page',
+ properties: {
+ screen_resolution: '800*600'
+ }
+ })
+
+ setConfigurationEvent.page?.(context)
+ expect(mockGtag).toHaveBeenCalledWith('config', 'G-XXXXXXXXXX', {
+ allow_ad_personalization_signals: false,
+ allow_google_signals: false,
+ send_page_view: true,
+ screen_resolution: '800*600'
+ })
+ })
+ it('should update config if payload has user_id', () => {
+ const context = new Context({
+ event: 'setConfigurationFields',
+ type: 'page',
+ properties: {
+ user_id: 'segment-123'
+ }
+ })
+
+ setConfigurationEvent.page?.(context)
+ expect(mockGtag).toHaveBeenCalledWith('config', 'G-XXXXXXXXXX', {
+ allow_ad_personalization_signals: false,
+ allow_google_signals: false,
+ send_page_view: true,
+ user_id: 'segment-123'
+ })
+ })
+ it('should update config if payload has page_title', () => {
+ const context = new Context({
+ event: 'setConfigurationFields',
+ type: 'page',
+ properties: {
+ page_title: 'User Registration Page'
+ }
+ })
+
+ setConfigurationEvent.page?.(context)
+ expect(mockGtag).toHaveBeenCalledWith('config', 'G-XXXXXXXXXX', {
+ allow_ad_personalization_signals: false,
+ allow_google_signals: false,
+ send_page_view: true,
+ page_title: 'User Registration Page'
+ })
+ })
+ it('should update config if payload has language', () => {
+ const context = new Context({
+ event: 'setConfigurationFields',
+ type: 'page',
+ properties: {
+ language: 'EN'
+ }
+ })
+
+ setConfigurationEvent.page?.(context)
+ expect(mockGtag).toHaveBeenCalledWith('config', 'G-XXXXXXXXXX', {
+ allow_ad_personalization_signals: false,
+ allow_google_signals: false,
+ send_page_view: true,
+ language: 'EN'
+ })
+ })
+ it('should update config if payload has content_group', () => {
+ const context = new Context({
+ event: 'setConfigurationFields',
+ type: 'page',
+ properties: {
+ content_group: '/home/login'
+ }
+ })
+
+ setConfigurationEvent.page?.(context)
+ expect(mockGtag).toHaveBeenCalledWith('config', 'G-XXXXXXXXXX', {
+ allow_ad_personalization_signals: false,
+ allow_google_signals: false,
+ send_page_view: true,
+ content_group: '/home/login'
+ })
+ })
+ it('should update config if payload has campaign_term', () => {
+ const context = new Context({
+ event: 'setConfigurationFields',
+ type: 'page',
+ properties: {
+ campaign_term: 'running+shoes'
+ }
+ })
+
+ setConfigurationEvent.page?.(context)
+ expect(mockGtag).toHaveBeenCalledWith('config', 'G-XXXXXXXXXX', {
+ allow_ad_personalization_signals: false,
+ allow_google_signals: false,
+ send_page_view: true,
+ campaign_term: 'running+shoes'
+ })
+ })
+ it('should update config if payload has campaign_source', () => {
+ const context = new Context({
+ event: 'setConfigurationFields',
+ type: 'page',
+ properties: {
+ campaign_source: 'google'
+ }
+ })
+
+ setConfigurationEvent.page?.(context)
+ expect(mockGtag).toHaveBeenCalledWith('config', 'G-XXXXXXXXXX', {
+ allow_ad_personalization_signals: false,
+ allow_google_signals: false,
+ send_page_view: true,
+ campaign_source: 'google'
+ })
+ })
+ it('should update config if payload has campaign_name', () => {
+ const context = new Context({
+ event: 'setConfigurationFields',
+ type: 'page',
+ properties: {
+ campaign_name: 'spring_sale'
+ }
+ })
+
+ setConfigurationEvent.page?.(context)
+ expect(mockGtag).toHaveBeenCalledWith('config', 'G-XXXXXXXXXX', {
+ allow_ad_personalization_signals: false,
+ allow_google_signals: false,
+ send_page_view: true,
+ campaign_name: 'spring_sale'
+ })
+ })
+ it('should update config if payload has campaign_medium', () => {
+ const context = new Context({
+ event: 'setConfigurationFields',
+ type: 'page',
+ properties: {
+ campaign_medium: 'cpc'
+ }
+ })
+
+ setConfigurationEvent.page?.(context)
+ expect(mockGtag).toHaveBeenCalledWith('config', 'G-XXXXXXXXXX', {
+ allow_ad_personalization_signals: false,
+ allow_google_signals: false,
+ send_page_view: true,
+ campaign_medium: 'cpc'
+ })
+ })
+ it('should update config if payload has campaign_id', () => {
+ const context = new Context({
+ event: 'setConfigurationFields',
+ type: 'page',
+ properties: {
+ campaign_id: 'abc.123'
+ }
+ })
+
+ setConfigurationEvent.page?.(context)
+ expect(mockGtag).toHaveBeenCalledWith('config', 'G-XXXXXXXXXX', {
+ allow_ad_personalization_signals: false,
+ allow_google_signals: false,
+ send_page_view: true,
+ campaign_id: 'abc.123'
+ })
+ })
+ it('should update config if payload has campaign_content', () => {
+ const context = new Context({
+ event: 'setConfigurationFields',
+ type: 'page',
+ properties: {
+ campaign_content: 'logolink'
+ }
+ })
+
+ setConfigurationEvent.page?.(context)
+ expect(mockGtag).toHaveBeenCalledWith('config', 'G-XXXXXXXXXX', {
+ allow_ad_personalization_signals: false,
+ allow_google_signals: false,
+ send_page_view: true,
+ campaign_content: 'logolink'
+ })
+ })
+
+ it('pageView is true and send_page_view is true -> nothing', async () => {
+ const settings = {
+ ...defaultSettings,
+ pageView: true
+ }
+
+ const [setConfigurationEventPlugin] = await googleAnalytics4Web({
+ ...settings,
+ subscriptions
+ })
+ setConfigurationEvent = setConfigurationEventPlugin
+ await setConfigurationEventPlugin.load(Context.system(), {} as Analytics)
+
+ const context = new Context({
+ event: 'setConfigurationFields',
+ type: 'page',
+ properties: {
+ send_page_view: true
+ }
+ })
+
+ setConfigurationEvent.page?.(context)
+ expect(mockGtag).toHaveBeenCalledWith('config', 'G-XXXXXXXXXX', {
+ allow_ad_personalization_signals: false,
+ allow_google_signals: false
+ })
+ })
+ it('pageView is true and send_page_view is false -> false', async () => {
+ const settings = {
+ ...defaultSettings,
+ pageView: true
+ }
+
+ const [setConfigurationEventPlugin] = await googleAnalytics4Web({
+ ...settings,
+ subscriptions
+ })
+ setConfigurationEvent = setConfigurationEventPlugin
+ await setConfigurationEventPlugin.load(Context.system(), {} as Analytics)
+
+ const context = new Context({
+ event: 'setConfigurationFields',
+ type: 'page',
+ properties: {
+ send_page_view: false
+ }
+ })
+
+ setConfigurationEvent.page?.(context)
+ expect(mockGtag).toHaveBeenCalledWith('config', 'G-XXXXXXXXXX', {
+ allow_ad_personalization_signals: false,
+ allow_google_signals: false,
+ send_page_view: false
+ })
+ })
+ it('pageView is true and send_page_view is undefined -> true', async () => {
+ const settings = {
+ ...defaultSettings,
+ pageView: true
+ }
+
+ const [setConfigurationEventPlugin] = await googleAnalytics4Web({
+ ...settings,
+ subscriptions
+ })
+ setConfigurationEvent = setConfigurationEventPlugin
+ await setConfigurationEventPlugin.load(Context.system(), {} as Analytics)
+
+ const context = new Context({
+ event: 'setConfigurationFields',
+ type: 'page',
+ properties: {
+ send_page_view: undefined
+ }
+ })
+
+ setConfigurationEvent.page?.(context)
+ expect(mockGtag).toHaveBeenCalledWith('config', 'G-XXXXXXXXXX', {
+ allow_ad_personalization_signals: false,
+ allow_google_signals: false,
+ send_page_view: true
+ })
+ })
+
+ it('pageView is false and send_page_view is true -> true', async () => {
+ const settings = {
+ ...defaultSettings,
+ pageView: false
+ }
+
+ const [setConfigurationEventPlugin] = await googleAnalytics4Web({
+ ...settings,
+ subscriptions
+ })
+ setConfigurationEvent = setConfigurationEventPlugin
+ await setConfigurationEventPlugin.load(Context.system(), {} as Analytics)
+
+ const context = new Context({
+ event: 'setConfigurationFields',
+ type: 'page',
+ properties: {
+ send_page_view: true
+ }
+ })
+
+ setConfigurationEvent.page?.(context)
+ expect(mockGtag).toHaveBeenCalledWith('config', 'G-XXXXXXXXXX', {
+ allow_ad_personalization_signals: false,
+ allow_google_signals: false,
+ send_page_view: true
+ })
+ })
+
+ it('pageView is false and send_page_view is false -> false', async () => {
+ const settings = {
+ ...defaultSettings,
+ pageView: false
+ }
+
+ const [setConfigurationEventPlugin] = await googleAnalytics4Web({
+ ...settings,
+ subscriptions
+ })
+ setConfigurationEvent = setConfigurationEventPlugin
+ await setConfigurationEventPlugin.load(Context.system(), {} as Analytics)
+
+ const context = new Context({
+ event: 'setConfigurationFields',
+ type: 'page',
+ properties: {
+ send_page_view: false
+ }
+ })
+
+ setConfigurationEvent.page?.(context)
+ expect(mockGtag).toHaveBeenCalledWith('config', 'G-XXXXXXXXXX', {
+ allow_ad_personalization_signals: false,
+ allow_google_signals: false,
+ send_page_view: false
+ })
+ })
+
+ it('pageView is false and send_page_view is undefined -> false', async () => {
+ const settings = {
+ ...defaultSettings,
+ pageView: false
+ }
+
+ const [setConfigurationEventPlugin] = await googleAnalytics4Web({
+ ...settings,
+ subscriptions
+ })
+ setConfigurationEvent = setConfigurationEventPlugin
+ await setConfigurationEventPlugin.load(Context.system(), {} as Analytics)
+
+ const context = new Context({
+ event: 'setConfigurationFields',
+ type: 'page',
+ properties: {
+ send_page_view: undefined
+ }
+ })
+
+ setConfigurationEvent.page?.(context)
+ expect(mockGtag).toHaveBeenCalledWith('config', 'G-XXXXXXXXXX', {
+ allow_ad_personalization_signals: false,
+ allow_google_signals: false,
+ send_page_view: false
+ })
+ })
+
+ it('pageView is undefined and send_page_view is undefined -> true', async () => {
+ const settings = {
+ ...defaultSettings,
+ pageView: undefined
+ }
+
+ const [setConfigurationEventPlugin] = await googleAnalytics4Web({
+ ...settings,
+ subscriptions
+ })
+ setConfigurationEvent = setConfigurationEventPlugin
+ await setConfigurationEventPlugin.load(Context.system(), {} as Analytics)
+
+ const context = new Context({
+ event: 'setConfigurationFields',
+ type: 'page',
+ properties: {
+ send_page_view: undefined
+ }
+ })
+
+ setConfigurationEvent.page?.(context)
+ expect(mockGtag).toHaveBeenCalledWith('config', 'G-XXXXXXXXXX', {
+ allow_ad_personalization_signals: false,
+ allow_google_signals: false,
+ send_page_view: true
+ })
+ })
+
+ it('should update consent if payload has ad_user_data_consent_state granted', () => {
+ const context = new Context({
+ event: 'setConfigurationFields',
+ type: 'page',
+ properties: {
+ ad_user_data_consent_state: 'granted'
+ }
+ })
+
+ setConfigurationEvent.page?.(context)
+ expect(mockGtag).toHaveBeenCalledWith('consent', 'update', {
+ ad_user_data: 'granted'
+ })
+ })
+
+ it('should update consent if payload has ad_user_data_consent_state denied', () => {
+ const context = new Context({
+ event: 'setConfigurationFields',
+ type: 'page',
+ properties: {
+ ad_user_data_consent_state: 'denied'
+ }
+ })
+
+ setConfigurationEvent.page?.(context)
+ expect(mockGtag).toHaveBeenCalledWith('consent', 'update', {
+ ad_user_data: 'denied'
+ })
+ })
+
+ it('should update consent if payload has ad_user_data_consent_state undefined', () => {
+ const context = new Context({
+ event: 'setConfigurationFields',
+ type: 'page',
+ properties: {
+ ad_user_data_consent_state: undefined
+ }
+ })
+
+ setConfigurationEvent.page?.(context)
+ expect(mockGtag).toHaveBeenCalledWith('consent', 'update', {})
+ })
+
+ it('should update consent if payload has ad_personalization_consent_state granted', () => {
+ const context = new Context({
+ event: 'setConfigurationFields',
+ type: 'page',
+ properties: {
+ ad_personalization_consent_state: 'granted'
+ }
+ })
+
+ setConfigurationEvent.page?.(context)
+ expect(mockGtag).toHaveBeenCalledWith('consent', 'update', {
+ ad_personalization: 'granted'
+ })
+ })
+ it('should update consent if payload has ad_personalization_consent_state denied', () => {
+ const context = new Context({
+ event: 'setConfigurationFields',
+ type: 'page',
+ properties: {
+ ad_personalization_consent_state: 'denied'
+ }
+ })
+
+ setConfigurationEvent.page?.(context)
+ expect(mockGtag).toHaveBeenCalledWith('consent', 'update', {
+ ad_personalization: 'denied'
+ })
+ })
+ it('should update consent if payload has ad_personalization_consent_state undefined', () => {
+ const context = new Context({
+ event: 'setConfigurationFields',
+ type: 'page',
+ properties: {
+ ad_personalization_consent_state: undefined
+ }
+ })
+
+ setConfigurationEvent.page?.(context)
+ expect(mockGtag).toHaveBeenCalledWith('consent', 'update', {})
+ })
+ it('should update config if payload has send_page_view undefined', () => {
+ const context = new Context({
+ event: 'setConfigurationFields',
+ type: 'page',
+ properties: {
+ send_page_view: undefined
+ }
+ })
+
+ setConfigurationEvent.page?.(context)
+ expect(mockGtag).toHaveBeenCalledWith('config', 'G-XXXXXXXXXX', {
+ allow_ad_personalization_signals: false,
+ allow_google_signals: false,
+ send_page_view: true
+ })
+ })
+ it('should update config if payload has send_page_view true', () => {
+ const context = new Context({
+ event: 'setConfigurationFields',
+ type: 'page',
+ properties: {
+ send_page_view: true
+ }
+ })
+
+ setConfigurationEvent.page?.(context)
+ expect(mockGtag).toHaveBeenCalledWith('config', 'G-XXXXXXXXXX', {
+ allow_ad_personalization_signals: false,
+ allow_google_signals: false
+ })
+ })
+
+ it('should update config if payload has send_page_view false', () => {
+ const context = new Context({
+ event: 'setConfigurationFields',
+ type: 'page',
+ properties: {
+ send_page_view: false
+ }
+ })
+
+ setConfigurationEvent.page?.(context)
+ expect(mockGtag).toHaveBeenCalledWith('config', 'G-XXXXXXXXXX', {
+ allow_ad_personalization_signals: false,
+ allow_google_signals: false,
+ send_page_view: false
+ })
+ })
+})
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/signUp.test.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/signUp.test.ts
index 60ea396d23..e17079d77f 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/signUp.test.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/signUp.test.ts
@@ -11,6 +11,9 @@ const subscriptions: Subscription[] = [
mapping: {
method: {
'@path': '$.properties.method'
+ },
+ send_to: {
+ '@path': '$.properties.send_to'
}
}
}
@@ -39,7 +42,44 @@ describe('GoogleAnalytics4Web.signUp', () => {
await trackEventPlugin.load(Context.system(), {} as Analytics)
})
- test('GA4 signUp Event', async () => {
+ test('GA4 signUp Event when send to is false', async () => {
+ const context = new Context({
+ event: 'signUp',
+ type: 'track',
+ properties: {
+ method: 'Google',
+ send_to: false
+ }
+ })
+
+ await signUpEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('sign_up'),
+ expect.objectContaining({ method: 'Google', send_to: 'default' })
+ )
+ })
+ test('GA4 signUp Event when send to is true', async () => {
+ const context = new Context({
+ event: 'signUp',
+ type: 'track',
+ properties: {
+ method: 'Google',
+ send_to: true
+ }
+ })
+
+ await signUpEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('sign_up'),
+ expect.objectContaining({ method: 'Google', send_to: settings.measurementID })
+ )
+ })
+
+ test('GA4 signUp Event when send to is undefined', async () => {
const context = new Context({
event: 'signUp',
type: 'track',
@@ -53,7 +93,7 @@ describe('GoogleAnalytics4Web.signUp', () => {
expect(mockGA4).toHaveBeenCalledWith(
expect.anything(),
expect.stringContaining('sign_up'),
- expect.objectContaining({ method: 'Google' })
+ expect.objectContaining({ method: 'Google', send_to: 'default' })
)
})
})
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/viewCart.test.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/viewCart.test.ts
index d4df588843..da2c7c52da 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/viewCart.test.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/viewCart.test.ts
@@ -15,6 +15,9 @@ const subscriptions: Subscription[] = [
value: {
'@path': '$.properties.value'
},
+ send_to: {
+ '@path': '$.properties.send_to'
+ },
items: [
{
item_name: {
@@ -61,13 +64,45 @@ describe('GoogleAnalytics4Web.viewCart', () => {
await trackEventPlugin.load(Context.system(), {} as Analytics)
})
- test('GA4 viewCart Event', async () => {
+ test('GA4 viewCart Event when send to is false', async () => {
+ const context = new Context({
+ event: 'View Cart',
+ type: 'track',
+ properties: {
+ currency: 'USD',
+ value: 10,
+ send_to: false,
+ products: [
+ {
+ product_id: '12345',
+ name: 'Monopoly: 3rd Edition',
+ currency: 'USD'
+ }
+ ]
+ }
+ })
+
+ await viewCartEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('view_cart'),
+ expect.objectContaining({
+ currency: 'USD',
+ items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
+ value: 10,
+ send_to: 'default'
+ })
+ )
+ })
+ test('GA4 viewCart Event when send to is true', async () => {
const context = new Context({
event: 'View Cart',
type: 'track',
properties: {
currency: 'USD',
value: 10,
+ send_to: true,
products: [
{
product_id: '12345',
@@ -86,7 +121,39 @@ describe('GoogleAnalytics4Web.viewCart', () => {
expect.objectContaining({
currency: 'USD',
items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
- value: 10
+ value: 10,
+ send_to: settings.measurementID
+ })
+ )
+ })
+
+ test('GA4 viewCart Event when send to is false', async () => {
+ const context = new Context({
+ event: 'View Cart',
+ type: 'track',
+ properties: {
+ currency: 'USD',
+ value: 10,
+ products: [
+ {
+ product_id: '12345',
+ name: 'Monopoly: 3rd Edition',
+ currency: 'USD'
+ }
+ ]
+ }
+ })
+
+ await viewCartEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('view_cart'),
+ expect.objectContaining({
+ currency: 'USD',
+ items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
+ value: 10,
+ send_to: 'default'
})
)
})
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/viewItem.test.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/viewItem.test.ts
index bcd6b35f86..08d3ab4706 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/viewItem.test.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/viewItem.test.ts
@@ -15,6 +15,9 @@ const subscriptions: Subscription[] = [
value: {
'@path': '$.properties.value'
},
+ send_to: {
+ '@path': '$.properties.send_to'
+ },
items: [
{
item_name: {
@@ -61,7 +64,69 @@ describe('GoogleAnalytics4Web.viewItem', () => {
await trackEventPlugin.load(Context.system(), {} as Analytics)
})
- test('GA4 viewItem Event', async () => {
+ test('GA4 viewItem Event when send to is false', async () => {
+ const context = new Context({
+ event: 'View Item',
+ type: 'track',
+ properties: {
+ currency: 'USD',
+ value: 10,
+ send_to: false,
+ products: [
+ {
+ product_id: '12345',
+ name: 'Monopoly: 3rd Edition',
+ currency: 'USD'
+ }
+ ]
+ }
+ })
+
+ await viewItemEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('view_item'),
+ expect.objectContaining({
+ currency: 'USD',
+ items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
+ value: 10,
+ send_to: 'default'
+ })
+ )
+ })
+ test('GA4 viewItem Event when send to is true', async () => {
+ const context = new Context({
+ event: 'View Item',
+ type: 'track',
+ properties: {
+ currency: 'USD',
+ value: 10,
+ send_to: true,
+ products: [
+ {
+ product_id: '12345',
+ name: 'Monopoly: 3rd Edition',
+ currency: 'USD'
+ }
+ ]
+ }
+ })
+
+ await viewItemEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('view_item'),
+ expect.objectContaining({
+ currency: 'USD',
+ items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
+ value: 10,
+ send_to: settings.measurementID
+ })
+ )
+ })
+ test('GA4 viewItem Event when send to is undefined', async () => {
const context = new Context({
event: 'View Item',
type: 'track',
@@ -86,7 +151,8 @@ describe('GoogleAnalytics4Web.viewItem', () => {
expect.objectContaining({
currency: 'USD',
items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
- value: 10
+ value: 10,
+ send_to: 'default'
})
)
})
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/viewItemList.test.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/viewItemList.test.ts
index e6630315f3..b9ec9bd1ad 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/viewItemList.test.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/viewItemList.test.ts
@@ -15,6 +15,9 @@ const subscriptions: Subscription[] = [
item_list_name: {
'@path': '$.properties.item_list_name'
},
+ send_to: {
+ '@path': '$.properties.send_to'
+ },
items: [
{
item_name: {
@@ -61,7 +64,71 @@ describe('GoogleAnalytics4Web.viewItemList', () => {
await trackEventPlugin.load(Context.system(), {} as Analytics)
})
- test('GA4 viewItemList Event', async () => {
+ test('GA4 viewItemList Event when send to is false', async () => {
+ const context = new Context({
+ event: 'View Item List',
+ type: 'track',
+ properties: {
+ item_list_id: 12321,
+ item_list_name: 'Monopoly: 3rd Edition',
+ send_to: false,
+ products: [
+ {
+ product_id: '12345',
+ name: 'Monopoly: 3rd Edition',
+ currency: 'USD'
+ }
+ ]
+ }
+ })
+
+ await viewItemListEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('view_item_list'),
+ expect.objectContaining({
+ item_list_id: 12321,
+ item_list_name: 'Monopoly: 3rd Edition',
+ items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
+ send_to: 'default'
+ })
+ )
+ })
+
+ test('GA4 viewItemList Event when send to is true', async () => {
+ const context = new Context({
+ event: 'View Item List',
+ type: 'track',
+ properties: {
+ item_list_id: 12321,
+ item_list_name: 'Monopoly: 3rd Edition',
+ send_to: true,
+ products: [
+ {
+ product_id: '12345',
+ name: 'Monopoly: 3rd Edition',
+ currency: 'USD'
+ }
+ ]
+ }
+ })
+
+ await viewItemListEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('view_item_list'),
+ expect.objectContaining({
+ item_list_id: 12321,
+ item_list_name: 'Monopoly: 3rd Edition',
+ items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
+ send_to: settings.measurementID
+ })
+ )
+ })
+
+ test('GA4 viewItemList Event when send to is undefined', async () => {
const context = new Context({
event: 'View Item List',
type: 'track',
@@ -86,7 +153,8 @@ describe('GoogleAnalytics4Web.viewItemList', () => {
expect.objectContaining({
item_list_id: 12321,
item_list_name: 'Monopoly: 3rd Edition',
- items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }]
+ items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
+ send_to: 'default'
})
)
})
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/viewPromotion.test.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/viewPromotion.test.ts
index 7b5f605c1c..3e5356242b 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/viewPromotion.test.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/__tests__/viewPromotion.test.ts
@@ -24,6 +24,9 @@ const subscriptions: Subscription[] = [
promotion_name: {
'@path': '$.properties.promotion_name'
},
+ send_to: {
+ '@path': '$.properties.send_to'
+ },
items: [
{
item_name: {
@@ -70,7 +73,81 @@ describe('GoogleAnalytics4Web.viewPromotion', () => {
await trackEventPlugin.load(Context.system(), {} as Analytics)
})
- test('GA4 viewPromotion Event', async () => {
+ test('GA4 viewPromotion Event when send to is false', async () => {
+ const context = new Context({
+ event: 'Select Promotion',
+ type: 'track',
+ properties: {
+ creative_name: 'summer_banner2',
+ creative_slot: 'featured_app_1',
+ location_id: 'ChIJIQBpAG2ahYAR_6128GcTUEo',
+ promotion_id: 'P_12345',
+ promotion_name: 'Summer Sale',
+ send_to: false,
+ products: [
+ {
+ product_id: '12345',
+ name: 'Monopoly: 3rd Edition',
+ currency: 'USD'
+ }
+ ]
+ }
+ })
+
+ await viewPromotionEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('view_promotion'),
+ expect.objectContaining({
+ creative_name: 'summer_banner2',
+ creative_slot: 'featured_app_1',
+ location_id: 'ChIJIQBpAG2ahYAR_6128GcTUEo',
+ promotion_id: 'P_12345',
+ promotion_name: 'Summer Sale',
+ items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
+ send_to: 'default'
+ })
+ )
+ })
+ test('GA4 viewPromotion Event when send to is true', async () => {
+ const context = new Context({
+ event: 'Select Promotion',
+ type: 'track',
+ properties: {
+ creative_name: 'summer_banner2',
+ creative_slot: 'featured_app_1',
+ location_id: 'ChIJIQBpAG2ahYAR_6128GcTUEo',
+ promotion_id: 'P_12345',
+ promotion_name: 'Summer Sale',
+ send_to: true,
+ products: [
+ {
+ product_id: '12345',
+ name: 'Monopoly: 3rd Edition',
+ currency: 'USD'
+ }
+ ]
+ }
+ })
+
+ await viewPromotionEvent.track?.(context)
+
+ expect(mockGA4).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.stringContaining('view_promotion'),
+ expect.objectContaining({
+ creative_name: 'summer_banner2',
+ creative_slot: 'featured_app_1',
+ location_id: 'ChIJIQBpAG2ahYAR_6128GcTUEo',
+ promotion_id: 'P_12345',
+ promotion_name: 'Summer Sale',
+ items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
+ send_to: settings.measurementID
+ })
+ )
+ })
+ test('GA4 viewPromotion Event when send to is undefined', async () => {
const context = new Context({
event: 'Select Promotion',
type: 'track',
@@ -101,7 +178,8 @@ describe('GoogleAnalytics4Web.viewPromotion', () => {
location_id: 'ChIJIQBpAG2ahYAR_6128GcTUEo',
promotion_id: 'P_12345',
promotion_name: 'Summer Sale',
- items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }]
+ items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }],
+ send_to: 'default'
})
)
})
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/addPaymentInfo/generated-types.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/addPaymentInfo/generated-types.ts
index 8babfe4438..c083cd3a00 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/addPaymentInfo/generated-types.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/addPaymentInfo/generated-types.ts
@@ -101,6 +101,7 @@ export interface Payload {
* Item quantity.
*/
quantity?: number
+ [k: string]: unknown
}[]
/**
* The user properties to send to Google Analytics 4. You must create user-scoped dimensions to ensure custom properties are picked up by Google. See Google’s [Custom user properties](https://support.google.com/analytics/answer/9269570) to learn how to set and register user properties.
@@ -114,4 +115,8 @@ export interface Payload {
params?: {
[k: string]: unknown
}
+ /**
+ * If the send_to parameter is not set, events are routed to all Tag Ids (AW-xxx, G-xxx) set via Google Tag
+ */
+ send_to?: boolean
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/addPaymentInfo/index.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/addPaymentInfo/index.ts
index d770fa1e20..b4d891e3b8 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/addPaymentInfo/index.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/addPaymentInfo/index.ts
@@ -1,7 +1,6 @@
import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'
-import { updateUser } from '../ga4-functions'
import {
user_id,
user_properties,
@@ -10,7 +9,8 @@ import {
coupon,
payment_type,
items_multi_products,
- params
+ params,
+ send_to
} from '../ga4-properties'
// Change from unknown to the partner SDK types
@@ -30,16 +30,19 @@ const action: BrowserActionDefinition = {
required: true
},
user_properties: user_properties,
- params: params
+ params: params,
+ send_to: send_to
},
- perform: (gtag, { payload }) => {
- updateUser(payload.user_id, payload.user_properties, gtag)
+ perform: (gtag, { payload, settings }) => {
gtag('event', 'add_payment_info', {
currency: payload.currency,
value: payload.value,
coupon: payload.coupon,
payment_type: payload.payment_type,
items: payload.items,
+ user_id: payload.user_id ?? undefined,
+ user_properties: payload.user_properties,
+ send_to: payload.send_to == true ? settings.measurementID : 'default',
...payload.params
})
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/addToCart/generated-types.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/addToCart/generated-types.ts
index 6e0f5789e3..93061827ac 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/addToCart/generated-types.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/addToCart/generated-types.ts
@@ -89,6 +89,7 @@ export interface Payload {
* Item quantity.
*/
quantity?: number
+ [k: string]: unknown
}[]
/**
* The monetary value of the event.
@@ -106,4 +107,8 @@ export interface Payload {
params?: {
[k: string]: unknown
}
+ /**
+ * If the send_to parameter is not set, events are routed to all Tag Ids (AW-xxx, G-xxx) set via Google Tag
+ */
+ send_to?: boolean
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/addToCart/index.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/addToCart/index.ts
index e11cf5ec89..359fbdd59c 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/addToCart/index.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/addToCart/index.ts
@@ -1,9 +1,8 @@
import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'
-import { updateUser } from '../ga4-functions'
-import { user_properties, params, value, currency, items_single_products, user_id } from '../ga4-properties'
+import { user_properties, params, value, currency, items_single_products, user_id, send_to } from '../ga4-properties'
const action: BrowserActionDefinition = {
title: 'Add to Cart',
@@ -19,15 +18,17 @@ const action: BrowserActionDefinition = {
},
value: value,
user_properties: user_properties,
- params: params
+ params: params,
+ send_to: send_to
},
- perform: (gtag, { payload }) => {
- updateUser(payload.user_id, payload.user_properties, gtag)
-
+ perform: (gtag, { payload, settings }) => {
gtag('event', 'add_to_cart', {
currency: payload.currency,
value: payload.value,
items: payload.items,
+ user_id: payload.user_id ?? undefined,
+ user_properties: payload.user_properties,
+ send_to: payload.send_to == true ? settings.measurementID : 'default',
...payload.params
})
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/addToWishlist/generated-types.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/addToWishlist/generated-types.ts
index a7f5a2e20e..b6d8985b27 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/addToWishlist/generated-types.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/addToWishlist/generated-types.ts
@@ -93,6 +93,7 @@ export interface Payload {
* Item quantity.
*/
quantity?: number
+ [k: string]: unknown
}[]
/**
* The user properties to send to Google Analytics 4. You must create user-scoped dimensions to ensure custom properties are picked up by Google. See Google’s [Custom user properties](https://support.google.com/analytics/answer/9269570) to learn how to set and register user properties.
@@ -106,4 +107,8 @@ export interface Payload {
params?: {
[k: string]: unknown
}
+ /**
+ * If the send_to parameter is not set, events are routed to all Tag Ids (AW-xxx, G-xxx) set via Google Tag
+ */
+ send_to?: boolean
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/addToWishlist/index.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/addToWishlist/index.ts
index b5a1145d0e..f416414283 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/addToWishlist/index.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/addToWishlist/index.ts
@@ -2,8 +2,7 @@ import type { BrowserActionDefinition } from '@segment/browser-destination-runti
import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'
-import { user_properties, params, value, currency, items_single_products, user_id } from '../ga4-properties'
-import { updateUser } from '../ga4-functions'
+import { user_properties, params, value, currency, items_single_products, user_id, send_to } from '../ga4-properties'
// Change from unknown to the partner SDK types
const action: BrowserActionDefinition = {
@@ -21,15 +20,17 @@ const action: BrowserActionDefinition = {
required: true
},
user_properties: user_properties,
- params: params
+ params: params,
+ send_to: send_to
},
- perform: (gtag, { payload }) => {
- updateUser(payload.user_id, payload.user_properties, gtag)
-
+ perform: (gtag, { payload, settings }) => {
gtag('event', 'add_to_wishlist', {
currency: payload.currency,
value: payload.value,
items: payload.items,
+ user_id: payload.user_id ?? undefined,
+ user_properties: payload.user_properties,
+ send_to: payload.send_to == true ? settings.measurementID : 'default',
...payload.params
})
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/beginCheckout/generated-types.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/beginCheckout/generated-types.ts
index 284bf69d75..084dee203d 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/beginCheckout/generated-types.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/beginCheckout/generated-types.ts
@@ -93,6 +93,7 @@ export interface Payload {
* Item quantity.
*/
quantity?: number
+ [k: string]: unknown
}[]
/**
* The monetary value of the event.
@@ -110,4 +111,8 @@ export interface Payload {
user_properties?: {
[k: string]: unknown
}
+ /**
+ * If the send_to parameter is not set, events are routed to all Tag Ids (AW-xxx, G-xxx) set via Google Tag
+ */
+ send_to?: boolean
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/beginCheckout/index.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/beginCheckout/index.ts
index 02d55fe142..37cfe3c65d 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/beginCheckout/index.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/beginCheckout/index.ts
@@ -2,8 +2,16 @@ import type { BrowserActionDefinition } from '@segment/browser-destination-runti
import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'
-import { params, coupon, currency, value, items_multi_products, user_id, user_properties } from '../ga4-properties'
-import { updateUser } from '../ga4-functions'
+import {
+ params,
+ coupon,
+ currency,
+ value,
+ items_multi_products,
+ user_id,
+ user_properties,
+ send_to
+} from '../ga4-properties'
const action: BrowserActionDefinition = {
title: 'Begin Checkout',
@@ -20,16 +28,18 @@ const action: BrowserActionDefinition = {
},
value: value,
params: params,
- user_properties: user_properties
+ user_properties: user_properties,
+ send_to: send_to
},
- perform: (gtag, { payload }) => {
- updateUser(payload.user_id, payload.user_properties, gtag)
-
+ perform: (gtag, { payload, settings }) => {
gtag('event', 'begin_checkout', {
currency: payload.currency,
value: payload.value,
coupon: payload.coupon,
items: payload.items,
+ user_id: payload.user_id ?? undefined,
+ user_properties: payload.user_properties,
+ send_to: payload.send_to == true ? settings.measurementID : 'default',
...payload.params
})
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/customEvent/generated-types.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/customEvent/generated-types.ts
index 3aa8ae426e..fbab80b0b2 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/customEvent/generated-types.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/customEvent/generated-types.ts
@@ -25,4 +25,8 @@ export interface Payload {
params?: {
[k: string]: unknown
}
+ /**
+ * If the send_to parameter is not set, events are routed to all Tag Ids (AW-xxx, G-xxx) set via Google Tag
+ */
+ send_to?: boolean
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/customEvent/index.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/customEvent/index.ts
index d8428e46a0..d711da5953 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/customEvent/index.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/customEvent/index.ts
@@ -1,8 +1,7 @@
import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'
-import { user_id, user_properties, params } from '../ga4-properties'
-import { updateUser } from '../ga4-functions'
+import { user_id, user_properties, params, send_to } from '../ga4-properties'
const normalizeEventName = (name: string, lowercase: boolean | undefined): string => {
name = name.trim()
@@ -39,13 +38,18 @@ const action: BrowserActionDefinition = {
},
user_id: { ...user_id },
user_properties: user_properties,
- params: params
+ params: params,
+ send_to: send_to
},
- perform: (gtag, { payload }) => {
- updateUser(payload.user_id, payload.user_properties, gtag)
+ perform: (gtag, { payload, settings }) => {
const event_name = normalizeEventName(payload.name, payload.lowercase)
- gtag('event', event_name, payload.params)
+ gtag('event', event_name, {
+ user_id: payload.user_id ?? undefined,
+ user_properties: payload.user_properties,
+ send_to: payload.send_to == true ? settings.measurementID : 'default',
+ ...payload.params
+ })
}
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/ga4-functions.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/ga4-functions.ts
deleted file mode 100644
index 0525e99038..0000000000
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/ga4-functions.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export function updateUser(userID: string | undefined, userProps: object | undefined, gtag: Function): void {
- if (userID) {
- gtag('set', { user_id: userID })
- }
- if (userProps) {
- gtag('set', { user_properties: userProps })
- }
-}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/ga4-properties.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/ga4-properties.ts
index f5a542afd1..bdbd03d360 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/ga4-properties.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/ga4-properties.ts
@@ -190,6 +190,7 @@ export const minimal_items: InputField = {
description: 'The list of products purchased.',
type: 'object',
multiple: true,
+ additionalProperties: true,
properties: {
item_id: {
label: 'Product ID',
@@ -366,3 +367,10 @@ export const items_multi_products: InputField = {
]
}
}
+export const send_to: InputField = {
+ label: 'Send To',
+ type: 'boolean',
+ default: true,
+ description:
+ 'If the send_to parameter is not set, events are routed to all Tag Ids (AW-xxx, G-xxx) set via Google Tag'
+}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/generateLead/generated-types.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/generateLead/generated-types.ts
index 2e7db72077..744738a197 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/generateLead/generated-types.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/generateLead/generated-types.ts
@@ -25,4 +25,8 @@ export interface Payload {
params?: {
[k: string]: unknown
}
+ /**
+ * If the send_to parameter is not set, events are routed to all Tag Ids (AW-xxx, G-xxx) set via Google Tag
+ */
+ send_to?: boolean
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/generateLead/index.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/generateLead/index.ts
index c73003aefb..c2302fa3b3 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/generateLead/index.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/generateLead/index.ts
@@ -2,8 +2,7 @@ import type { BrowserActionDefinition } from '@segment/browser-destination-runti
import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'
-import { user_properties, params, user_id, currency, value } from '../ga4-properties'
-import { updateUser } from '../ga4-functions'
+import { user_properties, params, user_id, currency, value, send_to } from '../ga4-properties'
const action: BrowserActionDefinition = {
title: 'Generate Lead',
@@ -16,14 +15,16 @@ const action: BrowserActionDefinition = {
currency: currency,
value: value,
user_properties: user_properties,
- params: params
+ params: params,
+ send_to: send_to
},
- perform: (gtag, { payload }) => {
- updateUser(payload.user_id, payload.user_properties, gtag)
-
+ perform: (gtag, { payload, settings }) => {
gtag('event', 'generate_lead', {
currency: payload.currency,
value: payload.value,
+ user_id: payload.user_id ?? undefined,
+ user_properties: payload.user_properties,
+ send_to: payload.send_to == true ? settings.measurementID : 'default',
...payload.params
})
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/generated-types.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/generated-types.ts
index 37712dfed4..3582face62 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/generated-types.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/generated-types.ts
@@ -26,9 +26,9 @@ export interface Settings {
*/
cookieFlags?: string[]
/**
- * Specifies the subpath used to store the analytics cookie.
+ * Specifies the subpath used to store the analytics cookie. We recommend to add a forward slash, / , in the first field as it is the Default Value for GA4.
*/
- cookiePath?: string[]
+ cookiePath?: string
/**
* Specifies a prefix to prepend to the analytics cookie name.
*/
@@ -49,6 +49,14 @@ export interface Settings {
* The default value for analytics cookies consent state. This is only used if Enable Consent Mode is on. Set to “granted” if it is not explicitly set. Consent state can be updated for each user in the Set Configuration Fields action.
*/
defaultAnalyticsStorageConsentState?: string
+ /**
+ * Consent state indicated by the user for ad cookies. Value must be "granted" or "denied." This is only used if the Enable Consent Mode setting is on.
+ */
+ adUserDataConsentState?: string
+ /**
+ * Consent state indicated by the user for ad cookies. Value must be "granted" or "denied." This is only used if the Enable Consent Mode setting is on.
+ */
+ adPersonalizationConsentState?: string
/**
* If your CMP loads asynchronously, it might not always run before the Google tag. To handle such situations, specify a millisecond value to control how long to wait before the consent state update is sent. Please input the wait_for_update in milliseconds.
*/
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/index.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/index.ts
index 480e43e0db..22400aa327 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/index.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/index.ts
@@ -35,7 +35,7 @@ type ConsentParamsArg = 'granted' | 'denied' | undefined
const presets: DestinationDefinition['presets'] = [
{
name: `Set Configuration Fields`,
- subscribe: 'type = "page" or type = "identify"',
+ subscribe: 'type = "page"',
partnerAction: 'setConfigurationFields',
mapping: defaultValues(setConfigurationFields.fields),
type: 'automatic'
@@ -83,18 +83,20 @@ export const destination: BrowserDestinationDefinition = {
description: `Appends additional flags to the analytics cookie. See [write a new cookie](https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie#write_a_new_cookie) for some examples of flags to set.`,
label: 'Cookie Flag',
type: 'string',
+ default: undefined,
multiple: true
},
cookiePath: {
- description: `Specifies the subpath used to store the analytics cookie.`,
+ description: `Specifies the subpath used to store the analytics cookie. We recommend to add a forward slash, / , in the first field as it is the Default Value for GA4.`,
label: 'Cookie Path',
type: 'string',
- multiple: true
+ default: '/'
},
cookiePrefix: {
description: `Specifies a prefix to prepend to the analytics cookie name.`,
label: 'Cookie Prefix',
type: 'string',
+ default: undefined,
multiple: true
},
cookieUpdate: {
@@ -131,6 +133,28 @@ export const destination: BrowserDestinationDefinition = {
],
default: 'granted'
},
+ adUserDataConsentState: {
+ description:
+ 'Consent state indicated by the user for ad cookies. Value must be "granted" or "denied." This is only used if the Enable Consent Mode setting is on.',
+ label: 'Ad User Data Consent State',
+ type: 'string',
+ choices: [
+ { label: 'Granted', value: 'granted' },
+ { label: 'Denied', value: 'denied' }
+ ],
+ default: undefined
+ },
+ adPersonalizationConsentState: {
+ description:
+ 'Consent state indicated by the user for ad cookies. Value must be "granted" or "denied." This is only used if the Enable Consent Mode setting is on.',
+ label: 'Ad Personalization Consent State',
+ type: 'string',
+ choices: [
+ { label: 'Granted', value: 'granted' },
+ { label: 'Denied', value: 'denied' }
+ ],
+ default: undefined
+ },
waitTimeToUpdateConsentStage: {
description:
'If your CMP loads asynchronously, it might not always run before the Google tag. To handle such situations, specify a millisecond value to control how long to wait before the consent state update is sent. Please input the wait_for_update in milliseconds.',
@@ -154,11 +178,24 @@ export const destination: BrowserDestinationDefinition = {
window.gtag('js', new Date())
if (settings.enableConsentMode) {
- window.gtag('consent', 'default', {
+ const consent: {
+ ad_storage: ConsentParamsArg
+ analytics_storage: ConsentParamsArg
+ wait_for_update: number | undefined
+ ad_user_data?: ConsentParamsArg
+ ad_personalization?: ConsentParamsArg
+ } = {
ad_storage: settings.defaultAdsStorageConsentState as ConsentParamsArg,
analytics_storage: settings.defaultAnalyticsStorageConsentState as ConsentParamsArg,
wait_for_update: settings.waitTimeToUpdateConsentStage
- })
+ }
+ if (settings.adUserDataConsentState) {
+ consent.ad_user_data = settings.adUserDataConsentState as ConsentParamsArg
+ }
+ if (settings.adPersonalizationConsentState) {
+ consent.ad_personalization = settings.adPersonalizationConsentState as ConsentParamsArg
+ }
+ gtag('consent', 'default', consent)
}
const script = `https://www.googletagmanager.com/gtag/js?id=${settings.measurementID}`
await deps.loadScript(script)
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/login/generated-types.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/login/generated-types.ts
index c305ce6764..e1e758672f 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/login/generated-types.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/login/generated-types.ts
@@ -21,4 +21,8 @@ export interface Payload {
params?: {
[k: string]: unknown
}
+ /**
+ * If the send_to parameter is not set, events are routed to all Tag Ids (AW-xxx, G-xxx) set via Google Tag
+ */
+ send_to?: boolean
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/login/index.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/login/index.ts
index 0b85ed3678..122cae19eb 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/login/index.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/login/index.ts
@@ -2,8 +2,7 @@ import type { BrowserActionDefinition } from '@segment/browser-destination-runti
import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'
-import { user_properties, params, user_id, method } from '../ga4-properties'
-import { updateUser } from '../ga4-functions'
+import { user_properties, params, user_id, method, send_to } from '../ga4-properties'
// Change from unknown to the partner SDK types
const action: BrowserActionDefinition = {
@@ -15,14 +14,16 @@ const action: BrowserActionDefinition = {
user_id: user_id,
method: method,
user_properties: user_properties,
- params: params
+ params: params,
+ send_to: send_to
},
- perform: (gtag, { payload }) => {
- updateUser(payload.user_id, payload.user_properties, gtag)
-
+ perform: (gtag, { payload, settings }) => {
gtag('event', 'login', {
method: payload.method,
+ user_id: payload.user_id ?? undefined,
+ user_properties: payload.user_properties,
+ send_to: payload.send_to == true ? settings.measurementID : 'default',
...payload.params
})
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/purchase/generated-types.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/purchase/generated-types.ts
index 498bba5c46..59e0cd9074 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/purchase/generated-types.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/purchase/generated-types.ts
@@ -93,6 +93,7 @@ export interface Payload {
* Item quantity.
*/
quantity?: number
+ [k: string]: unknown
}[]
/**
* The unique identifier of a transaction.
@@ -122,4 +123,8 @@ export interface Payload {
params?: {
[k: string]: unknown
}
+ /**
+ * If the send_to parameter is not set, events are routed to all Tag Ids (AW-xxx, G-xxx) set via Google Tag
+ */
+ send_to?: boolean
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/purchase/index.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/purchase/index.ts
index b694450600..09a5ef4017 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/purchase/index.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/purchase/index.ts
@@ -11,9 +11,9 @@ import {
tax,
items_multi_products,
params,
- user_properties
+ user_properties,
+ send_to
} from '../ga4-properties'
-import { updateUser } from '../ga4-functions'
const action: BrowserActionDefinition = {
title: 'Purchase',
@@ -33,11 +33,10 @@ const action: BrowserActionDefinition = {
tax: tax,
value: { ...value, default: { '@path': '$.properties.total' } },
user_properties: user_properties,
- params: params
+ params: params,
+ send_to: send_to
},
- perform: (gtag, { payload }) => {
- updateUser(payload.user_id, payload.user_properties, gtag)
-
+ perform: (gtag, { payload, settings }) => {
gtag('event', 'purchase', {
currency: payload.currency,
transaction_id: payload.transaction_id,
@@ -46,6 +45,9 @@ const action: BrowserActionDefinition = {
tax: payload.tax,
shipping: payload.shipping,
items: payload.items,
+ user_id: payload.user_id ?? undefined,
+ user_properties: payload.user_properties,
+ send_to: payload.send_to == true ? settings.measurementID : 'default',
...payload.params
})
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/refund/generated-types.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/refund/generated-types.ts
index e97d5a5bb2..e2a9e691e2 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/refund/generated-types.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/refund/generated-types.ts
@@ -113,6 +113,7 @@ export interface Payload {
* Item quantity.
*/
quantity?: number
+ [k: string]: unknown
}[]
/**
* The user properties to send to Google Analytics 4. You must create user-scoped dimensions to ensure custom properties are picked up by Google. See Google’s [Custom user properties](https://support.google.com/analytics/answer/9269570) to learn how to set and register user properties.
@@ -126,4 +127,8 @@ export interface Payload {
params?: {
[k: string]: unknown
}
+ /**
+ * If the send_to parameter is not set, events are routed to all Tag Ids (AW-xxx, G-xxx) set via Google Tag
+ */
+ send_to?: boolean
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/refund/index.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/refund/index.ts
index a3f2da2911..bfc5f0386b 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/refund/index.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/refund/index.ts
@@ -13,9 +13,9 @@ import {
items_multi_products,
params,
user_properties,
- tax
+ tax,
+ send_to
} from '../ga4-properties'
-import { updateUser } from '../ga4-functions'
const action: BrowserActionDefinition = {
title: 'Refund',
@@ -35,11 +35,10 @@ const action: BrowserActionDefinition = {
...items_multi_products
},
user_properties: user_properties,
- params: params
+ params: params,
+ send_to: send_to
},
- perform: (gtag, { payload }) => {
- updateUser(payload.user_id, payload.user_properties, gtag)
-
+ perform: (gtag, { payload, settings }) => {
gtag('event', 'refund', {
currency: payload.currency,
transaction_id: payload.transaction_id, // Transaction ID. Required for purchases and refunds.
@@ -49,6 +48,9 @@ const action: BrowserActionDefinition = {
shipping: payload.shipping,
tax: payload.tax,
items: payload.items,
+ user_id: payload.user_id ?? undefined,
+ user_properties: payload.user_properties,
+ send_to: payload.send_to == true ? settings.measurementID : 'default',
...payload.params
})
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/removeFromCart/generated-types.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/removeFromCart/generated-types.ts
index a7f5a2e20e..b6d8985b27 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/removeFromCart/generated-types.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/removeFromCart/generated-types.ts
@@ -93,6 +93,7 @@ export interface Payload {
* Item quantity.
*/
quantity?: number
+ [k: string]: unknown
}[]
/**
* The user properties to send to Google Analytics 4. You must create user-scoped dimensions to ensure custom properties are picked up by Google. See Google’s [Custom user properties](https://support.google.com/analytics/answer/9269570) to learn how to set and register user properties.
@@ -106,4 +107,8 @@ export interface Payload {
params?: {
[k: string]: unknown
}
+ /**
+ * If the send_to parameter is not set, events are routed to all Tag Ids (AW-xxx, G-xxx) set via Google Tag
+ */
+ send_to?: boolean
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/removeFromCart/index.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/removeFromCart/index.ts
index 314e9791b5..b72832ff69 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/removeFromCart/index.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/removeFromCart/index.ts
@@ -2,8 +2,7 @@ import type { BrowserActionDefinition } from '@segment/browser-destination-runti
import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'
-import { user_properties, params, value, user_id, currency, items_single_products } from '../ga4-properties'
-import { updateUser } from '../ga4-functions'
+import { user_properties, params, value, user_id, currency, items_single_products, send_to } from '../ga4-properties'
// Change from unknown to the partner SDK types
const action: BrowserActionDefinition = {
@@ -20,15 +19,17 @@ const action: BrowserActionDefinition = {
required: true
},
user_properties: user_properties,
- params: params
+ params: params,
+ send_to: send_to
},
- perform: (gtag, { payload }) => {
- updateUser(payload.user_id, payload.user_properties, gtag)
-
+ perform: (gtag, { payload, settings }) => {
gtag('event', 'remove_from_cart', {
currency: payload.currency,
value: payload.value,
items: payload.items,
+ user_id: payload.user_id ?? undefined,
+ user_properties: payload.user_properties,
+ send_to: payload.send_to == true ? settings.measurementID : 'default',
...payload.params
})
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/search/generated-types.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/search/generated-types.ts
index 94b8ec5941..686241fd58 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/search/generated-types.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/search/generated-types.ts
@@ -21,4 +21,8 @@ export interface Payload {
* The term that was searched for.
*/
search_term?: string
+ /**
+ * If the send_to parameter is not set, events are routed to all Tag Ids (AW-xxx, G-xxx) set via Google Tag
+ */
+ send_to?: boolean
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/search/index.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/search/index.ts
index caaa88163c..9a4ca89a71 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/search/index.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/search/index.ts
@@ -2,8 +2,7 @@ import type { BrowserActionDefinition } from '@segment/browser-destination-runti
import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'
-import { user_properties, params, user_id, search_term } from '../ga4-properties'
-import { updateUser } from '../ga4-functions'
+import { user_properties, params, user_id, search_term, send_to } from '../ga4-properties'
// Change from unknown to the partner SDK types
const action: BrowserActionDefinition = {
@@ -15,13 +14,15 @@ const action: BrowserActionDefinition = {
user_id: user_id,
user_properties: user_properties,
params: params,
- search_term: search_term
+ search_term: search_term,
+ send_to: send_to
},
- perform: (gtag, { payload }) => {
- updateUser(payload.user_id, payload.user_properties, gtag)
-
+ perform: (gtag, { payload, settings }) => {
gtag('event', 'search', {
search_term: payload.search_term,
+ user_id: payload.user_id ?? undefined,
+ user_properties: payload.user_properties,
+ send_to: payload.send_to == true ? settings.measurementID : 'default',
...payload.params
})
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/selectItem/generated-types.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/selectItem/generated-types.ts
index 32d9891f1e..70b44615db 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/selectItem/generated-types.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/selectItem/generated-types.ts
@@ -93,6 +93,7 @@ export interface Payload {
* Item quantity.
*/
quantity?: number
+ [k: string]: unknown
}[]
/**
* The user properties to send to Google Analytics 4. You must create user-scoped dimensions to ensure custom properties are picked up by Google. See Google’s [Custom user properties](https://support.google.com/analytics/answer/9269570) to learn how to set and register user properties.
@@ -106,4 +107,8 @@ export interface Payload {
params?: {
[k: string]: unknown
}
+ /**
+ * If the send_to parameter is not set, events are routed to all Tag Ids (AW-xxx, G-xxx) set via Google Tag
+ */
+ send_to?: boolean
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/selectItem/index.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/selectItem/index.ts
index d6c7f30004..a64c762435 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/selectItem/index.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/selectItem/index.ts
@@ -8,9 +8,9 @@ import {
user_id,
items_single_products,
item_list_name,
- item_list_id
+ item_list_id,
+ send_to
} from '../ga4-properties'
-import { updateUser } from '../ga4-functions'
const action: BrowserActionDefinition = {
title: 'Select Item',
@@ -26,15 +26,17 @@ const action: BrowserActionDefinition = {
required: true
},
user_properties: user_properties,
- params: params
+ params: params,
+ send_to: send_to
},
- perform: (gtag, { payload }) => {
- updateUser(payload.user_id, payload.user_properties, gtag)
-
+ perform: (gtag, { payload, settings }) => {
gtag('event', 'select_item', {
item_list_id: payload.item_list_id,
item_list_name: payload.item_list_name,
items: payload.items,
+ user_id: payload.user_id ?? undefined,
+ user_properties: payload.user_properties,
+ send_to: payload.send_to == true ? settings.measurementID : 'default',
...payload.params
})
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/selectPromotion/generated-types.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/selectPromotion/generated-types.ts
index a429b75c73..91725ada01 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/selectPromotion/generated-types.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/selectPromotion/generated-types.ts
@@ -121,6 +121,7 @@ export interface Payload {
* The ID of the promotion associated with the event.
*/
promotion_id?: string
+ [k: string]: unknown
}[]
/**
* The user properties to send to Google Analytics 4. You must create user-scoped dimensions to ensure custom properties are picked up by Google. See Google’s [Custom user properties](https://support.google.com/analytics/answer/9269570) to learn how to set and register user properties.
@@ -134,4 +135,8 @@ export interface Payload {
params?: {
[k: string]: unknown
}
+ /**
+ * If the send_to parameter is not set, events are routed to all Tag Ids (AW-xxx, G-xxx) set via Google Tag
+ */
+ send_to?: boolean
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/selectPromotion/index.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/selectPromotion/index.ts
index 9384484e34..28eee4036f 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/selectPromotion/index.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/selectPromotion/index.ts
@@ -12,10 +12,9 @@ import {
items_single_products,
params,
user_properties,
- location_id
+ location_id,
+ send_to
} from '../ga4-properties'
-import { updateUser } from '../ga4-functions'
-
const action: BrowserActionDefinition = {
title: 'Select Promotion',
description: 'This event signifies a promotion was selected from a list.',
@@ -47,11 +46,10 @@ const action: BrowserActionDefinition = {
}
},
user_properties: user_properties,
- params: params
+ params: params,
+ send_to: send_to
},
- perform: (gtag, { payload }) => {
- updateUser(payload.user_id, payload.user_properties, gtag)
-
+ perform: (gtag, { payload, settings }) => {
gtag('event', 'select_promotion', {
creative_name: payload.creative_name,
creative_slot: payload.creative_slot,
@@ -59,6 +57,9 @@ const action: BrowserActionDefinition = {
promotion_id: payload.promotion_id,
promotion_name: payload.promotion_name,
items: payload.items,
+ user_id: payload.user_id ?? undefined,
+ user_properties: payload.user_properties,
+ send_to: payload.send_to == true ? settings.measurementID : 'default',
...payload.params
})
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/setConfigurationFields/generated-types.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/setConfigurationFields/generated-types.ts
index bc813c4763..36d3f9f0a0 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/setConfigurationFields/generated-types.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/setConfigurationFields/generated-types.ts
@@ -19,6 +19,14 @@ export interface Payload {
* Consent state indicated by the user for ad cookies. Value must be “granted” or “denied.” This is only used if the Enable Consent Mode setting is on.
*/
analytics_storage_consent_state?: string
+ /**
+ * Consent state indicated by the user for ad cookies. Value must be "granted" or "denied." This is only used if the Enable Consent Mode setting is on.
+ */
+ ad_user_data_consent_state?: string
+ /**
+ * Consent state indicated by the user for ad cookies. Value must be "granted" or "denied." This is only used if the Enable Consent Mode setting is on.
+ */
+ ad_personalization_consent_state?: string
/**
* Use campaign content to differentiate ads or links that point to the same URL. Setting this value will override the utm_content query parameter.
*/
@@ -67,4 +75,14 @@ export interface Payload {
* The resolution of the screen. Format should be two positive integers separated by an x (i.e. 800x600). If not set, calculated from the user's window.screen value.
*/
screen_resolution?: string
+ /**
+ * Selection overrides toggled value set within Settings
+ */
+ send_page_view?: boolean
+ /**
+ * The event parameters to send to Google Analytics 4.
+ */
+ params?: {
+ [k: string]: unknown
+ }
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/setConfigurationFields/index.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/setConfigurationFields/index.ts
index 92c2c93766..bfc4451ceb 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/setConfigurationFields/index.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/setConfigurationFields/index.ts
@@ -1,17 +1,18 @@
import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'
-import { user_id, user_properties } from '../ga4-properties'
-import { updateUser } from '../ga4-functions'
-
+import { user_id, user_properties, params } from '../ga4-properties'
type ConsentParamsArg = 'granted' | 'denied' | undefined
+const defaultCookieExpiryInSecond = 63072000
+const defaultCookieDomain = 'auto'
// Change from unknown to the partner SDK types
const action: BrowserActionDefinition = {
title: 'Set Configuration Fields',
description: 'Set custom values for the GA4 configuration fields.',
platform: 'web',
- defaultSubscription: 'type = "identify" or type = "page"',
+ defaultSubscription: 'type = "page"',
+ lifecycleHook: 'before',
fields: {
user_id: user_id,
user_properties: user_properties,
@@ -27,6 +28,28 @@ const action: BrowserActionDefinition = {
label: 'Analytics Storage Consent State',
type: 'string'
},
+ ad_user_data_consent_state: {
+ description:
+ 'Consent state indicated by the user for ad cookies. Value must be "granted" or "denied." This is only used if the Enable Consent Mode setting is on.',
+ label: 'Ad User Data Consent State',
+ type: 'string',
+ choices: [
+ { label: 'Granted', value: 'granted' },
+ { label: 'Denied', value: 'denied' }
+ ],
+ default: undefined
+ },
+ ad_personalization_consent_state: {
+ description:
+ 'Consent state indicated by the user for ad cookies. Value must be "granted" or "denied." This is only used if the Enable Consent Mode setting is on.',
+ label: 'Ad Personalization Consent State',
+ type: 'string',
+ choices: [
+ { label: 'Granted', value: 'granted' },
+ { label: 'Denied', value: 'denied' }
+ ],
+ default: undefined
+ },
campaign_content: {
description:
'Use campaign content to differentiate ads or links that point to the same URL. Setting this value will override the utm_content query parameter.',
@@ -92,27 +115,73 @@ const action: BrowserActionDefinition = {
description: `The resolution of the screen. Format should be two positive integers separated by an x (i.e. 800x600). If not set, calculated from the user's window.screen value.`,
label: 'Screen Resolution',
type: 'string'
- }
+ },
+ send_page_view: {
+ description: 'Selection overrides toggled value set within Settings',
+ label: 'Send Page Views',
+ type: 'boolean',
+ choices: [
+ { label: 'True', value: 'true' },
+ { label: 'False', value: 'false' }
+ ],
+ default: true
+ },
+ params: params
},
perform: (gtag, { payload, settings }) => {
- updateUser(payload.user_id, payload.user_properties, gtag)
+ const checkCookiePathDefaultValue =
+ settings.cookiePath != undefined && settings.cookiePath?.length !== 1 && settings.cookiePath !== '/'
+
if (settings.enableConsentMode) {
- window.gtag('consent', 'update', {
- ad_storage: payload.ads_storage_consent_state as ConsentParamsArg,
- analytics_storage: payload.analytics_storage_consent_state as ConsentParamsArg
- })
+ const consentParams: {
+ ad_storage?: ConsentParamsArg
+ analytics_storage?: ConsentParamsArg
+ ad_user_data?: ConsentParamsArg
+ ad_personalization?: ConsentParamsArg
+ } = {}
+ if (payload.ads_storage_consent_state) {
+ consentParams.ad_storage = payload.ads_storage_consent_state as ConsentParamsArg
+ }
+ if (payload.analytics_storage_consent_state) {
+ consentParams.analytics_storage = payload.analytics_storage_consent_state as ConsentParamsArg
+ }
+ if (payload.ad_user_data_consent_state) {
+ consentParams.ad_user_data = payload.ad_user_data_consent_state as ConsentParamsArg
+ }
+ if (payload.ad_personalization_consent_state) {
+ consentParams.ad_personalization = payload.ad_personalization_consent_state as ConsentParamsArg
+ }
+ gtag('consent', 'update', consentParams)
}
type ConfigType = { [key: string]: unknown }
const config: ConfigType = {
- send_page_view: settings.pageView ?? true,
- cookie_update: settings.cookieUpdate,
- cookie_domain: settings.cookieDomain,
- cookie_prefix: settings.cookiePrefix,
- cookie_expires: settings.cookieExpirationInSeconds,
- cookie_path: settings.cookiePath,
allow_ad_personalization_signals: settings.allowAdPersonalizationSignals,
- allow_google_signals: settings.allowGoogleSignals
+ allow_google_signals: settings.allowGoogleSignals,
+ ...payload.params
+ }
+
+ if (settings.cookieUpdate != true) {
+ config.cookie_update = false
+ }
+ if (settings.cookieDomain != defaultCookieDomain) {
+ config.cookie_domain = settings.cookieDomain
+ }
+ if (settings.cookiePrefix) {
+ config.cookie_prefix = settings.cookiePrefix
+ }
+ if (settings.cookieExpirationInSeconds != defaultCookieExpiryInSecond) {
+ config.cookie_expires = settings.cookieExpirationInSeconds
+ }
+ if (checkCookiePathDefaultValue) {
+ config.cookie_path = settings.cookiePath
+ }
+
+ if (payload.send_page_view != true || settings.pageView != true) {
+ config.send_page_view = payload.send_page_view ?? settings.pageView ?? true
+ }
+ if (settings.cookieFlags) {
+ config.cookie_flags = settings.cookieFlags
}
if (payload.screen_resolution) {
@@ -157,6 +226,7 @@ const action: BrowserActionDefinition = {
if (payload.campaign_content) {
config.campaign_content = payload.campaign_content
}
+
gtag('config', settings.measurementID, config)
}
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/signUp/generated-types.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/signUp/generated-types.ts
index c305ce6764..e1e758672f 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/signUp/generated-types.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/signUp/generated-types.ts
@@ -21,4 +21,8 @@ export interface Payload {
params?: {
[k: string]: unknown
}
+ /**
+ * If the send_to parameter is not set, events are routed to all Tag Ids (AW-xxx, G-xxx) set via Google Tag
+ */
+ send_to?: boolean
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/signUp/index.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/signUp/index.ts
index eefae9248f..6e777976b0 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/signUp/index.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/signUp/index.ts
@@ -2,8 +2,7 @@ import type { BrowserActionDefinition } from '@segment/browser-destination-runti
import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'
-import { user_properties, params, user_id, method } from '../ga4-properties'
-import { updateUser } from '../ga4-functions'
+import { user_properties, params, user_id, method, send_to } from '../ga4-properties'
const action: BrowserActionDefinition = {
title: 'Sign Up',
@@ -14,13 +13,15 @@ const action: BrowserActionDefinition = {
user_id: user_id,
method: method,
user_properties: user_properties,
- params: params
+ params: params,
+ send_to: send_to
},
- perform: (gtag, { payload }) => {
- updateUser(payload.user_id, payload.user_properties, gtag)
-
+ perform: (gtag, { payload, settings }) => {
gtag('event', 'sign_up', {
method: payload.method,
+ user_id: payload.user_id ?? undefined,
+ user_properties: payload.user_properties,
+ send_to: payload.send_to == true ? settings.measurementID : 'default',
...payload.params
})
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/viewCart/generated-types.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/viewCart/generated-types.ts
index a7f5a2e20e..b6d8985b27 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/viewCart/generated-types.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/viewCart/generated-types.ts
@@ -93,6 +93,7 @@ export interface Payload {
* Item quantity.
*/
quantity?: number
+ [k: string]: unknown
}[]
/**
* The user properties to send to Google Analytics 4. You must create user-scoped dimensions to ensure custom properties are picked up by Google. See Google’s [Custom user properties](https://support.google.com/analytics/answer/9269570) to learn how to set and register user properties.
@@ -106,4 +107,8 @@ export interface Payload {
params?: {
[k: string]: unknown
}
+ /**
+ * If the send_to parameter is not set, events are routed to all Tag Ids (AW-xxx, G-xxx) set via Google Tag
+ */
+ send_to?: boolean
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/viewCart/index.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/viewCart/index.ts
index 5419eda24b..63c366cf30 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/viewCart/index.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/viewCart/index.ts
@@ -2,8 +2,7 @@ import type { BrowserActionDefinition } from '@segment/browser-destination-runti
import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'
-import { user_properties, params, currency, value, user_id, items_multi_products } from '../ga4-properties'
-import { updateUser } from '../ga4-functions'
+import { user_properties, params, currency, value, user_id, items_multi_products, send_to } from '../ga4-properties'
const action: BrowserActionDefinition = {
title: 'View Cart',
@@ -19,15 +18,17 @@ const action: BrowserActionDefinition = {
required: true
},
user_properties: user_properties,
- params: params
+ params: params,
+ send_to: send_to
},
- perform: (gtag, { payload }) => {
- updateUser(payload.user_id, payload.user_properties, gtag)
-
+ perform: (gtag, { payload, settings }) => {
gtag('event', 'view_cart', {
currency: payload.currency,
value: payload.value,
items: payload.items,
+ user_id: payload.user_id ?? undefined,
+ user_properties: payload.user_properties,
+ send_to: payload.send_to == true ? settings.measurementID : 'default',
...payload.params
})
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/viewItem/generated-types.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/viewItem/generated-types.ts
index a7f5a2e20e..b6d8985b27 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/viewItem/generated-types.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/viewItem/generated-types.ts
@@ -93,6 +93,7 @@ export interface Payload {
* Item quantity.
*/
quantity?: number
+ [k: string]: unknown
}[]
/**
* The user properties to send to Google Analytics 4. You must create user-scoped dimensions to ensure custom properties are picked up by Google. See Google’s [Custom user properties](https://support.google.com/analytics/answer/9269570) to learn how to set and register user properties.
@@ -106,4 +107,8 @@ export interface Payload {
params?: {
[k: string]: unknown
}
+ /**
+ * If the send_to parameter is not set, events are routed to all Tag Ids (AW-xxx, G-xxx) set via Google Tag
+ */
+ send_to?: boolean
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/viewItem/index.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/viewItem/index.ts
index 7bdc4bb6c3..1a23e19801 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/viewItem/index.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/viewItem/index.ts
@@ -2,8 +2,7 @@ import type { BrowserActionDefinition } from '@segment/browser-destination-runti
import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'
-import { user_properties, params, currency, user_id, value, items_single_products } from '../ga4-properties'
-import { updateUser } from '../ga4-functions'
+import { user_properties, params, currency, user_id, value, items_single_products, send_to } from '../ga4-properties'
const action: BrowserActionDefinition = {
title: 'View Item',
@@ -20,15 +19,17 @@ const action: BrowserActionDefinition = {
required: true
},
user_properties: user_properties,
- params: params
+ params: params,
+ send_to: send_to
},
- perform: (gtag, { payload }) => {
- updateUser(payload.user_id, payload.user_properties, gtag)
-
+ perform: (gtag, { payload, settings }) => {
gtag('event', 'view_item', {
currency: payload.currency,
value: payload.value,
items: payload.items,
+ user_id: payload.user_id ?? undefined,
+ user_properties: payload.user_properties,
+ send_to: payload.send_to == true ? settings.measurementID : 'default',
...payload.params
})
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/viewItemList/generated-types.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/viewItemList/generated-types.ts
index ad463886d4..c92981ba95 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/viewItemList/generated-types.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/viewItemList/generated-types.ts
@@ -93,6 +93,7 @@ export interface Payload {
* Item quantity.
*/
quantity?: number
+ [k: string]: unknown
}[]
/**
* The user properties to send to Google Analytics 4. You must create user-scoped dimensions to ensure custom properties are picked up by Google. See Google’s [Custom user properties](https://support.google.com/analytics/answer/9269570) to learn how to set and register user properties.
@@ -106,4 +107,8 @@ export interface Payload {
params?: {
[k: string]: unknown
}
+ /**
+ * If the send_to parameter is not set, events are routed to all Tag Ids (AW-xxx, G-xxx) set via Google Tag
+ */
+ send_to?: boolean
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/viewItemList/index.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/viewItemList/index.ts
index 6874780bb8..4e994d5110 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/viewItemList/index.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/viewItemList/index.ts
@@ -2,8 +2,15 @@ import type { BrowserActionDefinition } from '@segment/browser-destination-runti
import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'
-import { user_properties, params, user_id, items_multi_products, item_list_name, item_list_id } from '../ga4-properties'
-import { updateUser } from '../ga4-functions'
+import {
+ user_properties,
+ params,
+ user_id,
+ items_multi_products,
+ item_list_name,
+ item_list_id,
+ send_to
+} from '../ga4-properties'
const action: BrowserActionDefinition = {
title: 'View Item List',
@@ -19,15 +26,17 @@ const action: BrowserActionDefinition = {
required: true
},
user_properties: user_properties,
- params: params
+ params: params,
+ send_to: send_to
},
- perform: (gtag, { payload }) => {
- updateUser(payload.user_id, payload.user_properties, gtag)
-
+ perform: (gtag, { payload, settings }) => {
gtag('event', 'view_item_list', {
item_list_id: payload.item_list_id,
item_list_name: payload.item_list_name,
items: payload.items,
+ user_id: payload.user_id ?? undefined,
+ user_properties: payload.user_properties,
+ send_to: payload.send_to == true ? settings.measurementID : 'default',
...payload.params
})
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/viewPromotion/generated-types.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/viewPromotion/generated-types.ts
index a08bd958e6..826cba82d2 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/viewPromotion/generated-types.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/viewPromotion/generated-types.ts
@@ -121,6 +121,7 @@ export interface Payload {
* The ID of the promotion associated with the event.
*/
promotion_id?: string
+ [k: string]: unknown
}[]
/**
* The user properties to send to Google Analytics 4. You must create user-scoped dimensions to ensure custom properties are picked up by Google. See Google’s [Custom user properties](https://support.google.com/analytics/answer/9269570) to learn how to set and register user properties.
@@ -134,4 +135,8 @@ export interface Payload {
params?: {
[k: string]: unknown
}
+ /**
+ * If the send_to parameter is not set, events are routed to all Tag Ids (AW-xxx, G-xxx) set via Google Tag
+ */
+ send_to?: boolean
}
diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/src/viewPromotion/index.ts b/packages/browser-destinations/destinations/google-analytics-4-web/src/viewPromotion/index.ts
index 7d1d49c4fb..539d564fd5 100644
--- a/packages/browser-destinations/destinations/google-analytics-4-web/src/viewPromotion/index.ts
+++ b/packages/browser-destinations/destinations/google-analytics-4-web/src/viewPromotion/index.ts
@@ -11,9 +11,9 @@ import {
items_single_products,
params,
user_properties,
- location_id
+ location_id,
+ send_to
} from '../ga4-properties'
-import { updateUser } from '../ga4-functions'
const action: BrowserActionDefinition = {
title: 'View Promotion',
@@ -47,11 +47,10 @@ const action: BrowserActionDefinition = {
}
},
user_properties: user_properties,
- params: params
+ params: params,
+ send_to: send_to
},
- perform: (gtag, { payload }) => {
- updateUser(payload.user_id, payload.user_properties, gtag)
-
+ perform: (gtag, { payload, settings }) => {
gtag('event', 'view_promotion', {
creative_name: payload.creative_name,
creative_slot: payload.creative_slot,
@@ -59,6 +58,9 @@ const action: BrowserActionDefinition = {
promotion_id: payload.promotion_id,
promotion_name: payload.promotion_name,
items: payload.items,
+ user_id: payload.user_id ?? undefined,
+ user_properties: payload.user_properties,
+ send_to: payload.send_to == true ? settings.measurementID : 'default',
...payload.params
})
}
diff --git a/packages/browser-destinations/destinations/google-campaign-manager/README.md b/packages/browser-destinations/destinations/google-campaign-manager/README.md
index 35e5069d7d..b6cd3877f9 100644
--- a/packages/browser-destinations/destinations/google-campaign-manager/README.md
+++ b/packages/browser-destinations/destinations/google-campaign-manager/README.md
@@ -6,7 +6,7 @@ The Google Campaign Manager browser action destination for use with @segment/ana
MIT License
-Copyright (c) 2023 Segment
+Copyright (c) 2024 Segment
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/packages/browser-destinations/destinations/google-campaign-manager/package.json b/packages/browser-destinations/destinations/google-campaign-manager/package.json
index fb15e83df2..6701b66dd4 100644
--- a/packages/browser-destinations/destinations/google-campaign-manager/package.json
+++ b/packages/browser-destinations/destinations/google-campaign-manager/package.json
@@ -1,6 +1,6 @@
{
"name": "@segment/analytics-browser-actions-google-campaign-manager",
- "version": "1.5.0",
+ "version": "1.24.0",
"license": "MIT",
"publishConfig": {
"access": "public",
@@ -15,7 +15,7 @@
},
"typings": "./dist/esm",
"dependencies": {
- "@segment/browser-destination-runtime": "^1.14.0"
+ "@segment/browser-destination-runtime": "^1.33.0"
},
"peerDependencies": {
"@segment/analytics-next": ">=1.55.0"
diff --git a/packages/browser-destinations/destinations/heap/package.json b/packages/browser-destinations/destinations/heap/package.json
index b0a57ed43d..9ced226fc1 100644
--- a/packages/browser-destinations/destinations/heap/package.json
+++ b/packages/browser-destinations/destinations/heap/package.json
@@ -1,6 +1,6 @@
{
"name": "@segment/analytics-browser-actions-heap",
- "version": "1.15.0",
+ "version": "1.34.0",
"license": "MIT",
"publishConfig": {
"access": "public",
@@ -15,8 +15,8 @@
},
"typings": "./dist/esm",
"dependencies": {
- "@segment/actions-core": "^3.83.0",
- "@segment/browser-destination-runtime": "^1.14.0"
+ "@segment/actions-core": "^3.103.0",
+ "@segment/browser-destination-runtime": "^1.33.0"
},
"peerDependencies": {
"@segment/analytics-next": ">=1.55.0"
diff --git a/packages/browser-destinations/destinations/heap/src/trackEvent/__tests__/index.test.ts b/packages/browser-destinations/destinations/heap/src/trackEvent/__tests__/index.test.ts
index f680a5c2d9..84726972ad 100644
--- a/packages/browser-destinations/destinations/heap/src/trackEvent/__tests__/index.test.ts
+++ b/packages/browser-destinations/destinations/heap/src/trackEvent/__tests__/index.test.ts
@@ -42,76 +42,58 @@ describe('#trackEvent', () => {
type: 'track',
name: 'hello!',
properties: {
- banana: '📞',
- apple: [
+ products: [
{
- carrot: 12,
- broccoli: [
- {
- onion: 'crisp',
- tomato: 'fruit'
+ name: 'Test Product 1',
+ properties: {
+ color: 'red',
+ qty: 2,
+ custom_vars: {
+ position: 0,
+ something_else: 'test',
+ another_one: ['one', 'two', 'three']
}
- ]
+ }
},
{
- carrot: 21,
- broccoli: [
- {
- tomato: 'vegetable'
- },
- {
- tomato: 'fruit'
- },
- [
- {
- pickle: 'vinegar'
- },
- {
- pie: 3.1415
- }
- ]
- ]
+ name: 'Test Product 2',
+ properties: {
+ color: 'blue',
+ qty: 1,
+ custom_vars: {
+ position: 1,
+ something_else: 'blah',
+ another_one: ['four', 'five', 'six']
+ }
+ }
}
- ],
- emptyArray: [],
- float: 1.2345,
- booleanTrue: true,
- booleanFalse: false,
- nullValue: null,
- undefinedValue: undefined
+ ]
}
})
)
expect(heapTrackSpy).toHaveBeenCalledTimes(3)
- expect(heapTrackSpy).toHaveBeenNthCalledWith(1, 'hello! apple item', {
- carrot: 12,
- 'broccoli.0.onion': 'crisp',
- 'broccoli.0.tomato': 'fruit',
+ expect(heapTrackSpy).toHaveBeenNthCalledWith(1, 'hello! products item', {
+ name: 'Test Product 1',
+ color: 'red',
+ qty: '2',
+ 'custom_vars.position': '0',
+ 'custom_vars.something_else': 'test',
+ 'custom_vars.another_one': '["one","two","three"]',
segment_library: HEAP_SEGMENT_BROWSER_LIBRARY_NAME
})
- expect(heapTrackSpy).toHaveBeenNthCalledWith(2, 'hello! apple item', {
- carrot: 21,
- 'broccoli.0.tomato': 'vegetable',
- 'broccoli.1.tomato': 'fruit',
- 'broccoli.2.0.pickle': 'vinegar',
- 'broccoli.2.1.pie': '3.1415',
+ expect(heapTrackSpy).toHaveBeenNthCalledWith(2, 'hello! products item', {
+ name: 'Test Product 2',
+ color: 'blue',
+ qty: '1',
+ 'custom_vars.position': '1',
+ 'custom_vars.something_else': 'blah',
+ 'custom_vars.another_one': '["four","five","six"]',
segment_library: HEAP_SEGMENT_BROWSER_LIBRARY_NAME
})
expect(heapTrackSpy).toHaveBeenNthCalledWith(3, 'hello!', {
- banana: '📞',
- float: 1.2345,
- booleanTrue: true,
- booleanFalse: false,
- nullValue: null,
- segment_library: HEAP_SEGMENT_BROWSER_LIBRARY_NAME,
- 'apple.0.broccoli.0.onion': 'crisp',
- 'apple.0.broccoli.0.tomato': 'fruit',
- 'apple.0.carrot': '12',
- 'apple.1.broccoli.0.tomato': 'vegetable',
- 'apple.1.broccoli.1.tomato': 'fruit',
- 'apple.1.broccoli.2.0.pickle': 'vinegar',
- 'apple.1.broccoli.2.1.pie': '3.1415',
- 'apple.1.carrot': '21'
+ products:
+ '[{"name":"Test Product 1","properties":{"color":"red","qty":2,"custom_vars":{"position":0,"something_else":"test","another_one":["one","two","three"]}}},{"name":"Test Product 2","properties":{"color":"blue","qty":1,"custom_vars":{"position":1,"something_else":"blah","another_one":["four","five","six"]}}}]',
+ segment_library: HEAP_SEGMENT_BROWSER_LIBRARY_NAME
})
expect(addUserPropertiesSpy).toHaveBeenCalledTimes(0)
expect(identifySpy).toHaveBeenCalledTimes(0)
@@ -144,12 +126,8 @@ describe('#trackEvent', () => {
}
expect(heapTrackSpy).toHaveBeenNthCalledWith(6, 'hello!', {
segment_library: HEAP_SEGMENT_BROWSER_LIBRARY_NAME,
- 'testArray1.0.val': '1',
- 'testArray1.1.val': '2',
- 'testArray1.2.val': '3',
- 'testArray2.0.val': '4',
- 'testArray2.1.val': '5',
- 'testArray2.2.val': 'N/A'
+ testArray1: '[{"val":1},{"val":2},{"val":3}]',
+ testArray2: '[{"val":4},{"val":5},{"val":"N/A"}]'
})
})
@@ -167,12 +145,62 @@ describe('#trackEvent', () => {
expect(heapTrackSpy).toHaveBeenCalledTimes(1)
expect(heapTrackSpy).toHaveBeenCalledWith('hello!', {
- testArray1: [{ val: 1 }, { val: 2 }, { val: 3 }],
- testArray2: [{ val: 4 }, { val: 5 }, { val: 'N/A' }],
+ testArray1: '[{"val":1},{"val":2},{"val":3}]',
+ testArray2: '[{"val":4},{"val":5},{"val":"N/A"}]',
segment_library: HEAP_SEGMENT_BROWSER_LIBRARY_NAME
})
})
+ it('should stringify array', async () => {
+ await event.track?.(
+ new Context({
+ type: 'track',
+ name: 'hello!',
+ properties: {
+ testArray1: ['test', 'testing', 'tester']
+ }
+ })
+ )
+ expect(heapTrackSpy).toHaveBeenCalledTimes(1)
+
+ expect(heapTrackSpy).toHaveBeenCalledWith('hello!', {
+ testArray1: '["test","testing","tester"]',
+ segment_library: HEAP_SEGMENT_BROWSER_LIBRARY_NAME
+ })
+ })
+
+ it('should flatten properties', async () => {
+ await event.track?.(
+ new Context({
+ type: 'track',
+ name: 'hello!',
+ properties: {
+ isAutomated: true,
+ isClickable: true,
+ custom_vars: {
+ bodyText: 'Testing text',
+ ctaText: 'Click me',
+ position: 0,
+ testNestedValues: {
+ count: 5,
+ color: 'green'
+ }
+ }
+ }
+ })
+ )
+ expect(heapTrackSpy).toHaveBeenCalledWith('hello!', {
+ segment_library: HEAP_SEGMENT_BROWSER_LIBRARY_NAME,
+ isAutomated: true,
+ isClickable: true,
+ bodyText: 'Testing text',
+ ctaText: 'Click me',
+ position: '0',
+ 'testNestedValues.count': '5',
+ 'testNestedValues.color': 'green'
+ })
+ })
+
it('should send segment_library property if no other properties were provided', async () => {
await event.track?.(
new Context({
@@ -238,4 +266,92 @@ describe('#trackEvent', () => {
})
expect(identifySpy).toHaveBeenCalledTimes(0)
})
+
+ describe('data tests', () => {
+ it('should unroll and flatten', async () => {
+ await eventWithUnrolling.track?.(
+ new Context({
+ type: 'track',
+ name: 'Product List Viewed',
+ properties: {
+ membership_status: 'lead',
+ products: [
+ {
+ sku: 'PT2252152-0001-00',
+ url: '/products/THE-ONE-JOGGER-PT2252152-0001-2',
+ variant: 'Black',
+ vip_price: 59.95,
+ membership_brand_id: 1,
+ quantity: 1
+ },
+ {
+ sku: 'PT2252152-4846-00',
+ url: '/products/THE-ONE-JOGGER-PT2252152-4846',
+ variant: 'Deep Navy',
+ vip_price: 59.95,
+ membership_brand_id: 1,
+ quantity: 1
+ },
+ {
+ sku: 'PT2458220-0001-00',
+ url: '/products/THE-YEAR-ROUND-TERRY-JOGGER-PT2458220-0001',
+ variant: 'Black',
+ vip_price: 59.95,
+ membership_brand_id: 1,
+ quantity: 1
+ }
+ ],
+ store_group_id: '16',
+ session_id: '14322962105',
+ user_status_initial: 'lead',
+ utm_campaign: null,
+ utm_medium: null,
+ utm_source: null,
+ customer_id: '864832720'
+ }
+ })
+ )
+ expect(heapTrackSpy).toHaveBeenCalledTimes(4)
+ expect(heapTrackSpy).toHaveBeenNthCalledWith(1, 'Product List Viewed products item', {
+ sku: 'PT2252152-0001-00',
+ url: '/products/THE-ONE-JOGGER-PT2252152-0001-2',
+ variant: 'Black',
+ vip_price: 59.95,
+ membership_brand_id: 1,
+ quantity: 1,
+ segment_library: HEAP_SEGMENT_BROWSER_LIBRARY_NAME
+ })
+ expect(heapTrackSpy).toHaveBeenNthCalledWith(2, 'Product List Viewed products item', {
+ sku: 'PT2252152-4846-00',
+ url: '/products/THE-ONE-JOGGER-PT2252152-4846',
+ variant: 'Deep Navy',
+ vip_price: 59.95,
+ membership_brand_id: 1,
+ quantity: 1,
+ segment_library: HEAP_SEGMENT_BROWSER_LIBRARY_NAME
+ })
+ expect(heapTrackSpy).toHaveBeenNthCalledWith(3, 'Product List Viewed products item', {
+ sku: 'PT2458220-0001-00',
+ url: '/products/THE-YEAR-ROUND-TERRY-JOGGER-PT2458220-0001',
+ variant: 'Black',
+ vip_price: 59.95,
+ membership_brand_id: 1,
+ quantity: 1,
+ segment_library: HEAP_SEGMENT_BROWSER_LIBRARY_NAME
+ })
+ expect(heapTrackSpy).toHaveBeenNthCalledWith(4, 'Product List Viewed', {
+ membership_status: 'lead',
+ products:
+ '[{"sku":"PT2252152-0001-00","url":"/products/THE-ONE-JOGGER-PT2252152-0001-2","variant":"Black","vip_price":59.95,"membership_brand_id":1,"quantity":1},{"sku":"PT2252152-4846-00","url":"/products/THE-ONE-JOGGER-PT2252152-4846","variant":"Deep Navy","vip_price":59.95,"membership_brand_id":1,"quantity":1},{"sku":"PT2458220-0001-00","url":"/products/THE-YEAR-ROUND-TERRY-JOGGER-PT2458220-0001","variant":"Black","vip_price":59.95,"membership_brand_id":1,"quantity":1}]',
+ store_group_id: '16',
+ session_id: '14322962105',
+ user_status_initial: 'lead',
+ utm_campaign: null,
+ utm_medium: null,
+ utm_source: null,
+ customer_id: '864832720',
+ segment_library: HEAP_SEGMENT_BROWSER_LIBRARY_NAME
+ })
+ })
+ })
})
diff --git a/packages/browser-destinations/destinations/heap/src/trackEvent/index.ts b/packages/browser-destinations/destinations/heap/src/trackEvent/index.ts
index 5096e82d08..a81255bed1 100644
--- a/packages/browser-destinations/destinations/heap/src/trackEvent/index.ts
+++ b/packages/browser-destinations/destinations/heap/src/trackEvent/index.ts
@@ -78,6 +78,8 @@ const action: BrowserActionDefinition = {
if (browserArrayLimitSet) {
eventProperties = heapTrackArrays(heap, eventName, eventProperties, browserArrayLimit)
+ } else {
+ eventProperties = flattenProperties(eventProperties)
}
heapTrack(heap, eventName, eventProperties)
diff --git a/packages/browser-destinations/destinations/heap/src/utils.ts b/packages/browser-destinations/destinations/heap/src/utils.ts
index f19a1c039b..6b021fb841 100644
--- a/packages/browser-destinations/destinations/heap/src/utils.ts
+++ b/packages/browser-destinations/destinations/heap/src/utils.ts
@@ -19,12 +19,14 @@ export function flat(data?: Properties, prefix = ''): FlattenProperties | undefi
}
let result: FlattenProperties = {}
for (const key in data) {
- if (typeof data[key] == 'object' && data[key] !== null) {
+ if (typeof data[key] == 'object' && data[key] !== null && !Array.isArray(data[key])) {
const flatten = flat(data[key] as Properties, prefix + '.' + key)
result = { ...result, ...flatten }
} else {
const stringifiedValue = stringify(data[key])
- result[(prefix + '.' + key).replace(/^\./, '')] = stringifiedValue
+ // replaces the first . or .word.
+ const identifier = (prefix + '.' + key).replace(/^\.(\w+\.)?/, '')
+ result[identifier] = stringifiedValue
}
}
return result
diff --git a/packages/browser-destinations/destinations/hubble-web/README.md b/packages/browser-destinations/destinations/hubble-web/README.md
new file mode 100644
index 0000000000..b4884b277e
--- /dev/null
+++ b/packages/browser-destinations/destinations/hubble-web/README.md
@@ -0,0 +1,31 @@
+# @segment/analytics-browser-hubble-web
+
+The Hubble (actions) browser action destination for use with @segment/analytics-next.
+
+## License
+
+MIT License
+
+Copyright (c) 2024 Segment
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+## Contributing
+
+All third party contributors acknowledge that any contributions they provide will be made under the same open source license that the open source project is provided under.
diff --git a/packages/browser-destinations/destinations/hubble-web/package.json b/packages/browser-destinations/destinations/hubble-web/package.json
new file mode 100644
index 0000000000..ba70f9c598
--- /dev/null
+++ b/packages/browser-destinations/destinations/hubble-web/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "@segment/analytics-browser-hubble-web",
+ "version": "1.20.0",
+ "license": "MIT",
+ "publishConfig": {
+ "access": "public",
+ "registry": "https://registry.npmjs.org"
+ },
+ "main": "./dist/cjs",
+ "module": "./dist/esm",
+ "scripts": {
+ "build": "yarn build:esm && yarn build:cjs",
+ "build:cjs": "tsc --module commonjs --outDir ./dist/cjs",
+ "build:esm": "tsc --outDir ./dist/esm"
+ },
+ "typings": "./dist/esm",
+ "dependencies": {
+ "@segment/actions-core": "^3.103.0",
+ "@segment/browser-destination-runtime": "^1.33.0"
+ },
+ "peerDependencies": {
+ "@segment/analytics-next": ">=1.55.0"
+ }
+}
diff --git a/packages/browser-destinations/destinations/hubble-web/src/__tests__/__snapshots__/index.test.ts.snap b/packages/browser-destinations/destinations/hubble-web/src/__tests__/__snapshots__/index.test.ts.snap
new file mode 100644
index 0000000000..ec0642738e
--- /dev/null
+++ b/packages/browser-destinations/destinations/hubble-web/src/__tests__/__snapshots__/index.test.ts.snap
@@ -0,0 +1,16 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Hubble load Hubble SDK:
+
+ 1`] = `
+NodeList [
+ ,
+]
+`;
diff --git a/packages/browser-destinations/destinations/hubble-web/src/__tests__/index.test.ts b/packages/browser-destinations/destinations/hubble-web/src/__tests__/index.test.ts
new file mode 100644
index 0000000000..ebd9421846
--- /dev/null
+++ b/packages/browser-destinations/destinations/hubble-web/src/__tests__/index.test.ts
@@ -0,0 +1,101 @@
+import { Subscription } from '@segment/browser-destination-runtime/types'
+import { Analytics, Context } from '@segment/analytics-next'
+import hubbleDestination, { destination } from '../index'
+
+const subscriptions: Subscription[] = [
+ {
+ name: 'Identify user',
+ subscribe: 'type = "identify"',
+ partnerAction: 'identify',
+ enabled: true,
+ mapping: {
+ userId: {
+ type: 'string',
+ required: true,
+ label: 'User ID',
+ description: 'Unique user ID',
+ default: {
+ '@path': '$.userId'
+ }
+ },
+ anonymousId: {
+ type: 'string',
+ required: false,
+ description: 'Anonymous id of the user',
+ label: 'Anonymous ID',
+ default: {
+ '@path': '$.anonymousId'
+ }
+ },
+ attributes: {
+ type: 'object',
+ required: false,
+ description: 'User traits used to enrich user identification',
+ label: 'Traits',
+ default: {
+ '@path': '$.traits'
+ }
+ }
+ }
+ },
+ {
+ name: 'Track event',
+ subscribe: 'type = "track"',
+ partnerAction: 'track',
+ enabled: true,
+ mapping: {
+ event: {
+ description: 'Event to be tracked',
+ label: 'Event',
+ required: true,
+ type: 'string',
+ default: {
+ '@path': '$.event'
+ }
+ },
+ attributes: {
+ description: 'Object containing the attributes (properties) of the event',
+ type: 'object',
+ required: false,
+ label: 'Event Attributes',
+ default: {
+ '@path': '$.properties'
+ }
+ }
+ }
+ }
+]
+
+describe('Hubble', () => {
+ beforeAll(() => {
+ jest.mock('@segment/browser-destination-runtime/load-script', () => ({
+ loadScript: (_src: any, _attributes: any) => {}
+ }))
+ jest.mock('@segment/browser-destination-runtime/resolve-when', () => ({
+ resolveWhen: (_fn: any, _timeout: any) => {}
+ }))
+ })
+
+ const testID = 'testId'
+
+ test('load Hubble SDK', async () => {
+ const [event] = await hubbleDestination({
+ id: testID,
+ subscriptions
+ })
+
+ jest.spyOn(destination, 'initialize')
+ await event.load(Context.system(), {} as Analytics)
+ expect(destination.initialize).toHaveBeenCalled()
+
+ const scripts = window.document.querySelectorAll('script')
+ expect(scripts).toMatchSnapshot(`
+
+ `)
+ })
+})
diff --git a/packages/browser-destinations/destinations/hubble-web/src/generated-types.ts b/packages/browser-destinations/destinations/hubble-web/src/generated-types.ts
new file mode 100644
index 0000000000..eef1f71582
--- /dev/null
+++ b/packages/browser-destinations/destinations/hubble-web/src/generated-types.ts
@@ -0,0 +1,8 @@
+// Generated file. DO NOT MODIFY IT BY HAND.
+
+export interface Settings {
+ /**
+ * Unique identifier for your team (given in Hubble app)
+ */
+ id: string
+}
diff --git a/packages/browser-destinations/destinations/hubble-web/src/identify/__tests__/index.test.ts b/packages/browser-destinations/destinations/hubble-web/src/identify/__tests__/index.test.ts
new file mode 100644
index 0000000000..f7cfa89e93
--- /dev/null
+++ b/packages/browser-destinations/destinations/hubble-web/src/identify/__tests__/index.test.ts
@@ -0,0 +1,92 @@
+import { Analytics, Context } from '@segment/analytics-next'
+import hubbleDestination, { destination } from '../../index'
+import { Subscription } from '@segment/browser-destination-runtime/types'
+
+const subscriptions: Subscription[] = [
+ {
+ partnerAction: 'identify',
+ name: 'Identify User',
+ enabled: true,
+ subscribe: 'type = "identify"',
+ mapping: {
+ anonymousId: {
+ '@path': '$.anonymousId'
+ },
+ userId: {
+ '@path': '$.userId'
+ },
+ attributes: {
+ '@path': '$.traits'
+ }
+ }
+ }
+]
+
+describe('identify', () => {
+ beforeAll(() => {
+ jest.mock('@segment/browser-destination-runtime/load-script', () => ({
+ loadScript: (_src: any, _attributes: any) => {}
+ }))
+ jest.mock('@segment/browser-destination-runtime/resolve-when', () => ({
+ resolveWhen: (_fn: any, _timeout: any) => {}
+ }))
+ })
+
+ let identify: any
+ const mockIdentify: jest.Mock = jest.fn()
+
+ beforeEach(async () => {
+ const [hubbleIdentify] = await hubbleDestination({
+ id: 'testID',
+ subscriptions
+ })
+
+ identify = hubbleIdentify
+
+ jest.spyOn(destination, 'initialize').mockImplementation(() => {
+ const mockedWithTrack = {
+ id: 'testID',
+ initialized: true,
+ emitter: { setSource: jest.fn() },
+ track: mockIdentify,
+ identify: jest.fn(),
+ setSource: jest.fn()
+ }
+ return Promise.resolve(mockedWithTrack)
+ })
+ await identify.load(Context.system(), {} as Analytics)
+ })
+
+ test('it maps event parameters correctly to identify function ', async () => {
+ jest.spyOn(destination.actions.identify, 'perform')
+ await identify.load(Context.system(), {} as Analytics)
+
+ await identify.identify?.(
+ new Context({
+ type: 'identify',
+ anonymousId: 'anon-123',
+ userId: 'some-user-123',
+ traits: {
+ someNumber: 123,
+ hello: 'world',
+ email: 'this_email@hubble.team'
+ }
+ })
+ )
+
+ expect(destination.actions.identify.perform).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({
+ payload: {
+ anonymousId: 'anon-123',
+ userId: 'some-user-123',
+ attributes: {
+ someNumber: 123,
+ hello: 'world',
+ email: 'this_email@hubble.team'
+ }
+ }
+ })
+ )
+ })
+})
diff --git a/packages/browser-destinations/destinations/hubble-web/src/identify/generated-types.ts b/packages/browser-destinations/destinations/hubble-web/src/identify/generated-types.ts
new file mode 100644
index 0000000000..a3a0d93825
--- /dev/null
+++ b/packages/browser-destinations/destinations/hubble-web/src/identify/generated-types.ts
@@ -0,0 +1,18 @@
+// Generated file. DO NOT MODIFY IT BY HAND.
+
+export interface Payload {
+ /**
+ * Unique identifer of the user
+ */
+ userId?: string
+ /**
+ * Anonymous identifier of the user
+ */
+ anonymousId?: string
+ /**
+ * User traits used to enrich user identification
+ */
+ attributes?: {
+ [k: string]: unknown
+ }
+}
diff --git a/packages/browser-destinations/destinations/hubble-web/src/identify/index.ts b/packages/browser-destinations/destinations/hubble-web/src/identify/index.ts
new file mode 100644
index 0000000000..8122a46a17
--- /dev/null
+++ b/packages/browser-destinations/destinations/hubble-web/src/identify/index.ts
@@ -0,0 +1,49 @@
+import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
+import { Hubble } from '../types'
+import type { Settings } from '../generated-types'
+import type { Payload } from './generated-types'
+
+// Change from unknown to the partner SDK types
+const action: BrowserActionDefinition = {
+ title: 'Identify',
+ description: 'Set identifiers and attributes for a user',
+ platform: 'web',
+ defaultSubscription: 'type = "identify"',
+ fields: {
+ userId: {
+ description: 'Unique identifer of the user',
+ type: 'string',
+ required: false,
+ label: 'User ID',
+ default: {
+ '@path': '$.userId'
+ }
+ },
+ anonymousId: {
+ description: 'Anonymous identifier of the user',
+ type: 'string',
+ required: false,
+ label: 'Anonymous ID',
+ default: {
+ '@path': '$.anonymousId'
+ }
+ },
+ attributes: {
+ description: 'User traits used to enrich user identification',
+ type: 'object',
+ required: false,
+ label: 'User Attributes (Traits)',
+ default: {
+ '@path': '$.traits'
+ }
+ }
+ },
+ perform: (hubble, event) => {
+ const payload = event.payload
+
+ hubble.identify &&
+ hubble.identify({ userId: payload.userId, anonymousId: payload.anonymousId, attributes: payload.attributes })
+ }
+}
+
+export default action
diff --git a/packages/browser-destinations/destinations/hubble-web/src/index.ts b/packages/browser-destinations/destinations/hubble-web/src/index.ts
new file mode 100644
index 0000000000..adef7740f4
--- /dev/null
+++ b/packages/browser-destinations/destinations/hubble-web/src/index.ts
@@ -0,0 +1,64 @@
+import type { Settings } from './generated-types'
+import type { BrowserDestinationDefinition } from '@segment/browser-destination-runtime/types'
+import { browserDestination } from '@segment/browser-destination-runtime/shim'
+
+import { Hubble } from './types'
+import { defaultValues } from '@segment/actions-core'
+
+import track from './track'
+import identify from './identify'
+
+declare global {
+ interface Window {
+ Hubble: Hubble
+ }
+}
+
+export const destination: BrowserDestinationDefinition = {
+ name: 'Hubble (actions)',
+ slug: 'hubble-web',
+ mode: 'device',
+ description:
+ 'From design to production, monitor, measure and enhance your user experience with seamless integration with Segment',
+ presets: [
+ {
+ name: 'Identify user',
+ subscribe: 'type = "identify"',
+ partnerAction: 'identify',
+ mapping: defaultValues(identify.fields),
+ type: 'automatic'
+ },
+ {
+ name: 'Track event',
+ subscribe: 'type = "track"',
+ partnerAction: 'track',
+ mapping: defaultValues(track.fields),
+ type: 'automatic'
+ }
+ ],
+
+ settings: {
+ id: {
+ description: 'Unique identifier for your team (given in Hubble app)',
+ label: 'id',
+ type: 'string',
+ required: true
+ }
+ },
+
+ initialize: async ({ settings }, deps) => {
+ await deps.loadScript(`https://sdk.hubble.team/api/sdk/${settings.id}`)
+ await deps.resolveWhen(() => window?.Hubble?.initialized, 250)
+
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
+ window?.Hubble?.setSource('__segment__')
+ return window.Hubble
+ },
+
+ actions: {
+ track,
+ identify
+ }
+}
+
+export default browserDestination(destination)
diff --git a/packages/browser-destinations/destinations/hubble-web/src/track/__tests__/index.test.ts b/packages/browser-destinations/destinations/hubble-web/src/track/__tests__/index.test.ts
new file mode 100644
index 0000000000..b6f29b62ec
--- /dev/null
+++ b/packages/browser-destinations/destinations/hubble-web/src/track/__tests__/index.test.ts
@@ -0,0 +1,84 @@
+import { Analytics, Context } from '@segment/analytics-next'
+import hubbleDestination, { destination } from '../../index'
+import { Subscription } from '@segment/browser-destination-runtime/types'
+
+const subscriptions: Subscription[] = [
+ {
+ partnerAction: 'track',
+ name: 'Track event',
+ enabled: true,
+ subscribe: 'type = "track"',
+ mapping: {
+ event: {
+ '@path': '$.event'
+ },
+ attributes: {
+ '@path': '$.properties'
+ }
+ }
+ }
+]
+
+describe('track', () => {
+ beforeAll(() => {
+ jest.mock('@segment/browser-destination-runtime/load-script', () => ({
+ loadScript: (_src: any, _attributes: any) => {}
+ }))
+ jest.mock('@segment/browser-destination-runtime/resolve-when', () => ({
+ resolveWhen: (_fn: any, _timeout: any) => {}
+ }))
+ })
+
+ let track: any
+ const mockTrack: jest.Mock = jest.fn()
+
+ beforeEach(async () => {
+ const [hubbleTrack] = await hubbleDestination({
+ id: 'testID',
+ subscriptions
+ })
+
+ track = hubbleTrack
+
+ jest.spyOn(destination, 'initialize').mockImplementation(() => {
+ const mockedWithTrack = {
+ id: 'testID',
+ initialized: true,
+ emitter: { setSource: jest.fn() },
+ track: mockTrack,
+ identify: jest.fn(),
+ setSource: jest.fn()
+ }
+ return Promise.resolve(mockedWithTrack)
+ })
+ await track.load(Context.system(), {} as Analytics)
+ })
+
+ test('it maps event parameters correctly to track function', async () => {
+ jest.spyOn(destination.actions.track, 'perform')
+
+ await track.track?.(
+ new Context({
+ type: 'track',
+ event: 'event-test',
+ properties: {
+ prop1: 'something',
+ prop2: 'another-thing'
+ }
+ })
+ )
+
+ expect(destination.actions.track.perform).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({
+ payload: {
+ event: 'event-test',
+ attributes: {
+ prop1: 'something',
+ prop2: 'another-thing'
+ }
+ }
+ })
+ )
+ })
+})
diff --git a/packages/browser-destinations/destinations/hubble-web/src/track/generated-types.ts b/packages/browser-destinations/destinations/hubble-web/src/track/generated-types.ts
new file mode 100644
index 0000000000..95b0d403de
--- /dev/null
+++ b/packages/browser-destinations/destinations/hubble-web/src/track/generated-types.ts
@@ -0,0 +1,22 @@
+// Generated file. DO NOT MODIFY IT BY HAND.
+
+export interface Payload {
+ /**
+ * Event to be tracked
+ */
+ event: string
+ /**
+ * Object containing the attributes (properties) of the event
+ */
+ attributes?: {
+ [k: string]: unknown
+ }
+ /**
+ * Unique identifer of the user
+ */
+ userId?: string
+ /**
+ * Anonymous identifier of the user
+ */
+ anonymousId?: string
+}
diff --git a/packages/browser-destinations/destinations/hubble-web/src/track/index.ts b/packages/browser-destinations/destinations/hubble-web/src/track/index.ts
new file mode 100644
index 0000000000..ffbf3fad2b
--- /dev/null
+++ b/packages/browser-destinations/destinations/hubble-web/src/track/index.ts
@@ -0,0 +1,63 @@
+import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
+import { Hubble } from '../types'
+import type { Settings } from '../generated-types'
+import type { Payload } from './generated-types'
+
+// Change from unknown to the partner SDK types
+const action: BrowserActionDefinition = {
+ title: 'Track',
+ description: 'Track events to trigger Hubble surveys',
+ platform: 'web',
+ defaultSubscription: 'type = "track"',
+ fields: {
+ event: {
+ description: 'Event to be tracked',
+ label: 'Event',
+ required: true,
+ type: 'string',
+ default: {
+ '@path': '$.event'
+ }
+ },
+ attributes: {
+ description: 'Object containing the attributes (properties) of the event',
+ type: 'object',
+ required: false,
+ label: 'Event Attributes',
+ default: {
+ '@path': '$.properties'
+ }
+ },
+ userId: {
+ description: 'Unique identifer of the user',
+ type: 'string',
+ required: false,
+ label: 'User ID',
+ default: {
+ '@path': '$.userId'
+ }
+ },
+ anonymousId: {
+ description: 'Anonymous identifier of the user',
+ type: 'string',
+ required: false,
+ label: 'Anonymous ID',
+ default: {
+ '@path': '$.anonymousId'
+ }
+ }
+ },
+ perform: (hubble, event) => {
+ const payload = event.payload
+
+ hubble.track &&
+ hubble.track({
+ event: payload.event,
+ attributes: payload.attributes,
+ userId: payload.userId,
+ anonymousId: payload.anonymousId
+ })
+ }
+}
+
+export default action
diff --git a/packages/browser-destinations/destinations/hubble-web/src/types.ts b/packages/browser-destinations/destinations/hubble-web/src/types.ts
new file mode 100644
index 0000000000..5e172e079b
--- /dev/null
+++ b/packages/browser-destinations/destinations/hubble-web/src/types.ts
@@ -0,0 +1,10 @@
+export type Methods = {
+ track?: (...args: unknown[]) => unknown
+ identify?: (...args: unknown[]) => unknown
+}
+
+export type Hubble = {
+ id: string
+ initialized: boolean
+ setSource: (source: string) => void
+} & Methods
diff --git a/packages/browser-destinations/destinations/hubble-web/tsconfig.json b/packages/browser-destinations/destinations/hubble-web/tsconfig.json
new file mode 100644
index 0000000000..c2a7897afd
--- /dev/null
+++ b/packages/browser-destinations/destinations/hubble-web/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.build.json",
+ "compilerOptions": {
+ "rootDir": "./src",
+ "baseUrl": "."
+ },
+ "include": ["src"],
+ "exclude": ["dist", "**/__tests__"]
+}
diff --git a/packages/browser-destinations/destinations/hubspot-web/package.json b/packages/browser-destinations/destinations/hubspot-web/package.json
index c8342cebb0..59948eb9fc 100644
--- a/packages/browser-destinations/destinations/hubspot-web/package.json
+++ b/packages/browser-destinations/destinations/hubspot-web/package.json
@@ -1,6 +1,6 @@
{
"name": "@segment/analytics-browser-actions-hubspot",
- "version": "1.15.0",
+ "version": "1.34.0",
"license": "MIT",
"publishConfig": {
"access": "public",
@@ -15,8 +15,8 @@
},
"typings": "./dist/esm",
"dependencies": {
- "@segment/actions-core": "^3.83.0",
- "@segment/browser-destination-runtime": "^1.14.0"
+ "@segment/actions-core": "^3.103.0",
+ "@segment/browser-destination-runtime": "^1.33.0"
},
"peerDependencies": {
"@segment/analytics-next": ">=1.55.0"
diff --git a/packages/browser-destinations/destinations/intercom/package.json b/packages/browser-destinations/destinations/intercom/package.json
index b8420e4ff9..bd6574d77c 100644
--- a/packages/browser-destinations/destinations/intercom/package.json
+++ b/packages/browser-destinations/destinations/intercom/package.json
@@ -1,6 +1,6 @@
{
"name": "@segment/analytics-browser-actions-intercom",
- "version": "1.15.0",
+ "version": "1.34.0",
"license": "MIT",
"publishConfig": {
"access": "public",
@@ -15,9 +15,9 @@
},
"typings": "./dist/esm",
"dependencies": {
- "@segment/actions-core": "^3.83.0",
- "@segment/actions-shared": "^1.65.0",
- "@segment/browser-destination-runtime": "^1.14.0"
+ "@segment/actions-core": "^3.103.0",
+ "@segment/actions-shared": "^1.84.0",
+ "@segment/browser-destination-runtime": "^1.33.0"
},
"peerDependencies": {
"@segment/analytics-next": ">=1.55.0"
diff --git a/packages/browser-destinations/destinations/iterate/package.json b/packages/browser-destinations/destinations/iterate/package.json
index fd2254f2dd..06970065a9 100644
--- a/packages/browser-destinations/destinations/iterate/package.json
+++ b/packages/browser-destinations/destinations/iterate/package.json
@@ -1,6 +1,6 @@
{
"name": "@segment/analytics-browser-actions-iterate",
- "version": "1.15.0",
+ "version": "1.34.0",
"license": "MIT",
"publishConfig": {
"access": "public",
@@ -15,8 +15,8 @@
},
"typings": "./dist/esm",
"dependencies": {
- "@segment/actions-core": "^3.83.0",
- "@segment/browser-destination-runtime": "^1.14.0"
+ "@segment/actions-core": "^3.103.0",
+ "@segment/browser-destination-runtime": "^1.33.0"
},
"peerDependencies": {
"@segment/analytics-next": ">=1.55.0"
diff --git a/packages/browser-destinations/destinations/jimo/README.md b/packages/browser-destinations/destinations/jimo/README.md
new file mode 100644
index 0000000000..be894a1ed3
--- /dev/null
+++ b/packages/browser-destinations/destinations/jimo/README.md
@@ -0,0 +1,31 @@
+# @segment/analytics-browser-actions-jimo
+
+The Jimo browser action destination for use with @segment/analytics-next.
+
+## License
+
+MIT License
+
+Copyright (c) 2024 Segment
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+## Contributing
+
+All third party contributors acknowledge that any contributions they provide will be made under the same open source license that the open source project is provided under.
diff --git a/packages/browser-destinations/destinations/jimo/package.json b/packages/browser-destinations/destinations/jimo/package.json
new file mode 100644
index 0000000000..fc162ed2c4
--- /dev/null
+++ b/packages/browser-destinations/destinations/jimo/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "@segment/analytics-browser-actions-jimo",
+ "version": "1.22.0",
+ "license": "MIT",
+ "publishConfig": {
+ "access": "public",
+ "registry": "https://registry.npmjs.org"
+ },
+ "main": "./dist/cjs",
+ "module": "./dist/esm",
+ "scripts": {
+ "build": "yarn build:esm && yarn build:cjs",
+ "build:cjs": "tsc --module commonjs --outDir ./dist/cjs",
+ "build:esm": "tsc --outDir ./dist/esm"
+ },
+ "typings": "./dist/esm",
+ "dependencies": {
+ "@segment/browser-destination-runtime": "^1.33.0"
+ },
+ "peerDependencies": {
+ "@segment/analytics-next": ">=1.55.0"
+ }
+}
diff --git a/packages/browser-destinations/destinations/jimo/src/generated-types.ts b/packages/browser-destinations/destinations/jimo/src/generated-types.ts
new file mode 100644
index 0000000000..89ab4c93fd
--- /dev/null
+++ b/packages/browser-destinations/destinations/jimo/src/generated-types.ts
@@ -0,0 +1,8 @@
+// Generated file. DO NOT MODIFY IT BY HAND.
+
+export interface Settings {
+ /**
+ * Id of the Jimo project. You can find the Project Id here: https://i.usejimo.com/settings/install/portal
+ */
+ projectId: string
+}
diff --git a/packages/browser-destinations/destinations/jimo/src/index.ts b/packages/browser-destinations/destinations/jimo/src/index.ts
new file mode 100644
index 0000000000..fd06c0c8d5
--- /dev/null
+++ b/packages/browser-destinations/destinations/jimo/src/index.ts
@@ -0,0 +1,66 @@
+import { defaultValues } from '@segment/actions-core'
+import { browserDestination } from '@segment/browser-destination-runtime/shim'
+import type { BrowserDestinationDefinition } from '@segment/browser-destination-runtime/types'
+import type { Settings } from './generated-types'
+import { initScript } from './init-script'
+import sendTrackEvent from './sendTrackEvent'
+import sendUserData from './sendUserData'
+import { JimoSDK } from './types'
+
+declare global {
+ interface Window {
+ jimo: JimoSDK | never[]
+ JIMO_PROJECT_ID: string
+ JIMO_MANUAL_INIT: boolean
+ }
+}
+
+const ENDPOINT_UNDERCITY = 'https://undercity.usejimo.com/jimo-invader.js'
+
+export const destination: BrowserDestinationDefinition = {
+ name: 'Jimo (Actions)',
+ slug: 'actions-jimo',
+ mode: 'device',
+ description: 'Load Jimo SDK and send user profile data to Jimo',
+
+ settings: {
+ projectId: {
+ description:
+ 'Id of the Jimo project. You can find the Project Id here: https://i.usejimo.com/settings/install/portal',
+ label: 'Id',
+ type: 'string',
+ required: true
+ }
+ },
+ presets: [
+ {
+ name: 'Send User Data',
+ subscribe: 'type = "identify"',
+ partnerAction: 'sendUserData',
+ mapping: defaultValues(sendUserData.fields),
+ type: 'automatic'
+ },
+ {
+ name: 'Send Track Event',
+ subscribe: 'type = "track"',
+ partnerAction: 'sendTrackEvent',
+ mapping: defaultValues(sendTrackEvent.fields),
+ type: 'automatic'
+ }
+ ],
+ initialize: async ({ settings }, deps) => {
+ initScript(settings)
+
+ await deps.loadScript(`${ENDPOINT_UNDERCITY}`)
+
+ await deps.resolveWhen(() => Array.isArray(window.jimo) === false, 100)
+
+ return window.jimo as JimoSDK
+ },
+ actions: {
+ sendUserData,
+ sendTrackEvent
+ }
+}
+
+export default browserDestination(destination)
diff --git a/packages/browser-destinations/destinations/jimo/src/init-script.ts b/packages/browser-destinations/destinations/jimo/src/init-script.ts
new file mode 100644
index 0000000000..a93d29eed8
--- /dev/null
+++ b/packages/browser-destinations/destinations/jimo/src/init-script.ts
@@ -0,0 +1,11 @@
+import { Settings } from './generated-types'
+/* eslint-disable */
+// @ts-nocheck
+export function initScript(settings: Settings) {
+ if (window.jimo) {
+ return
+ }
+
+ window.jimo = []
+ window['JIMO_PROJECT_ID'] = settings.projectId
+}
diff --git a/packages/browser-destinations/destinations/jimo/src/sendTrackEvent/__tests__/index.test.ts b/packages/browser-destinations/destinations/jimo/src/sendTrackEvent/__tests__/index.test.ts
new file mode 100644
index 0000000000..7af2ff272b
--- /dev/null
+++ b/packages/browser-destinations/destinations/jimo/src/sendTrackEvent/__tests__/index.test.ts
@@ -0,0 +1,51 @@
+import { Analytics, Context } from '@segment/analytics-next'
+import sendTrackEvent from '..'
+import { JimoSDK } from '../../types'
+import { Payload } from '../generated-types'
+
+describe('Jimo - Send Track Event', () => {
+ test('do:segmentio:track is called', async () => {
+ const client = {
+ push: jest.fn()
+ } as any as JimoSDK
+
+ const context = new Context({
+ type: 'track'
+ })
+
+ await sendTrackEvent.perform(client as any as JimoSDK, {
+ settings: { projectId: 'unk' },
+ analytics: jest.fn() as any as Analytics,
+ context: context,
+ payload: {
+ messageId: '42',
+ timestamp: 'timestamp-as-iso-string',
+ userId: 'u1',
+ anonymousId: 'a1',
+ event_name: 'foo',
+ properties: {
+ foo: 'bar'
+ }
+ } as Payload
+ })
+
+ expect(client.push).toHaveBeenCalled()
+ expect(client.push).toHaveBeenCalledWith([
+ 'do',
+ 'segmentio:track',
+ [
+ {
+ event: 'foo',
+ messageId: '42',
+ timestamp: 'timestamp-as-iso-string',
+ receivedAt: 'timestamp-as-iso-string',
+ userId: 'u1',
+ anonymousId: 'a1',
+ properties: {
+ foo: 'bar'
+ }
+ }
+ ]
+ ])
+ })
+})
diff --git a/packages/browser-destinations/destinations/jimo/src/sendTrackEvent/generated-types.ts b/packages/browser-destinations/destinations/jimo/src/sendTrackEvent/generated-types.ts
new file mode 100644
index 0000000000..b3f2dddd97
--- /dev/null
+++ b/packages/browser-destinations/destinations/jimo/src/sendTrackEvent/generated-types.ts
@@ -0,0 +1,30 @@
+// Generated file. DO NOT MODIFY IT BY HAND.
+
+export interface Payload {
+ /**
+ * The internal id of the message.
+ */
+ messageId: string
+ /**
+ * The timestamp of the event.
+ */
+ timestamp: string
+ /**
+ * The name of the event.
+ */
+ event_name: string
+ /**
+ * A unique identifier for the user.
+ */
+ userId?: string
+ /**
+ * An anonymous identifier for the user.
+ */
+ anonymousId?: string
+ /**
+ * Information associated with the event
+ */
+ properties?: {
+ [k: string]: unknown
+ }
+}
diff --git a/packages/browser-destinations/destinations/jimo/src/sendTrackEvent/index.ts b/packages/browser-destinations/destinations/jimo/src/sendTrackEvent/index.ts
new file mode 100644
index 0000000000..64cf089125
--- /dev/null
+++ b/packages/browser-destinations/destinations/jimo/src/sendTrackEvent/index.ts
@@ -0,0 +1,80 @@
+import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
+import { JimoSDK } from 'src/types'
+import type { Settings } from '../generated-types'
+import { Payload } from './generated-types'
+
+const action: BrowserActionDefinition = {
+ title: 'Send Track Event',
+ description: 'Submit an event to Jimo',
+ defaultSubscription: 'type = "track"',
+ platform: 'web',
+ fields: {
+ messageId: {
+ description: 'The internal id of the message.',
+ label: 'Message Id',
+ type: 'string',
+ required: true,
+ default: {
+ '@path': '$.messageId'
+ }
+ },
+ timestamp: {
+ description: 'The timestamp of the event.',
+ label: 'Timestamp',
+ type: 'string',
+ required: true,
+ default: {
+ '@path': '$.timestamp'
+ }
+ },
+ event_name: {
+ description: 'The name of the event.',
+ label: 'Event Name',
+ type: 'string',
+ required: true,
+ default: {
+ '@path': '$.event'
+ }
+ },
+ userId: {
+ description: 'A unique identifier for the user.',
+ label: 'User ID',
+ type: 'string',
+ required: false,
+ default: {
+ '@path': '$.userId'
+ }
+ },
+ anonymousId: {
+ description: 'An anonymous identifier for the user.',
+ label: 'Anonymous ID',
+ type: 'string',
+ required: false,
+ default: {
+ '@path': '$.anonymousId'
+ }
+ },
+ properties: {
+ description: 'Information associated with the event',
+ label: 'Event Properties',
+ type: 'object',
+ required: false,
+ default: {
+ '@path': '$.properties'
+ }
+ }
+ },
+ perform: (jimo, { payload }) => {
+ const { event_name, userId, anonymousId, timestamp, messageId, properties } = payload
+ const receivedAt = timestamp
+
+ jimo.push([
+ 'do',
+ 'segmentio:track',
+ [{ event: event_name, userId, anonymousId, messageId, timestamp, receivedAt, properties }]
+ ])
+ window.dispatchEvent(new CustomEvent(`jimo-segmentio-track:${event_name}`))
+ }
+}
+
+export default action
diff --git a/packages/browser-destinations/destinations/jimo/src/sendUserData/__tests__/index.test.ts b/packages/browser-destinations/destinations/jimo/src/sendUserData/__tests__/index.test.ts
new file mode 100644
index 0000000000..0e5d5c95ff
--- /dev/null
+++ b/packages/browser-destinations/destinations/jimo/src/sendUserData/__tests__/index.test.ts
@@ -0,0 +1,87 @@
+import { Analytics, Context } from '@segment/analytics-next'
+
+import sendUserData from '..'
+import { JimoSDK } from '../../types'
+import { Payload } from '../generated-types'
+
+describe('Jimo - Send User Data', () => {
+ test('user id', async () => {
+ const client = {
+ push: jest.fn()
+ } as any as JimoSDK
+
+ const context = new Context({
+ type: 'identify'
+ })
+
+ await sendUserData.perform(client as any as JimoSDK, {
+ settings: { projectId: 'unk' },
+ analytics: jest.fn() as any as Analytics,
+ context: context,
+ payload: {
+ userId: 'u1'
+ } as Payload
+ })
+
+ expect(client.push).toHaveBeenCalled()
+ expect(client.push).toHaveBeenCalledWith(['set', 'user:id', ['u1']])
+ })
+ test('user email', async () => {
+ const client = {
+ push: jest.fn()
+ } as any as JimoSDK
+
+ const context = new Context({
+ type: 'identify'
+ })
+
+ await sendUserData.perform(client as any as JimoSDK, {
+ settings: { projectId: 'unk' },
+ analytics: jest.fn() as any as Analytics,
+ context: context,
+ payload: {
+ email: 'foo@bar.com'
+ } as Payload
+ })
+
+ expect(client.push).toHaveBeenCalled()
+ expect(client.push).toHaveBeenCalledWith(['set', 'user:email', ['foo@bar.com']])
+ })
+ test('user traits', async () => {
+ const client = {
+ push: jest.fn()
+ } as any as JimoSDK
+
+ const context = new Context({
+ type: 'identify'
+ })
+
+ await sendUserData.perform(client as any as JimoSDK, {
+ settings: { projectId: 'unk' },
+ analytics: jest.fn() as any as Analytics,
+ context: context,
+ payload: {
+ traits: {
+ trait1: true,
+ trait2: 'foo',
+ trait3: 1
+ }
+ } as Payload
+ })
+
+ expect(client.push).toHaveBeenCalled()
+ expect(client.push).toHaveBeenCalledWith([
+ 'set',
+ 'user:attributes',
+ [
+ {
+ trait1: true,
+ trait2: 'foo',
+ trait3: 1
+ },
+ false,
+ true
+ ]
+ ])
+ })
+})
diff --git a/packages/browser-destinations/destinations/jimo/src/sendUserData/generated-types.ts b/packages/browser-destinations/destinations/jimo/src/sendUserData/generated-types.ts
new file mode 100644
index 0000000000..3105bc1871
--- /dev/null
+++ b/packages/browser-destinations/destinations/jimo/src/sendUserData/generated-types.ts
@@ -0,0 +1,18 @@
+// Generated file. DO NOT MODIFY IT BY HAND.
+
+export interface Payload {
+ /**
+ * The unique user identifier
+ */
+ userId?: string | null
+ /**
+ * The email of the user
+ */
+ email?: string | null
+ /**
+ * A list of attributes coming from segment traits
+ */
+ traits?: {
+ [k: string]: unknown
+ }
+}
diff --git a/packages/browser-destinations/destinations/jimo/src/sendUserData/index.ts b/packages/browser-destinations/destinations/jimo/src/sendUserData/index.ts
new file mode 100644
index 0000000000..a6ee2c1a75
--- /dev/null
+++ b/packages/browser-destinations/destinations/jimo/src/sendUserData/index.ts
@@ -0,0 +1,57 @@
+import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
+import { JimoSDK } from 'src/types'
+import type { Settings } from '../generated-types'
+import type { Payload } from './generated-types'
+
+const action: BrowserActionDefinition = {
+ title: 'Send User Data',
+ description: 'Send user ID and email to Jimo',
+ platform: 'web',
+ fields: {
+ userId: {
+ label: 'User ID',
+ description: 'The unique user identifier',
+ type: 'string',
+ allowNull: true,
+ required: false,
+ default: {
+ '@path': '$.userId'
+ }
+ },
+ email: {
+ label: 'User email',
+ description: 'The email of the user',
+ type: 'string',
+ allowNull: true,
+ required: false,
+ default: {
+ '@path': '$.traits.email'
+ }
+ },
+ traits: {
+ label: 'User Traits',
+ description: 'A list of attributes coming from segment traits',
+ type: 'object',
+ default: {
+ '@path': '$.traits'
+ }
+ }
+ },
+ defaultSubscription: 'type = "identify"',
+ perform: (jimo, { payload }) => {
+ if (payload.userId != null) {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
+ jimo.push(['set', 'user:id', [payload.userId]])
+ }
+ if (payload.email != null) {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
+ jimo.push(['set', 'user:email', [payload.email]])
+ }
+ if (payload.traits != null) {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
+ jimo.push(['set', 'user:attributes', [payload.traits, false, true]])
+ }
+ }
+}
+
+export default action
diff --git a/packages/browser-destinations/destinations/jimo/src/types.ts b/packages/browser-destinations/destinations/jimo/src/types.ts
new file mode 100644
index 0000000000..0bb96075b5
--- /dev/null
+++ b/packages/browser-destinations/destinations/jimo/src/types.ts
@@ -0,0 +1,3 @@
+export interface JimoSDK {
+ push: (params: Array) => Promise
+}
diff --git a/packages/browser-destinations/destinations/jimo/tsconfig.json b/packages/browser-destinations/destinations/jimo/tsconfig.json
new file mode 100644
index 0000000000..c2a7897afd
--- /dev/null
+++ b/packages/browser-destinations/destinations/jimo/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.build.json",
+ "compilerOptions": {
+ "rootDir": "./src",
+ "baseUrl": "."
+ },
+ "include": ["src"],
+ "exclude": ["dist", "**/__tests__"]
+}
diff --git a/packages/browser-destinations/destinations/koala/package.json b/packages/browser-destinations/destinations/koala/package.json
index 30ea2eaf20..8a0b40f8d0 100644
--- a/packages/browser-destinations/destinations/koala/package.json
+++ b/packages/browser-destinations/destinations/koala/package.json
@@ -1,6 +1,6 @@
{
"name": "@segment/analytics-browser-actions-koala",
- "version": "1.15.0",
+ "version": "1.34.0",
"license": "MIT",
"publishConfig": {
"access": "public",
@@ -15,8 +15,8 @@
},
"typings": "./dist/esm",
"dependencies": {
- "@segment/actions-core": "^3.83.0",
- "@segment/browser-destination-runtime": "^1.14.0"
+ "@segment/actions-core": "^3.103.0",
+ "@segment/browser-destination-runtime": "^1.33.0"
},
"peerDependencies": {
"@segment/analytics-next": ">=1.55.0"
diff --git a/packages/browser-destinations/destinations/koala/src/index.ts b/packages/browser-destinations/destinations/koala/src/index.ts
index 1d164cb2a6..c991ac6db1 100644
--- a/packages/browser-destinations/destinations/koala/src/index.ts
+++ b/packages/browser-destinations/destinations/koala/src/index.ts
@@ -30,7 +30,7 @@ export const destination: BrowserDestinationDefinition = {
initialize: async ({ settings, analytics }, deps) => {
initScript()
- await deps.loadScript(`https://cdn.koala.live/v1/${settings.project_slug}/umd.js`)
+ await deps.loadScript(`https://cdn.getkoala.com/v1/${settings.project_slug}/umd.js`)
const ko = await window.KoalaSDK.load({
project: settings.project_slug,
diff --git a/packages/browser-destinations/destinations/logrocket/package.json b/packages/browser-destinations/destinations/logrocket/package.json
index 72b47c5061..b93ed7feb2 100644
--- a/packages/browser-destinations/destinations/logrocket/package.json
+++ b/packages/browser-destinations/destinations/logrocket/package.json
@@ -1,6 +1,6 @@
{
"name": "@segment/analytics-browser-actions-logrocket",
- "version": "1.15.0",
+ "version": "1.34.0",
"license": "MIT",
"publishConfig": {
"access": "public",
@@ -15,8 +15,8 @@
},
"typings": "./dist/esm",
"dependencies": {
- "@segment/actions-core": "^3.83.0",
- "@segment/browser-destination-runtime": "^1.14.0",
+ "@segment/actions-core": "^3.103.0",
+ "@segment/browser-destination-runtime": "^1.33.0",
"logrocket": "^3.0.1"
},
"peerDependencies": {
diff --git a/packages/browser-destinations/destinations/pendo-web-actions/README.md b/packages/browser-destinations/destinations/pendo-web-actions/README.md
index dae01eb768..8856d5cb30 100644
--- a/packages/browser-destinations/destinations/pendo-web-actions/README.md
+++ b/packages/browser-destinations/destinations/pendo-web-actions/README.md
@@ -6,7 +6,7 @@ The Pendo Web (actions) browser action destination for use with @segment/analyti
MIT License
-Copyright (c) 2023 Segment
+Copyright (c) 2024 Segment
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/packages/browser-destinations/destinations/pendo-web-actions/package.json b/packages/browser-destinations/destinations/pendo-web-actions/package.json
index 2a5bca0b2a..b82267250c 100644
--- a/packages/browser-destinations/destinations/pendo-web-actions/package.json
+++ b/packages/browser-destinations/destinations/pendo-web-actions/package.json
@@ -1,6 +1,6 @@
{
"name": "@segment/analytics-browser-actions-pendo-web-actions",
- "version": "1.3.0",
+ "version": "1.23.0",
"license": "MIT",
"publishConfig": {
"access": "public",
@@ -15,7 +15,7 @@
},
"typings": "./dist/esm",
"dependencies": {
- "@segment/browser-destination-runtime": "^1.14.0"
+ "@segment/browser-destination-runtime": "^1.33.0"
},
"peerDependencies": {
"@segment/analytics-next": ">=1.55.0"
diff --git a/packages/browser-destinations/destinations/pendo-web-actions/src/generated-types.ts b/packages/browser-destinations/destinations/pendo-web-actions/src/generated-types.ts
index 5ba99e076d..4743b78a59 100644
--- a/packages/browser-destinations/destinations/pendo-web-actions/src/generated-types.ts
+++ b/packages/browser-destinations/destinations/pendo-web-actions/src/generated-types.ts
@@ -6,19 +6,19 @@ export interface Settings {
*/
apiKey: string
/**
- * Segment can set the Pendo Account ID upon page load. This can be overridden via the Account ID field in the Send Identify/Group Actions
+ * The region for your Pendo subscription.
*/
- accountId?: string
+ region: string
/**
- * Segment can set the Pendo Parent Account ID upon page load. This can be overridden via the Parent Account ID field in the Send Identify/Group Actions. Note: Contact Pendo to request enablement of Parent Account feature.
+ * If you are using Pendo's CNAME feature, this will update your Pendo install snippet with your content host.
*/
- parentAccountId?: string
+ cnameContentHost?: string
/**
- * The Pendo Region you'd like to send data to
+ * Override sending Segment's user traits on load. This will prevent Pendo from initializing with the user traits from Segment (analytics.user().traits()). Allowing you to adjust the mapping of visitor metadata in Segment's identify event.
*/
- region: string
+ disableUserTraitsOnLoad?: boolean
/**
- * Segment can set the Pendo Visitor ID upon page load to either the Segment userId or anonymousId. This can be overridden via the Visitor ID field in the Send Identify/Group Actions
+ * Override sending Segment's group id for Pendo's account id. This will prevent Pendo from initializing with the group id from Segment (analytics.group().id()). Allowing you to adjust the mapping of account id in Segment's group event.
*/
- setVisitorIdOnLoad: string
+ disableGroupIdAndTraitsOnLoad?: boolean
}
diff --git a/packages/browser-destinations/destinations/pendo-web-actions/src/group/__tests__/index.test.ts b/packages/browser-destinations/destinations/pendo-web-actions/src/group/__tests__/index.test.ts
index 988a9cd282..aed5c01894 100644
--- a/packages/browser-destinations/destinations/pendo-web-actions/src/group/__tests__/index.test.ts
+++ b/packages/browser-destinations/destinations/pendo-web-actions/src/group/__tests__/index.test.ts
@@ -18,6 +18,9 @@ const subscriptions: Subscription[] = [
},
accountData: {
'@path': '$.traits'
+ },
+ parentAccountData: {
+ '@path': '$.traits.parentAccount'
}
}
}
@@ -46,7 +49,8 @@ describe('Pendo.group', () => {
initialize: jest.fn(),
isReady: jest.fn(),
track: jest.fn(),
- identify: jest.fn()
+ identify: jest.fn(),
+ flushNow: jest.fn()
}
return Promise.resolve(mockPendo)
})
@@ -69,4 +73,25 @@ describe('Pendo.group', () => {
visitor: { id: 'testUserId' }
})
})
+
+ test('parentAccountData is being deduped from accountData correctly', async () => {
+ const context = new Context({
+ type: 'group',
+ userId: 'testUserId',
+ traits: {
+ company_name: 'Megacorp 2000',
+ parentAccount: {
+ id: 'some_id'
+ }
+ },
+ groupId: 'company_id_1'
+ })
+ await groupAction.group?.(context)
+
+ expect(mockPendo.identify).toHaveBeenCalledWith({
+ account: { id: 'company_id_1', company_name: 'Megacorp 2000' },
+ visitor: { id: 'testUserId' },
+ parentAccount: { id: 'some_id' }
+ })
+ })
})
diff --git a/packages/browser-destinations/destinations/pendo-web-actions/src/group/generated-types.ts b/packages/browser-destinations/destinations/pendo-web-actions/src/group/generated-types.ts
index 508cd67b55..cd82232d85 100644
--- a/packages/browser-destinations/destinations/pendo-web-actions/src/group/generated-types.ts
+++ b/packages/browser-destinations/destinations/pendo-web-actions/src/group/generated-types.ts
@@ -2,11 +2,11 @@
export interface Payload {
/**
- * Pendo Visitor ID. Defaults to Segment userId
+ * Pendo Visitor ID. Maps to Segment userId
*/
visitorId: string
/**
- * Pendo Account ID. This overrides the Pendo Account ID setting
+ * Pendo Account ID. Maps to Segment groupId. Note: If you plan to change this, enable the setting "Use custom Segment group trait for Pendo account id"
*/
accountId: string
/**
@@ -15,14 +15,11 @@ export interface Payload {
accountData?: {
[k: string]: unknown
}
- /**
- * Pendo Parent Account ID. This overrides the Pendo Parent Account ID setting. Note: Contact Pendo to request enablement of Parent Account feature.
- */
- parentAccountId?: string
/**
* Additional Parent Account data to send. Note: Contact Pendo to request enablement of Parent Account feature.
*/
parentAccountData?: {
+ id: string
[k: string]: unknown
}
}
diff --git a/packages/browser-destinations/destinations/pendo-web-actions/src/group/index.ts b/packages/browser-destinations/destinations/pendo-web-actions/src/group/index.ts
index cae05fca82..900dbb44cd 100644
--- a/packages/browser-destinations/destinations/pendo-web-actions/src/group/index.ts
+++ b/packages/browser-destinations/destinations/pendo-web-actions/src/group/index.ts
@@ -1,7 +1,8 @@
import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'
-import type { PendoSDK, identifyPayload } from '../types'
+import type { PendoSDK, PendoOptions } from '../types'
+import { removeNestedObject, AnyObject, getSubstringDifference } from '../utils'
const action: BrowserActionDefinition = {
title: 'Send Group Event',
@@ -11,60 +12,82 @@ const action: BrowserActionDefinition = {
fields: {
visitorId: {
label: 'Visitor ID',
- description: 'Pendo Visitor ID. Defaults to Segment userId',
+ description: 'Pendo Visitor ID. Maps to Segment userId',
type: 'string',
required: true,
default: {
'@path': '$.userId'
- }
+ },
+ readOnly: true
},
accountId: {
label: 'Account ID',
- description: 'Pendo Account ID. This overrides the Pendo Account ID setting',
+ description:
+ 'Pendo Account ID. Maps to Segment groupId. Note: If you plan to change this, enable the setting "Use custom Segment group trait for Pendo account id"',
type: 'string',
required: true,
- default: { '@path': '$.groupId' }
+ default: { '@path': '$.groupId' },
+ readOnly: false
},
accountData: {
label: 'Account Metadata',
description: 'Additional Account data to send',
type: 'object',
- required: false
- },
- parentAccountId: {
- label: 'Parent Account ID',
- description:
- 'Pendo Parent Account ID. This overrides the Pendo Parent Account ID setting. Note: Contact Pendo to request enablement of Parent Account feature.',
- type: 'string',
- required: false
+ required: false,
+ default: { '@path': '$.traits' },
+ readOnly: false
},
parentAccountData: {
label: 'Parent Account Metadata',
description:
'Additional Parent Account data to send. Note: Contact Pendo to request enablement of Parent Account feature.',
type: 'object',
+ properties: {
+ id: {
+ label: 'Parent Account ID',
+ type: 'string',
+ required: true
+ }
+ },
+ additionalProperties: true,
+ default: { '@path': '$.traits.parentAccount' },
required: false
}
},
- perform: (pendo, event) => {
- const payload: identifyPayload = {
- visitor: {
- id: event.payload.visitorId
- }
+ perform: (pendo, { mapping, payload }) => {
+ // remove parentAccountData field data from the accountData if the paths overlap
+
+ type pathMapping = {
+ '@path': string
}
- if (event.payload.accountId || event.settings.accountId) {
- payload.account = {
- id: event.payload.accountId ?? (event.settings.accountId as string),
- ...event.payload.accountData
- }
+
+ const parentAccountDataMapping = mapping && (mapping.parentAccountData as pathMapping)?.['@path']
+ const accountDataMapping = mapping && (mapping.accountData as pathMapping)?.['@path']
+
+ const difference: string | null = getSubstringDifference(parentAccountDataMapping, accountDataMapping)
+
+ let accountData = undefined
+ if (difference !== null) {
+ accountData = removeNestedObject(payload.accountData as AnyObject, difference)
+ } else {
+ accountData = payload.accountData
}
- if (event.payload.parentAccountId || event.settings.parentAccountId) {
- payload.parentAccount = {
- id: (event.payload.parentAccountId as string) ?? (event.settings.parentAccountId as string),
- ...event.payload.parentAccountData
+
+ const pendoPayload: PendoOptions = {
+ visitor: {
+ id: payload.visitorId
+ },
+ account: {
+ ...accountData,
+ id: payload.accountId
}
}
- pendo.identify(payload)
+
+ if (payload.parentAccountData) {
+ pendoPayload.parentAccount = payload.parentAccountData
+ }
+
+ pendo.identify(pendoPayload)
}
}
diff --git a/packages/browser-destinations/destinations/pendo-web-actions/src/identify/__tests__/index.test.ts b/packages/browser-destinations/destinations/pendo-web-actions/src/identify/__tests__/index.test.ts
index 7ab72648c7..b7fb53b742 100644
--- a/packages/browser-destinations/destinations/pendo-web-actions/src/identify/__tests__/index.test.ts
+++ b/packages/browser-destinations/destinations/pendo-web-actions/src/identify/__tests__/index.test.ts
@@ -15,9 +15,6 @@ const subscriptions: Subscription[] = [
},
visitorData: {
'@path': '$.traits'
- },
- accountId: {
- '@path': '$.context.group_id'
}
}
}
@@ -26,7 +23,6 @@ const subscriptions: Subscription[] = [
describe('Pendo.identify', () => {
const settings = {
apiKey: 'abc123',
- setVisitorIdOnLoad: 'disabled',
region: 'io'
}
@@ -46,7 +42,8 @@ describe('Pendo.identify', () => {
initialize: jest.fn(),
isReady: jest.fn(),
track: jest.fn(),
- identify: jest.fn()
+ identify: jest.fn(),
+ flushNow: jest.fn()
}
return Promise.resolve(mockPendo)
})
@@ -59,15 +56,11 @@ describe('Pendo.identify', () => {
userId: 'testUserId',
traits: {
first_name: 'Jimbo'
- },
- context: {
- group_id: 'company_id_1'
}
})
await identifyAction.identify?.(context)
expect(mockPendo.identify).toHaveBeenCalledWith({
- account: { id: 'company_id_1' },
visitor: { first_name: 'Jimbo', id: 'testUserId' }
})
})
diff --git a/packages/browser-destinations/destinations/pendo-web-actions/src/identify/generated-types.ts b/packages/browser-destinations/destinations/pendo-web-actions/src/identify/generated-types.ts
index 1e87e6bc18..0be6efbb47 100644
--- a/packages/browser-destinations/destinations/pendo-web-actions/src/identify/generated-types.ts
+++ b/packages/browser-destinations/destinations/pendo-web-actions/src/identify/generated-types.ts
@@ -2,7 +2,7 @@
export interface Payload {
/**
- * Pendo Visitor ID. Defaults to Segment userId
+ * Pendo Visitor ID. Maps to Segment userId
*/
visitorId: string
/**
@@ -11,24 +11,4 @@ export interface Payload {
visitorData?: {
[k: string]: unknown
}
- /**
- * Pendo Account ID. This overrides the Pendo Account ID setting
- */
- accountId?: string
- /**
- * Additional Account data to send
- */
- accountData?: {
- [k: string]: unknown
- }
- /**
- * Pendo Parent Account ID. This overrides the Pendo Parent Account ID setting. Note: Contact Pendo to request enablement of Parent Account feature.
- */
- parentAccountId?: string
- /**
- * Additional Parent Account data to send. Note: Contact Pendo to request enablement of Parent Account feature.
- */
- parentAccountData?: {
- [k: string]: unknown
- }
}
diff --git a/packages/browser-destinations/destinations/pendo-web-actions/src/identify/index.ts b/packages/browser-destinations/destinations/pendo-web-actions/src/identify/index.ts
index 7af0b7c945..221cac0c84 100644
--- a/packages/browser-destinations/destinations/pendo-web-actions/src/identify/index.ts
+++ b/packages/browser-destinations/destinations/pendo-web-actions/src/identify/index.ts
@@ -1,7 +1,7 @@
import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'
-import type { PendoSDK, identifyPayload } from '../types'
+import type { PendoSDK, PendoOptions } from '../types'
const action: BrowserActionDefinition = {
title: 'Send Identify Event',
@@ -11,12 +11,13 @@ const action: BrowserActionDefinition = {
fields: {
visitorId: {
label: 'Visitor ID',
- description: 'Pendo Visitor ID. Defaults to Segment userId',
+ description: 'Pendo Visitor ID. Maps to Segment userId',
type: 'string',
required: true,
default: {
'@path': '$.userId'
- }
+ },
+ readOnly: true
},
visitorData: {
label: 'Visitor Metadata',
@@ -24,61 +25,18 @@ const action: BrowserActionDefinition = {
type: 'object',
default: {
'@path': '$.traits'
- }
- },
- accountId: {
- label: 'Account ID',
- description: 'Pendo Account ID. This overrides the Pendo Account ID setting',
- type: 'string',
- required: false,
- default: {
- '@if': {
- exists: { '@path': '$.context.group_id' },
- then: { '@path': '$.context.group_id' },
- else: { '@path': '$.groupId' }
- }
- }
- },
- accountData: {
- label: 'Account Metadata',
- description: 'Additional Account data to send',
- type: 'object',
- required: false
- },
- parentAccountId: {
- label: 'Parent Account ID',
- description:
- 'Pendo Parent Account ID. This overrides the Pendo Parent Account ID setting. Note: Contact Pendo to request enablement of Parent Account feature.',
- type: 'string',
- required: false
- },
- parentAccountData: {
- label: 'Parent Account Metadata',
- description:
- 'Additional Parent Account data to send. Note: Contact Pendo to request enablement of Parent Account feature.',
- type: 'object',
- required: false
+ },
+ readOnly: false
}
},
perform: (pendo, event) => {
- const payload: identifyPayload = {
+ const payload: PendoOptions = {
visitor: {
- id: event.payload.visitorId,
- ...event.payload.visitorData
- }
- }
- if (event.payload.accountId || event.settings.accountId) {
- payload.account = {
- id: (event.payload.accountId as string) ?? (event.settings.accountId as string),
- ...event.payload.accountData
- }
- }
- if (event.payload.parentAccountId || event.settings.parentAccountId) {
- payload.parentAccount = {
- id: (event.payload.parentAccountId as string) ?? (event.settings.parentAccountId as string),
- ...event.payload.parentAccountData
+ ...event.payload.visitorData,
+ id: event.payload.visitorId
}
}
+
pendo.identify(payload)
}
}
diff --git a/packages/browser-destinations/destinations/pendo-web-actions/src/index.ts b/packages/browser-destinations/destinations/pendo-web-actions/src/index.ts
index 124869a876..79e902ebbb 100644
--- a/packages/browser-destinations/destinations/pendo-web-actions/src/index.ts
+++ b/packages/browser-destinations/destinations/pendo-web-actions/src/index.ts
@@ -2,7 +2,9 @@ import type { Settings } from './generated-types'
import type { BrowserDestinationDefinition } from '@segment/browser-destination-runtime/types'
import { browserDestination } from '@segment/browser-destination-runtime/shim'
import { loadPendo } from './loadScript'
-import { InitializeData, PendoSDK } from './types'
+import { PendoOptions, PendoSDK } from './types'
+import { ID } from '@segment/analytics-next'
+import { defaultValues } from '@segment/actions-core'
import identify from './identify'
import track from './track'
@@ -28,90 +30,84 @@ export const destination: BrowserDestinationDefinition = {
type: 'string',
required: true
},
- accountId: {
- label: 'Set Pendo Account ID on Load',
- description:
- 'Segment can set the Pendo Account ID upon page load. This can be overridden via the Account ID field in the Send Identify/Group Actions',
- type: 'string',
- required: false
- },
- parentAccountId: {
- label: 'Set Pendo Parent Account ID on Load',
- description:
- 'Segment can set the Pendo Parent Account ID upon page load. This can be overridden via the Parent Account ID field in the Send Identify/Group Actions. Note: Contact Pendo to request enablement of Parent Account feature.',
- type: 'string',
- required: false
- },
region: {
label: 'Region',
type: 'string',
- description: "The Pendo Region you'd like to send data to",
+ description: 'The region for your Pendo subscription.',
required: true,
- default: 'io',
+ default: 'https://cdn.pendo.io',
choices: [
- { value: 'io', label: 'io' },
- { value: 'eu', label: 'eu' }
+ { value: 'https://cdn.pendo.io', label: 'US (default)' },
+ { value: 'https://cdn.eu.pendo.io', label: 'EU' },
+ { value: 'https://us1.cdn.pendo.io', label: 'US restricted' },
+ { value: 'https://cdn.jpn.pendo.io', label: 'Japan' }
]
},
- setVisitorIdOnLoad: {
- label: 'Set Vistor ID on Load',
+ cnameContentHost: {
+ label: 'Optional CNAME content host',
description:
- 'Segment can set the Pendo Visitor ID upon page load to either the Segment userId or anonymousId. This can be overridden via the Visitor ID field in the Send Identify/Group Actions',
+ "If you are using Pendo's CNAME feature, this will update your Pendo install snippet with your content host.",
type: 'string',
- default: 'disabled',
- choices: [
- { value: 'disabled', label: 'Do not set Visitor ID on load' },
- { value: 'userIdOnly', label: 'Set Visitor ID to userId on load' },
- { value: 'userIdOrAnonymousId', label: 'Set Visitor ID to userId or anonymousId on load' },
- { value: 'anonymousIdOnly', label: 'Set Visitor ID to anonymousId on load' }
- ],
- required: true
+ required: false
+ },
+ disableUserTraitsOnLoad: {
+ label: "Disable passing Segment's user traits to Pendo on start up",
+ description:
+ "Override sending Segment's user traits on load. This will prevent Pendo from initializing with the user traits from Segment (analytics.user().traits()). Allowing you to adjust the mapping of visitor metadata in Segment's identify event.",
+ type: 'boolean',
+ required: false,
+ default: false
+ },
+ disableGroupIdAndTraitsOnLoad: {
+ label: "Disable passing Segment's group id and group traits to Pendo on start up",
+ description:
+ "Override sending Segment's group id for Pendo's account id. This will prevent Pendo from initializing with the group id from Segment (analytics.group().id()). Allowing you to adjust the mapping of account id in Segment's group event.",
+ type: 'boolean',
+ required: false,
+ default: false
}
},
initialize: async ({ settings, analytics }, deps) => {
- loadPendo(settings.apiKey, settings.region)
-
- await deps.resolveWhen(() => window.pendo != null, 100)
+ if (settings.cnameContentHost && !/^https?:/.exec(settings.cnameContentHost) && settings.cnameContentHost.length) {
+ settings.cnameContentHost = 'https://' + settings.cnameContentHost
+ }
- const initialData: InitializeData = {}
+ loadPendo(settings.apiKey, settings.region, settings.cnameContentHost)
- if (settings.setVisitorIdOnLoad) {
- let vistorId: string | null = null
+ await deps.resolveWhen(() => window.pendo != null, 100)
- switch (settings.setVisitorIdOnLoad) {
- case 'disabled':
- vistorId = null
- break
- case 'userIdOnly':
- vistorId = analytics.user().id() ?? null
- break
- case 'userIdOrAnonymousId':
- vistorId = analytics.user().id() ?? analytics.user().anonymousId() ?? null
- break
- case 'anonymousIdOnly':
- vistorId = analytics.user().anonymousId() ?? null
- break
- }
+ let visitorId: ID = null
+ let accountId: ID = null
- if (vistorId) {
- initialData.visitor = {
- id: vistorId
- }
- }
+ if (analytics.user().id()) {
+ visitorId = analytics.user().id()
+ } else if (analytics.user().anonymousId()) {
+ // Append Pendo anonymous visitor tag
+ // https://github.com/segmentio/analytics.js-integrations/blob/master/integrations/pendo/lib/index.js#L114
+ visitorId = '_PENDO_T_' + analytics.user().anonymousId()
}
- if (settings.accountId) {
- initialData.account = {
- id: settings.accountId
- }
+
+ if (analytics.group().id()) {
+ accountId = analytics.group().id()
}
- if (settings.parentAccountId) {
- initialData.parentAccount = {
- id: settings.parentAccountId
- }
+
+ const options: PendoOptions = {
+ visitor: {
+ ...(!settings.disableUserTraitsOnLoad ? analytics.user().traits() : {}),
+ id: visitorId
+ },
+ ...(accountId && !settings.disableGroupIdAndTraitsOnLoad
+ ? {
+ account: {
+ ...analytics.group().traits(),
+ id: accountId
+ }
+ }
+ : {})
}
- window.pendo.initialize(initialData)
+ window.pendo.initialize(options)
return window.pendo
},
@@ -119,7 +115,30 @@ export const destination: BrowserDestinationDefinition = {
track,
identify,
group
- }
+ },
+ presets: [
+ {
+ name: 'Send Track Event',
+ subscribe: 'type = "track"',
+ partnerAction: 'track',
+ mapping: defaultValues(track.fields),
+ type: 'automatic'
+ },
+ {
+ name: 'Send Identify Event',
+ subscribe: 'type = "identify"',
+ partnerAction: 'identify',
+ mapping: defaultValues(identify.fields),
+ type: 'automatic'
+ },
+ {
+ name: 'Send Group Event',
+ subscribe: 'type = "group"',
+ partnerAction: 'group',
+ mapping: defaultValues(group.fields),
+ type: 'automatic'
+ }
+ ]
}
export default browserDestination(destination)
diff --git a/packages/browser-destinations/destinations/pendo-web-actions/src/loadScript.ts b/packages/browser-destinations/destinations/pendo-web-actions/src/loadScript.ts
index 21a14f7fba..97ca3820a7 100644
--- a/packages/browser-destinations/destinations/pendo-web-actions/src/loadScript.ts
+++ b/packages/browser-destinations/destinations/pendo-web-actions/src/loadScript.ts
@@ -1,6 +1,6 @@
/* eslint-disable */
// @ts-nocheck
-export function loadPendo(apiKey, region) {
+export function loadPendo(apiKey, region, cnameContentHost) {
;(function (p, e, n, d, o) {
var v, w, x, y, z
o = p[d] = p[d] || {}
@@ -16,7 +16,7 @@ export function loadPendo(apiKey, region) {
})(v[w])
y = e.createElement(n)
y.async = !0
- y.src = `https://cdn.pendo.${region}/agent/static/` + apiKey + '/pendo.js'
+ y.src = `${cnameContentHost || region}/agent/static/${apiKey}/pendo.js`
z = e.getElementsByTagName(n)[0]
z.parentNode.insertBefore(y, z)
})(window, document, 'script', 'pendo')
diff --git a/packages/browser-destinations/destinations/pendo-web-actions/src/track/index.ts b/packages/browser-destinations/destinations/pendo-web-actions/src/track/index.ts
index bafe6bd850..2aed3ed065 100644
--- a/packages/browser-destinations/destinations/pendo-web-actions/src/track/index.ts
+++ b/packages/browser-destinations/destinations/pendo-web-actions/src/track/index.ts
@@ -3,7 +3,6 @@ import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'
import type { PendoSDK } from '../types'
-// Change from unknown to the partner SDK types
const action: BrowserActionDefinition = {
title: 'Send Track Event',
description: 'Send Segment track() events to Pendo',
@@ -17,7 +16,8 @@ const action: BrowserActionDefinition = {
required: true,
default: {
'@path': '$.event'
- }
+ },
+ readOnly: false
},
metadata: {
label: 'Metadata',
@@ -25,12 +25,12 @@ const action: BrowserActionDefinition = {
type: 'object',
default: {
'@path': '$.properties'
- }
+ },
+ readOnly: false
}
},
perform: (pendo, { payload }) => {
pendo.track(payload.event, payload.metadata)
- pendo.flushNow(true)
}
}
diff --git a/packages/browser-destinations/destinations/pendo-web-actions/src/types.ts b/packages/browser-destinations/destinations/pendo-web-actions/src/types.ts
index d4408805b2..3d9ed6dc91 100644
--- a/packages/browser-destinations/destinations/pendo-web-actions/src/types.ts
+++ b/packages/browser-destinations/destinations/pendo-web-actions/src/types.ts
@@ -1,27 +1,25 @@
+import { ID } from '@segment/analytics-next'
+
export type Visitor = {
- id?: string | null | undefined
+ id: ID
+ [propName: string]: unknown
}
export type Account = {
- id?: string | null | undefined
+ id: ID
+ [propName: string]: unknown
}
-export type InitializeData = {
+export type PendoOptions = {
visitor?: Visitor
account?: Account
parentAccount?: Account
}
-export type identifyPayload = {
- visitor: { [key: string]: string }
- account?: { [key: string]: string }
- parentAccount?: { [key: string]: string }
-}
-
export type PendoSDK = {
- initialize: ({ visitor, account }: InitializeData) => void
+ initialize: ({ visitor, account }: PendoOptions) => void
track: (eventName: string, metadata?: { [key: string]: unknown }) => void
- identify: (data: identifyPayload) => void
+ identify: (data: PendoOptions) => void
flushNow: (force: boolean) => void
isReady: () => boolean
}
diff --git a/packages/browser-destinations/destinations/pendo-web-actions/src/utils.ts b/packages/browser-destinations/destinations/pendo-web-actions/src/utils.ts
new file mode 100644
index 0000000000..5832639ebc
--- /dev/null
+++ b/packages/browser-destinations/destinations/pendo-web-actions/src/utils.ts
@@ -0,0 +1,42 @@
+export interface AnyObject {
+ [key: string]: AnyObject | undefined
+}
+
+export const removeNestedObject = function (obj: AnyObject, path: string): AnyObject {
+ const pathArray = path.split('.').filter(Boolean)
+
+ const newObj: AnyObject = { ...obj } // Create a new object to avoid mutating the original
+
+ let currentObj: AnyObject | undefined = newObj
+
+ for (let i = 0; i < pathArray.length; i++) {
+ const key = pathArray[i]
+
+ if (
+ typeof currentObj === 'object' &&
+ currentObj !== null &&
+ Object.prototype.hasOwnProperty.call(currentObj, key)
+ ) {
+ if (i == pathArray.length - 1) {
+ delete currentObj[key]
+ } else {
+ currentObj[key] = { ...currentObj[key] } as AnyObject // Create a new object for nested properties
+ currentObj = currentObj[key]
+ }
+ } else {
+ return newObj
+ }
+ }
+ return newObj
+}
+
+export const getSubstringDifference = (
+ str1: string | undefined | null,
+ str2: string | undefined | null
+): string | null => {
+ if (str1 === undefined || str1 === null || str2 === undefined || str2 === null) {
+ return null
+ }
+
+ return str1.startsWith(str2) ? str1.substring(str2.length) : null
+}
diff --git a/packages/browser-destinations/destinations/playerzero-web/package.json b/packages/browser-destinations/destinations/playerzero-web/package.json
index e9f55a9ebc..422b2cda18 100644
--- a/packages/browser-destinations/destinations/playerzero-web/package.json
+++ b/packages/browser-destinations/destinations/playerzero-web/package.json
@@ -1,6 +1,6 @@
{
"name": "@segment/analytics-browser-actions-playerzero",
- "version": "1.15.0",
+ "version": "1.34.0",
"license": "MIT",
"publishConfig": {
"access": "public",
@@ -15,8 +15,8 @@
},
"typings": "./dist/esm",
"dependencies": {
- "@segment/actions-core": "^3.83.0",
- "@segment/browser-destination-runtime": "^1.14.0"
+ "@segment/actions-core": "^3.103.0",
+ "@segment/browser-destination-runtime": "^1.33.0"
},
"peerDependencies": {
"@segment/analytics-next": ">=1.55.0"
diff --git a/packages/browser-destinations/destinations/replaybird/README.md b/packages/browser-destinations/destinations/replaybird/README.md
new file mode 100644
index 0000000000..b89dd05b88
--- /dev/null
+++ b/packages/browser-destinations/destinations/replaybird/README.md
@@ -0,0 +1,31 @@
+# @segment/analytics-browser-actions-replaybird
+
+The Replaybird browser action destination for use with @segment/analytics-next.
+
+## License
+
+MIT License
+
+Copyright (c) 2024 Segment
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+## Contributing
+
+All third party contributors acknowledge that any contributions they provide will be made under the same open source license that the open source project is provided under.
diff --git a/packages/browser-destinations/destinations/replaybird/package.json b/packages/browser-destinations/destinations/replaybird/package.json
new file mode 100644
index 0000000000..8ab4073d03
--- /dev/null
+++ b/packages/browser-destinations/destinations/replaybird/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "@segment/analytics-browser-actions-replaybird",
+ "version": "1.15.0",
+ "license": "MIT",
+ "publishConfig": {
+ "access": "public",
+ "registry": "https://registry.npmjs.org"
+ },
+ "main": "./dist/cjs",
+ "module": "./dist/esm",
+ "scripts": {
+ "build": "yarn build:esm && yarn build:cjs",
+ "build:cjs": "tsc --module commonjs --outDir ./dist/cjs",
+ "build:esm": "tsc --outDir ./dist/esm"
+ },
+ "typings": "./dist/esm",
+ "dependencies": {
+ "@segment/browser-destination-runtime": "^1.33.0"
+ },
+ "peerDependencies": {
+ "@segment/analytics-next": ">=1.55.0"
+ }
+}
diff --git a/packages/browser-destinations/destinations/replaybird/src/__tests__/index.test.ts b/packages/browser-destinations/destinations/replaybird/src/__tests__/index.test.ts
new file mode 100644
index 0000000000..687767ddeb
--- /dev/null
+++ b/packages/browser-destinations/destinations/replaybird/src/__tests__/index.test.ts
@@ -0,0 +1,21 @@
+import { Analytics, Context } from '@segment/analytics-next'
+import replaybird, { destination } from '../index'
+import { subscriptions, REPLAYBIRD_API_KEY, mockReplaybirdJsHttpRequest, createMockedReplaybirdJsSdk } from '../utils'
+
+describe('Replaybird', () => {
+ test('Load replaybird cdn script file', async () => {
+ jest.spyOn(destination, 'initialize')
+
+ mockReplaybirdJsHttpRequest()
+ window.replaybird = createMockedReplaybirdJsSdk()
+
+ const [event] = await replaybird({
+ apiKey: REPLAYBIRD_API_KEY,
+ subscriptions: subscriptions
+ })
+
+ await event.load(Context.system(), {} as Analytics)
+ expect(destination.initialize).toHaveBeenCalled()
+ expect(window.replaybird.apiKey).toEqual(REPLAYBIRD_API_KEY)
+ })
+})
diff --git a/packages/browser-destinations/destinations/replaybird/src/generated-types.ts b/packages/browser-destinations/destinations/replaybird/src/generated-types.ts
new file mode 100644
index 0000000000..8e9f33bd98
--- /dev/null
+++ b/packages/browser-destinations/destinations/replaybird/src/generated-types.ts
@@ -0,0 +1,8 @@
+// Generated file. DO NOT MODIFY IT BY HAND.
+
+export interface Settings {
+ /**
+ * The api key for replaybird
+ */
+ apiKey: string
+}
diff --git a/packages/browser-destinations/destinations/replaybird/src/identifyUser/__tests__/index.test.ts b/packages/browser-destinations/destinations/replaybird/src/identifyUser/__tests__/index.test.ts
new file mode 100644
index 0000000000..a43a0d6d39
--- /dev/null
+++ b/packages/browser-destinations/destinations/replaybird/src/identifyUser/__tests__/index.test.ts
@@ -0,0 +1,66 @@
+import { Analytics, Context } from '@segment/analytics-next'
+import replaybird, { destination } from '../../index'
+import {
+ identifySubscription,
+ REPLAYBIRD_API_KEY,
+ createMockedReplaybirdJsSdk,
+ mockReplaybirdJsHttpRequest
+} from '../../utils'
+
+describe('replaybird.identify', () => {
+ it('should not call identify if user id is not provided and anonymous user id is provided', async () => {
+ mockReplaybirdJsHttpRequest()
+ window.replaybird = createMockedReplaybirdJsSdk()
+
+ const [identifyUser] = await replaybird({
+ apiKey: REPLAYBIRD_API_KEY,
+ subscriptions: [identifySubscription]
+ })
+
+ await identifyUser.load(Context.system(), {} as Analytics)
+ const identifySpy = jest.spyOn(window.replaybird, 'identify')
+
+ const traits = {
+ name: 'Mathew',
+ email: 'user@example.com'
+ }
+
+ await identifyUser.identify?.(
+ new Context({
+ type: 'identify',
+ traits
+ })
+ )
+
+ expect(identifySpy).not.toHaveBeenCalled()
+ })
+
+ it('should call identify if user id is provided', async () => {
+ mockReplaybirdJsHttpRequest()
+ window.replaybird = createMockedReplaybirdJsSdk()
+
+ const [identifyUser] = await replaybird({
+ apiKey: REPLAYBIRD_API_KEY,
+ subscriptions: [identifySubscription]
+ })
+
+ jest.spyOn(destination.actions.identifyUser, 'perform')
+ await identifyUser.load(Context.system(), {} as Analytics)
+ const identifySpy = jest.spyOn(window.replaybird, 'identify')
+
+ const userId = 'user_123'
+ const traits = {
+ name: 'Mathew',
+ email: 'user@example.com'
+ }
+
+ await identifyUser.identify?.(
+ new Context({
+ type: 'identify',
+ traits,
+ userId
+ })
+ )
+ expect(identifySpy).toHaveBeenCalledWith(userId, traits)
+ })
+})
diff --git a/packages/browser-destinations/destinations/replaybird/src/identifyUser/generated-types.ts b/packages/browser-destinations/destinations/replaybird/src/identifyUser/generated-types.ts
new file mode 100644
index 0000000000..4b5078481c
--- /dev/null
+++ b/packages/browser-destinations/destinations/replaybird/src/identifyUser/generated-types.ts
@@ -0,0 +1,14 @@
+// Generated file. DO NOT MODIFY IT BY HAND.
+
+export interface Payload {
+ /**
+ * A unique ID for a known user
+ */
+ userId: string
+ /**
+ * The Segment traits to be forwarded to ReplayBird
+ */
+ traits?: {
+ [k: string]: unknown
+ }
+}
diff --git a/packages/browser-destinations/destinations/replaybird/src/identifyUser/index.ts b/packages/browser-destinations/destinations/replaybird/src/identifyUser/index.ts
new file mode 100644
index 0000000000..b5f13145b7
--- /dev/null
+++ b/packages/browser-destinations/destinations/replaybird/src/identifyUser/index.ts
@@ -0,0 +1,40 @@
+import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
+import type { Settings } from '../generated-types'
+import type { Payload } from './generated-types'
+import { ReplayBird } from '../types'
+
+// Change from unknown to the partner SDK types
+const action: BrowserActionDefinition = {
+ title: 'Identify User',
+ description: 'Sets user identifier and user profile details',
+ platform: 'web',
+ defaultSubscription: 'type = "identify"',
+ fields: {
+ userId: {
+ type: 'string',
+ required: true,
+ description: 'A unique ID for a known user',
+ label: 'User Id',
+ default: {
+ '@path': '$.userId'
+ }
+ },
+ traits: {
+ type: 'object',
+ required: false,
+ description: 'The Segment traits to be forwarded to ReplayBird',
+ label: 'User Traits',
+ default: {
+ '@path': '$.traits'
+ }
+ }
+ },
+ perform: (replaybird, event) => {
+ const payload = event.payload || {}
+ if (payload.userId) {
+ replaybird.identify(payload.userId, payload.traits || {})
+ }
+ }
+}
+
+export default action
diff --git a/packages/browser-destinations/destinations/replaybird/src/index.ts b/packages/browser-destinations/destinations/replaybird/src/index.ts
new file mode 100644
index 0000000000..22b0f703da
--- /dev/null
+++ b/packages/browser-destinations/destinations/replaybird/src/index.ts
@@ -0,0 +1,63 @@
+import type { Settings } from './generated-types'
+import type { BrowserDestinationDefinition } from '@segment/browser-destination-runtime/types'
+import { browserDestination } from '@segment/browser-destination-runtime/shim'
+import { ReplayBird } from './types'
+import { defaultValues } from '@segment/actions-core'
+import trackEvent from './trackEvent'
+import identifyUser from './identifyUser'
+
+declare global {
+ interface Window {
+ replaybird: ReplayBird
+ }
+}
+
+// Switch from unknown to the partner SDK client types
+export const destination: BrowserDestinationDefinition = {
+ name: 'ReplayBird Web (Actions)',
+ slug: 'actions-replaybird-web',
+ mode: 'device',
+ presets: [
+ {
+ name: 'Track Event',
+ subscribe: 'type = "track" or type = "page"',
+ partnerAction: 'trackEvent',
+ mapping: defaultValues(trackEvent.fields),
+ type: 'automatic'
+ },
+ {
+ name: 'Identify User',
+ subscribe: 'type = "identify"',
+ partnerAction: 'identifyUser',
+ mapping: defaultValues(identifyUser.fields),
+ type: 'automatic'
+ }
+ ],
+ settings: {
+ apiKey: {
+ description: 'The api key for replaybird',
+ label: 'API Key',
+ type: 'string',
+ required: true
+ }
+ },
+
+ initialize: async ({ settings }, deps) => {
+ // initialize client code here
+ await deps.loadScript(`https://cdn.replaybird.com/agent/latest/replaybird.js`)
+ await deps.resolveWhen(() => Object.prototype.hasOwnProperty.call(window, 'replaybird'), 100)
+
+ if (settings.apiKey) {
+ window.replaybird.init(settings.apiKey, {})
+ window.replaybird.apiKey = settings.apiKey
+ }
+ return window.replaybird
+ },
+
+ actions: {
+ trackEvent,
+ identifyUser
+ }
+}
+
+export default browserDestination(destination)
diff --git a/packages/browser-destinations/destinations/replaybird/src/trackEvent/__tests__/index.test.ts b/packages/browser-destinations/destinations/replaybird/src/trackEvent/__tests__/index.test.ts
new file mode 100644
index 0000000000..ff74128a51
--- /dev/null
+++ b/packages/browser-destinations/destinations/replaybird/src/trackEvent/__tests__/index.test.ts
@@ -0,0 +1,39 @@
+import { Analytics, Context } from '@segment/analytics-next'
+import replaybird from '../../index'
+import {
+ trackSubscription,
+ REPLAYBIRD_API_KEY,
+ createMockedReplaybirdJsSdk,
+ mockReplaybirdJsHttpRequest
+} from '../../utils'
+
+describe('replaybird.track', () => {
+ it('Should send events to replaybird', async () => {
+ mockReplaybirdJsHttpRequest()
+ window.replaybird = createMockedReplaybirdJsSdk()
+
+ const [event] = await replaybird({
+ apiKey: REPLAYBIRD_API_KEY,
+ subscriptions: [trackSubscription]
+ })
+
+ await event.load(Context.system(), {} as Analytics)
+ const trackSpy = jest.spyOn(window.replaybird, 'capture')
+
+ const name = 'Signup'
+ const properties = {
+ email: 'user@example.com',
+ name: 'Mathew',
+ country: 'USA'
+ }
+
+ await event.track?.(
+ new Context({
+ type: 'track',
+ name,
+ properties
+ })
+ )
+ expect(trackSpy).toHaveBeenCalledWith(name, properties)
+ })
+})
diff --git a/packages/browser-destinations/destinations/replaybird/src/trackEvent/generated-types.ts b/packages/browser-destinations/destinations/replaybird/src/trackEvent/generated-types.ts
new file mode 100644
index 0000000000..11c32b7e70
--- /dev/null
+++ b/packages/browser-destinations/destinations/replaybird/src/trackEvent/generated-types.ts
@@ -0,0 +1,22 @@
+// Generated file. DO NOT MODIFY IT BY HAND.
+
+export interface Payload {
+ /**
+ * The track() event name or page() name for the event.
+ */
+ name: string
+ /**
+ * A JSON object containing additional information about the event that will be indexed by replaybird.
+ */
+ properties?: {
+ [k: string]: unknown
+ }
+ /**
+ * A unique ID for a known user
+ */
+ userId?: string
+ /**
+ * A unique ID for a anonymous user
+ */
+ anonymousId?: string
+}
diff --git a/packages/browser-destinations/destinations/replaybird/src/trackEvent/index.ts b/packages/browser-destinations/destinations/replaybird/src/trackEvent/index.ts
new file mode 100644
index 0000000000..fefd63d50b
--- /dev/null
+++ b/packages/browser-destinations/destinations/replaybird/src/trackEvent/index.ts
@@ -0,0 +1,68 @@
+import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
+import type { Settings } from '../generated-types'
+import type { Payload } from './generated-types'
+import { ReplayBird } from '../types'
+
+// Change from unknown to the partner SDK types
+const action: BrowserActionDefinition = {
+ title: 'Send Track Events',
+ description: 'Send Segment track() events and / or Segment page() events to ReplayBird',
+ platform: 'web',
+ defaultSubscription: 'type = "track" or type = "page"',
+ fields: {
+ name: {
+ description: 'The track() event name or page() name for the event.',
+ label: 'Event Name',
+ required: true,
+ type: 'string',
+ default: {
+ '@if': {
+ exists: { '@path': '$.event' },
+ then: { '@path': '$.event' },
+ else: { '@path': '$.name' }
+ }
+ }
+ },
+ properties: {
+ description:
+ 'A JSON object containing additional information about the event that will be indexed by replaybird.',
+ label: 'Properties',
+ required: false,
+ type: 'object',
+ default: {
+ '@path': '$.properties'
+ }
+ },
+ userId: {
+ description: 'A unique ID for a known user',
+ label: 'User ID',
+ required: false,
+ type: 'string',
+ default: {
+ '@path': '$.userId'
+ }
+ },
+ anonymousId: {
+ description: 'A unique ID for a anonymous user',
+ label: 'Anonymous ID',
+ required: false,
+ type: 'string',
+ default: {
+ '@path': '$.anonymousId'
+ }
+ }
+ },
+ perform: (replaybird, event) => {
+ // Invoke Partner SDK here
+ const payload = event.payload
+ if (payload) {
+ replaybird.capture(payload.name, {
+ ...payload.properties,
+ userId: payload.userId,
+ anonymousId: payload.anonymousId
+ })
+ }
+ }
+}
+
+export default action
diff --git a/packages/browser-destinations/destinations/replaybird/src/types.ts b/packages/browser-destinations/destinations/replaybird/src/types.ts
new file mode 100644
index 0000000000..f1e13cc0e5
--- /dev/null
+++ b/packages/browser-destinations/destinations/replaybird/src/types.ts
@@ -0,0 +1,14 @@
+type EventProperties = {
+ [key: string]: unknown
+}
+
+type UserProperties = {
+ [k: string]: unknown
+}
+
+export type ReplayBird = {
+ apiKey: string
+ capture: (eventName: string, eventProperties: EventProperties) => void
+ identify: (userId: string, traits: UserProperties) => void
+ init: (apiKey: string, options: object) => void
+}
diff --git a/packages/browser-destinations/destinations/replaybird/src/utils.ts b/packages/browser-destinations/destinations/replaybird/src/utils.ts
new file mode 100644
index 0000000000..5b536c3bc2
--- /dev/null
+++ b/packages/browser-destinations/destinations/replaybird/src/utils.ts
@@ -0,0 +1,51 @@
+import { Subscription } from '@segment/browser-destination-runtime/types'
+import nock from 'nock'
+import { ReplayBird } from './types'
+
+export const trackSubscription: Subscription = {
+ partnerAction: 'trackEvent',
+ name: 'Track',
+ enabled: true,
+ subscribe: 'type = "track"',
+ mapping: {
+ name: {
+ '@path': '$.name'
+ },
+ properties: {
+ '@path': '$.properties'
+ }
+ }
+}
+
+export const identifySubscription: Subscription = {
+ partnerAction: 'identifyUser',
+ name: 'Identify',
+ enabled: true,
+ subscribe: 'type = "identify"',
+ mapping: {
+ userId: {
+ '@path': '$.userId'
+ },
+ traits: {
+ '@path': '$.traits'
+ }
+ }
+}
+
+export const REPLAYBIRD_API_KEY = 'secret'
+
+export const createMockedReplaybirdJsSdk = (): ReplayBird => {
+ return {
+ apiKey: REPLAYBIRD_API_KEY,
+ init: jest.fn(),
+ capture: jest.fn(),
+ identify: jest.fn()
+ }
+}
+
+// https://cdn.replaybird.com/agent/latest/replaybird.js
+export const mockReplaybirdJsHttpRequest = (): void => {
+ nock('https://cdn.replaybird.com').get(`/agent/latest/replaybird.js`).reply(200, {})
+}
+
+export const subscriptions = [trackSubscription, identifySubscription]
diff --git a/packages/browser-destinations/destinations/replaybird/tsconfig.json b/packages/browser-destinations/destinations/replaybird/tsconfig.json
new file mode 100644
index 0000000000..c2a7897afd
--- /dev/null
+++ b/packages/browser-destinations/destinations/replaybird/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.build.json",
+ "compilerOptions": {
+ "rootDir": "./src",
+ "baseUrl": "."
+ },
+ "include": ["src"],
+ "exclude": ["dist", "**/__tests__"]
+}
diff --git a/packages/browser-destinations/destinations/ripe/package.json b/packages/browser-destinations/destinations/ripe/package.json
index de56a05def..f9b14c2a03 100644
--- a/packages/browser-destinations/destinations/ripe/package.json
+++ b/packages/browser-destinations/destinations/ripe/package.json
@@ -1,6 +1,6 @@
{
"name": "@segment/analytics-browser-actions-ripe",
- "version": "1.15.0",
+ "version": "1.34.0",
"license": "MIT",
"publishConfig": {
"access": "public",
@@ -15,8 +15,8 @@
},
"typings": "./dist/esm",
"dependencies": {
- "@segment/actions-core": "^3.83.0",
- "@segment/browser-destination-runtime": "^1.14.0"
+ "@segment/actions-core": "^3.103.0",
+ "@segment/browser-destination-runtime": "^1.33.0"
},
"peerDependencies": {
"@segment/analytics-next": ">=1.55.0"
diff --git a/packages/browser-destinations/destinations/ripe/src/index.ts b/packages/browser-destinations/destinations/ripe/src/index.ts
index 204cb69fb2..7f1c0f534f 100644
--- a/packages/browser-destinations/destinations/ripe/src/index.ts
+++ b/packages/browser-destinations/destinations/ripe/src/index.ts
@@ -1,6 +1,6 @@
-import type { Settings } from './generated-types'
-import type { BrowserDestinationDefinition } from '@segment/browser-destination-runtime/types'
import { browserDestination } from '@segment/browser-destination-runtime/shim'
+import type { BrowserDestinationDefinition } from '@segment/browser-destination-runtime/types'
+import type { Settings } from './generated-types'
import { RipeSDK } from './types'
import group from './group'
diff --git a/packages/browser-destinations/destinations/rupt/README.md b/packages/browser-destinations/destinations/rupt/README.md
index cf045c87dc..f72bcdacb4 100644
--- a/packages/browser-destinations/destinations/rupt/README.md
+++ b/packages/browser-destinations/destinations/rupt/README.md
@@ -6,7 +6,7 @@ The Rupt browser action destination for use with @segment/analytics-next.
MIT License
-Copyright (c) 2023 Segment
+Copyright (c) 2024 Segment
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/packages/browser-destinations/destinations/rupt/package.json b/packages/browser-destinations/destinations/rupt/package.json
index e4c2b59b53..c50916663f 100644
--- a/packages/browser-destinations/destinations/rupt/package.json
+++ b/packages/browser-destinations/destinations/rupt/package.json
@@ -1,6 +1,6 @@
{
"name": "@segment/analytics-browser-actions-rupt",
- "version": "1.4.0",
+ "version": "1.23.0",
"license": "MIT",
"publishConfig": {
"access": "public",
@@ -15,8 +15,8 @@
},
"typings": "./dist/esm",
"dependencies": {
- "@segment/actions-core": "^3.83.0",
- "@segment/browser-destination-runtime": "^1.14.0"
+ "@segment/actions-core": "^3.103.0",
+ "@segment/browser-destination-runtime": "^1.33.0"
},
"peerDependencies": {
"@segment/analytics-next": ">=1.55.0"
diff --git a/packages/browser-destinations/destinations/screeb/package.json b/packages/browser-destinations/destinations/screeb/package.json
index 92787ef5ae..3c43a2bbef 100644
--- a/packages/browser-destinations/destinations/screeb/package.json
+++ b/packages/browser-destinations/destinations/screeb/package.json
@@ -1,6 +1,6 @@
{
"name": "@segment/analytics-browser-actions-screeb",
- "version": "1.15.0",
+ "version": "1.34.0",
"license": "MIT",
"publishConfig": {
"access": "public",
@@ -15,8 +15,8 @@
},
"typings": "./dist/esm",
"dependencies": {
- "@segment/actions-core": "^3.83.0",
- "@segment/browser-destination-runtime": "^1.14.0"
+ "@segment/actions-core": "^3.103.0",
+ "@segment/browser-destination-runtime": "^1.33.0"
},
"peerDependencies": {
"@segment/analytics-next": ">=1.55.0"
diff --git a/packages/browser-destinations/destinations/segment-utilities-web/package.json b/packages/browser-destinations/destinations/segment-utilities-web/package.json
index 4aa8d99d71..5e52b1ff4b 100644
--- a/packages/browser-destinations/destinations/segment-utilities-web/package.json
+++ b/packages/browser-destinations/destinations/segment-utilities-web/package.json
@@ -1,6 +1,6 @@
{
"name": "@segment/analytics-browser-actions-utils",
- "version": "1.15.0",
+ "version": "1.34.0",
"license": "MIT",
"publishConfig": {
"access": "public",
@@ -15,7 +15,7 @@
},
"typings": "./dist/esm",
"dependencies": {
- "@segment/browser-destination-runtime": "^1.14.0"
+ "@segment/browser-destination-runtime": "^1.33.0"
},
"peerDependencies": {
"@segment/analytics-next": ">=1.55.0"
diff --git a/packages/browser-destinations/destinations/snap-plugins/README.md b/packages/browser-destinations/destinations/snap-plugins/README.md
new file mode 100644
index 0000000000..251004a21e
--- /dev/null
+++ b/packages/browser-destinations/destinations/snap-plugins/README.md
@@ -0,0 +1,31 @@
+# @segment/analytics-browser-actions-snap-plugins
+
+The Snap Browser Plugins browser action destination for use with @segment/analytics-next.
+
+## License
+
+MIT License
+
+Copyright (c) 2024 Segment
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+## Contributing
+
+All third party contributors acknowledge that any contributions they provide will be made under the same open source license that the open source project is provided under.
diff --git a/packages/browser-destinations/destinations/snap-plugins/package.json b/packages/browser-destinations/destinations/snap-plugins/package.json
new file mode 100644
index 0000000000..287cae7ca9
--- /dev/null
+++ b/packages/browser-destinations/destinations/snap-plugins/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "@segment/analytics-browser-actions-snap-plugins",
+ "version": "1.15.0",
+ "license": "MIT",
+ "publishConfig": {
+ "access": "public",
+ "registry": "https://registry.npmjs.org"
+ },
+ "main": "./dist/cjs",
+ "module": "./dist/esm",
+ "scripts": {
+ "build": "yarn build:esm && yarn build:cjs",
+ "build:cjs": "tsc --module commonjs --outDir ./dist/cjs",
+ "build:esm": "tsc --outDir ./dist/esm"
+ },
+ "typings": "./dist/esm",
+ "dependencies": {
+ "@segment/browser-destination-runtime": "^1.33.0"
+ },
+ "peerDependencies": {
+ "@segment/analytics-next": ">=1.55.0"
+ }
+}
diff --git a/packages/browser-destinations/destinations/snap-plugins/src/__tests__/index.test.ts b/packages/browser-destinations/destinations/snap-plugins/src/__tests__/index.test.ts
new file mode 100644
index 0000000000..255d3d6220
--- /dev/null
+++ b/packages/browser-destinations/destinations/snap-plugins/src/__tests__/index.test.ts
@@ -0,0 +1,81 @@
+import { Analytics, Context, Plugin } from '@segment/analytics-next'
+import { Subscription } from '@segment/browser-destination-runtime/types'
+import browserPluginsDestination from '../'
+import { clickIdIntegrationFieldName, clickIdQuerystringName, scidCookieName, scidIntegrationFieldName } from '../utils'
+
+const example: Subscription[] = [
+ {
+ partnerAction: 'snapPlugin',
+ name: 'Snap Browser Plugin',
+ enabled: true,
+ subscribe: 'type = "track"',
+ mapping: {}
+ }
+]
+
+let browserActions: Plugin[]
+let snapPlugin: Plugin
+let ajs: Analytics
+
+beforeEach(async () => {
+ browserActions = await browserPluginsDestination({ subscriptions: example })
+ snapPlugin = browserActions[0]
+
+ ajs = new Analytics({
+ writeKey: 'w_123'
+ })
+
+ Object.defineProperty(window, 'location', {
+ value: {
+ search: ''
+ },
+ writable: true
+ })
+
+ document.cookie = `${scidCookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`
+})
+
+describe('ajs-integration', () => {
+ test('updates the original event with a Snap clientId from the querystring', async () => {
+ Object.defineProperty(window, 'location', {
+ value: {
+ search: `?${clickIdQuerystringName}=dummyQuerystringValue`
+ },
+ writable: true
+ })
+
+ await snapPlugin.load(Context.system(), ajs)
+
+ const ctx = new Context({
+ type: 'track',
+ event: 'Test Event',
+ properties: {
+ greeting: 'Yo!'
+ }
+ })
+
+ const updatedCtx = await snapPlugin.track?.(ctx)
+
+ const snapIntegrationsObj = updatedCtx?.event?.integrations['Snap Conversions Api']
+ expect(snapIntegrationsObj[clickIdIntegrationFieldName]).toEqual('dummyQuerystringValue')
+ })
+
+ test('updates the original event with a Snap cookie value', async () => {
+ document.cookie = `${scidCookieName}=dummyCookieValue`
+
+ await snapPlugin.load(Context.system(), ajs)
+
+ const ctx = new Context({
+ type: 'track',
+ event: 'Test Event',
+ properties: {
+ greeting: 'Yo!'
+ }
+ })
+
+ const updatedCtx = await snapPlugin.track?.(ctx)
+
+ const snapIntegrationsObj = updatedCtx?.event?.integrations['Snap Conversions Api']
+ expect(snapIntegrationsObj[scidIntegrationFieldName]).toEqual('dummyCookieValue')
+ })
+})
diff --git a/packages/browser-destinations/destinations/snap-plugins/src/generated-types.ts b/packages/browser-destinations/destinations/snap-plugins/src/generated-types.ts
new file mode 100644
index 0000000000..4ab2786ec6
--- /dev/null
+++ b/packages/browser-destinations/destinations/snap-plugins/src/generated-types.ts
@@ -0,0 +1,3 @@
+// Generated file. DO NOT MODIFY IT BY HAND.
+
+export interface Settings {}
diff --git a/packages/browser-destinations/destinations/snap-plugins/src/index.ts b/packages/browser-destinations/destinations/snap-plugins/src/index.ts
new file mode 100644
index 0000000000..5d80d6e814
--- /dev/null
+++ b/packages/browser-destinations/destinations/snap-plugins/src/index.ts
@@ -0,0 +1,42 @@
+import type { Settings } from './generated-types'
+import type { BrowserDestinationDefinition } from '@segment/browser-destination-runtime/types'
+import { browserDestination } from '@segment/browser-destination-runtime/shim'
+import {
+ storageSCIDCookieKey,
+ storageClickIdKey,
+ clickIdQuerystringName,
+ scidCookieName,
+ getCookieValue,
+ storageFallback
+} from './utils'
+import { UniversalStorage } from '@segment/analytics-next'
+import snapPlugin from './snapPlugin'
+
+// Switch from unknown to the partner SDK client types
+export const destination: BrowserDestinationDefinition = {
+ name: 'Snap Browser Plugins',
+ mode: 'device',
+ initialize: async ({ analytics }) => {
+ const storage = (analytics.storage as UniversalStorage>) ?? storageFallback
+
+ const scid: string | null = getCookieValue(scidCookieName)
+ if (scid) {
+ storage.set(storageSCIDCookieKey, scid)
+ }
+
+ const urlParams = new URLSearchParams(window.location.search)
+ const clickId: string | null = urlParams.get(clickIdQuerystringName) || null
+
+ if (clickId) {
+ storage.set(storageClickIdKey, clickId)
+ }
+
+ return {}
+ },
+ settings: {},
+ actions: {
+ snapPlugin
+ }
+}
+
+export default browserDestination(destination)
diff --git a/packages/browser-destinations/destinations/snap-plugins/src/snapPlugin/generated-types.ts b/packages/browser-destinations/destinations/snap-plugins/src/snapPlugin/generated-types.ts
new file mode 100644
index 0000000000..944d22b085
--- /dev/null
+++ b/packages/browser-destinations/destinations/snap-plugins/src/snapPlugin/generated-types.ts
@@ -0,0 +1,3 @@
+// Generated file. DO NOT MODIFY IT BY HAND.
+
+export interface Payload {}
diff --git a/packages/browser-destinations/destinations/snap-plugins/src/snapPlugin/index.ts b/packages/browser-destinations/destinations/snap-plugins/src/snapPlugin/index.ts
new file mode 100644
index 0000000000..71650a7a42
--- /dev/null
+++ b/packages/browser-destinations/destinations/snap-plugins/src/snapPlugin/index.ts
@@ -0,0 +1,45 @@
+import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
+import type { Settings } from '../generated-types'
+import type { Payload } from './generated-types'
+import {
+ storageSCIDCookieKey,
+ storageClickIdKey,
+ scidIntegrationFieldName,
+ clickIdIntegrationFieldName,
+ storageFallback
+} from '../utils'
+import { UniversalStorage } from '@segment/analytics-next'
+
+const action: BrowserActionDefinition = {
+ title: 'Snap Browser Plugin',
+ description: 'Enriches all Segment payloads with Snap click_id Querystring and _scid Cookie values',
+ platform: 'web',
+ hidden: false,
+ defaultSubscription: 'type = "track" or type = "identify" or type = "page" or type = "group" or type = "alias"',
+ fields: {},
+ lifecycleHook: 'enrichment',
+ perform: (_, { context, analytics }) => {
+ const storage = (analytics.storage as UniversalStorage>) ?? storageFallback
+
+ const scid: string | null = storage.get(storageSCIDCookieKey)
+
+ const clickId: string | null = storage.get(storageClickIdKey)
+
+ if (scid || clickId) {
+ const integrationsData: Record = {}
+ if (clickId) {
+ integrationsData[clickIdIntegrationFieldName] = clickId
+ }
+ if (scid) {
+ integrationsData[scidIntegrationFieldName] = scid
+ }
+ if (context.event.integrations?.All !== false || context.event.integrations['Snap Conversions Api']) {
+ context.updateEvent(`integrations.Snap Conversions Api`, integrationsData)
+ }
+ }
+
+ return
+ }
+}
+
+export default action
diff --git a/packages/browser-destinations/destinations/snap-plugins/src/utils.ts b/packages/browser-destinations/destinations/snap-plugins/src/utils.ts
new file mode 100644
index 0000000000..3662e89fd2
--- /dev/null
+++ b/packages/browser-destinations/destinations/snap-plugins/src/utils.ts
@@ -0,0 +1,41 @@
+// The name of the storage location where we'll cache the Snap click_id Querystring value
+export const storageClickIdKey = 'analytics_snap_capi_click_id'
+
+// The name of the storage location where we'll cache the Snap scid cookie value
+export const storageSCIDCookieKey = 'analytics_snap_capi_scid_cookie'
+
+// The name of the Snap click_id querystring to retrieve when the page loads
+export const clickIdQuerystringName = 'ScCid'
+
+// The name of the Snap cookie to retrieve to retrieve when the page loads
+export const scidCookieName = '_scid'
+
+// The field name to include for the Snap scid cookie in the context.integrations.snap_conversions_api
+export const scidIntegrationFieldName = 'uuid_c1'
+
+// The field name to include for the Snap click_id Querystring in the context.integrations.snap_conversions_api
+export const clickIdIntegrationFieldName = 'click_id'
+
+export const getCookieValue = (cookieName: string): string | null => {
+ const name = cookieName + '='
+ const decodedCookie = decodeURIComponent(document.cookie)
+ const cookieArray = decodedCookie.split('; ')
+
+ for (const cookie of cookieArray) {
+ if (cookie.startsWith(name)) {
+ return cookie.substring(name.length)
+ }
+ }
+
+ return null
+}
+
+export const storageFallback = {
+ get: (key: string) => {
+ const data = window.localStorage.getItem(key)
+ return data
+ },
+ set: (key: string, value: string) => {
+ return window.localStorage.setItem(key, value)
+ }
+}
diff --git a/packages/browser-destinations/destinations/snap-plugins/tsconfig.json b/packages/browser-destinations/destinations/snap-plugins/tsconfig.json
new file mode 100644
index 0000000000..c2a7897afd
--- /dev/null
+++ b/packages/browser-destinations/destinations/snap-plugins/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.build.json",
+ "compilerOptions": {
+ "rootDir": "./src",
+ "baseUrl": "."
+ },
+ "include": ["src"],
+ "exclude": ["dist", "**/__tests__"]
+}
diff --git a/packages/browser-destinations/destinations/sprig-web/package.json b/packages/browser-destinations/destinations/sprig-web/package.json
index 1a44035571..35ae27b462 100644
--- a/packages/browser-destinations/destinations/sprig-web/package.json
+++ b/packages/browser-destinations/destinations/sprig-web/package.json
@@ -1,6 +1,6 @@
{
"name": "@segment/analytics-browser-actions-sprig",
- "version": "1.15.0",
+ "version": "1.34.0",
"license": "MIT",
"publishConfig": {
"access": "public",
@@ -15,8 +15,8 @@
},
"typings": "./dist/esm",
"dependencies": {
- "@segment/actions-core": "^3.83.0",
- "@segment/browser-destination-runtime": "^1.14.0"
+ "@segment/actions-core": "^3.103.0",
+ "@segment/browser-destination-runtime": "^1.33.0"
},
"peerDependencies": {
"@segment/analytics-next": ">=1.55.0"
diff --git a/packages/browser-destinations/destinations/stackadapt/package.json b/packages/browser-destinations/destinations/stackadapt/package.json
index 9c9932b17d..8dc098ed75 100644
--- a/packages/browser-destinations/destinations/stackadapt/package.json
+++ b/packages/browser-destinations/destinations/stackadapt/package.json
@@ -1,6 +1,6 @@
{
"name": "@segment/analytics-browser-actions-stackadapt",
- "version": "1.15.0",
+ "version": "1.34.0",
"license": "MIT",
"publishConfig": {
"access": "public",
@@ -15,8 +15,8 @@
},
"typings": "./dist/esm",
"dependencies": {
- "@segment/actions-core": "^3.83.0",
- "@segment/browser-destination-runtime": "^1.14.0"
+ "@segment/actions-core": "^3.103.0",
+ "@segment/browser-destination-runtime": "^1.33.0"
},
"peerDependencies": {
"@segment/analytics-next": ">=1.55.0"
diff --git a/packages/browser-destinations/destinations/survicate/README.md b/packages/browser-destinations/destinations/survicate/README.md
new file mode 100644
index 0000000000..03ca772161
--- /dev/null
+++ b/packages/browser-destinations/destinations/survicate/README.md
@@ -0,0 +1,31 @@
+# @segment/analytics-browser-actions-survicate
+
+The Survicate browser action destination for use with @segment/analytics-next.
+
+## License
+
+MIT License
+
+Copyright (c) 2023 Segment
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+## Contributing
+
+All third party contributors acknowledge that any contributions they provide will be made under the same open source license that the open source project is provided under.
diff --git a/packages/browser-destinations/destinations/survicate/package.json b/packages/browser-destinations/destinations/survicate/package.json
new file mode 100644
index 0000000000..cecd5a675f
--- /dev/null
+++ b/packages/browser-destinations/destinations/survicate/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "@segment/analytics-browser-actions-survicate",
+ "version": "1.10.0",
+ "license": "MIT",
+ "publishConfig": {
+ "access": "public",
+ "registry": "https://registry.npmjs.org"
+ },
+ "main": "./dist/cjs",
+ "module": "./dist/esm",
+ "scripts": {
+ "build": "yarn build:esm && yarn build:cjs",
+ "build:cjs": "tsc --module commonjs --outDir ./dist/cjs",
+ "build:esm": "tsc --outDir ./dist/esm"
+ },
+ "typings": "./dist/esm",
+ "dependencies": {
+ "@segment/browser-destination-runtime": "^1.33.0"
+ },
+ "peerDependencies": {
+ "@segment/analytics-next": ">=1.55.0"
+ }
+}
diff --git a/packages/browser-destinations/destinations/survicate/src/__tests__/index.test.ts b/packages/browser-destinations/destinations/survicate/src/__tests__/index.test.ts
new file mode 100644
index 0000000000..95276927ef
--- /dev/null
+++ b/packages/browser-destinations/destinations/survicate/src/__tests__/index.test.ts
@@ -0,0 +1,110 @@
+import { Analytics, Context } from '@segment/analytics-next'
+import survicate, { destination } from '../index'
+import { Subscription } from '@segment/browser-destination-runtime/types'
+import { Survicate } from '../types'
+
+const example: Subscription[] = [
+ {
+ partnerAction: 'trackEvent',
+ name: 'Track Event',
+ enabled: true,
+ subscribe: 'type = "track"',
+ mapping: {
+ name: {
+ '@path': '$.name'
+ },
+ properties: {
+ '@path': '$.properties'
+ }
+ }
+ },
+ {
+ partnerAction: 'identifyUser',
+ name: 'Identify User',
+ enabled: true,
+ subscribe: 'type = "identify"',
+ mapping: {
+ traits: {
+ '@path': '$.traits'
+ }
+ }
+ }
+]
+
+describe('Survicate', () => {
+ let mockSurvicate: Survicate
+ beforeEach(async () => {
+ jest.restoreAllMocks()
+
+ const [trackEventPlugin] = await survicate({
+ workspaceKey: 'xMIeFQrceKnfKOuoYXZOVgqbsLlqYMGD',
+ subscriptions: example
+ })
+
+ jest.spyOn(destination, 'initialize').mockImplementation(() => {
+ mockSurvicate = {
+ invokeEvent: jest.fn(),
+ setVisitorTraits: jest.fn()
+ }
+ window._sva = mockSurvicate
+ return Promise.resolve(mockSurvicate)
+ })
+ await trackEventPlugin.load(Context.system(), {} as Analytics)
+ })
+
+ test('#load', async () => {
+ const [event] = await survicate({
+ workspaceKey: 'xMIeFQrceKnfKOuoYXZOVgqbsLlqYMGD',
+ subscriptions: example
+ })
+
+ jest.spyOn(destination.actions.trackEvent, 'perform')
+ jest.spyOn(destination, 'initialize')
+
+ await event.load(Context.system(), {} as Analytics)
+ expect(destination.initialize).toHaveBeenCalled()
+ expect(window).toHaveProperty('_sva')
+ })
+
+ it('#track', async () => {
+ const [event] = await survicate({
+ workspaceKey: 'xMIeFQrceKnfKOuoYXZOVgqbsLlqYMGD',
+ subscriptions: example
+ })
+
+ await event.load(Context.system(), {} as Analytics)
+ const sva = jest.spyOn(window._sva, 'invokeEvent')
+
+ await event.track?.(
+ new Context({
+ type: 'track',
+ name: 'event',
+ properties: {}
+ })
+ )
+
+ expect(sva).toHaveBeenCalledWith('segmentEvent-event', {})
+ })
+
+ it('#identify', async () => {
+ const [_, identifyUser] = await survicate({
+ workspaceKey: 'xMIeFQrceKnfKOuoYXZOVgqbsLlqYMGD',
+ subscriptions: example
+ })
+
+ await identifyUser.load(Context.system(), {} as Analytics)
+ const setVisitorTraits = jest.spyOn(window._sva, 'setVisitorTraits')
+
+ await identifyUser.identify?.(
+ new Context({
+ type: 'identify',
+ traits: {
+ date: '2024-01-01'
+ }
+ })
+ )
+
+ expect(setVisitorTraits).toHaveBeenCalled()
+ expect(setVisitorTraits).toHaveBeenCalledWith({ date: '2024-01-01' })
+ })
+})
diff --git a/packages/browser-destinations/destinations/survicate/src/generated-types.ts b/packages/browser-destinations/destinations/survicate/src/generated-types.ts
new file mode 100644
index 0000000000..3fa65f86e2
--- /dev/null
+++ b/packages/browser-destinations/destinations/survicate/src/generated-types.ts
@@ -0,0 +1,8 @@
+// Generated file. DO NOT MODIFY IT BY HAND.
+
+export interface Settings {
+ /**
+ * The workspace key for your Survicate account.
+ */
+ workspaceKey: string
+}
diff --git a/packages/browser-destinations/destinations/survicate/src/identifyGroup/generated-types.ts b/packages/browser-destinations/destinations/survicate/src/identifyGroup/generated-types.ts
new file mode 100644
index 0000000000..0d2482a791
--- /dev/null
+++ b/packages/browser-destinations/destinations/survicate/src/identifyGroup/generated-types.ts
@@ -0,0 +1,14 @@
+// Generated file. DO NOT MODIFY IT BY HAND.
+
+export interface Payload {
+ /**
+ * The Segment groupId to be forwarded to Survicate
+ */
+ groupId: string
+ /**
+ * The Segment traits to be forwarded to Survicate
+ */
+ traits: {
+ [k: string]: unknown
+ }
+}
diff --git a/packages/browser-destinations/destinations/survicate/src/identifyGroup/index.ts b/packages/browser-destinations/destinations/survicate/src/identifyGroup/index.ts
new file mode 100644
index 0000000000..8a957521f1
--- /dev/null
+++ b/packages/browser-destinations/destinations/survicate/src/identifyGroup/index.ts
@@ -0,0 +1,39 @@
+import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
+import type { Settings } from '../generated-types'
+import type { Payload } from './generated-types'
+import { Survicate } from 'src/types'
+
+const action: BrowserActionDefinition = {
+ title: 'Identify Group',
+ description: 'Send group traits to Survicate',
+ defaultSubscription: 'type = "group"',
+ platform: 'web',
+ fields: {
+ groupId: {
+ type: 'string',
+ required: true,
+ description: 'The Segment groupId to be forwarded to Survicate',
+ label: 'Group ID',
+ default: {
+ '@path': '$.groupId'
+ }
+ },
+ traits: {
+ type: 'object',
+ required: true,
+ description: 'The Segment traits to be forwarded to Survicate',
+ label: 'Traits',
+ default: {
+ '@path': '$.traits'
+ }
+ }
+ },
+ perform: (_, { payload }) => {
+ const groupTraits = Object.fromEntries(
+ Object.entries(payload.traits).map(([key, value]) => [`group_${key}`, value])
+ )
+ window._sva.setVisitorTraits({ groupId: payload.groupId, ...groupTraits })
+ }
+}
+
+export default action
diff --git a/packages/browser-destinations/destinations/survicate/src/identifyUser/generated-types.ts b/packages/browser-destinations/destinations/survicate/src/identifyUser/generated-types.ts
new file mode 100644
index 0000000000..531b64a2c6
--- /dev/null
+++ b/packages/browser-destinations/destinations/survicate/src/identifyUser/generated-types.ts
@@ -0,0 +1,10 @@
+// Generated file. DO NOT MODIFY IT BY HAND.
+
+export interface Payload {
+ /**
+ * The Segment traits to be forwarded to Survicate
+ */
+ traits: {
+ [k: string]: unknown
+ }
+}
diff --git a/packages/browser-destinations/destinations/survicate/src/identifyUser/index.ts b/packages/browser-destinations/destinations/survicate/src/identifyUser/index.ts
new file mode 100644
index 0000000000..439f5e3d4f
--- /dev/null
+++ b/packages/browser-destinations/destinations/survicate/src/identifyUser/index.ts
@@ -0,0 +1,27 @@
+import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
+import type { Settings } from '../generated-types'
+import type { Payload } from './generated-types'
+import { Survicate } from 'src/types'
+
+const action: BrowserActionDefinition = {
+ title: 'Identify User',
+ description: 'Set visitor traits with Segment Identify event',
+ defaultSubscription: 'type = "identify"',
+ platform: 'web',
+ fields: {
+ traits: {
+ type: 'object',
+ required: true,
+ description: 'The Segment traits to be forwarded to Survicate',
+ label: 'Traits',
+ default: {
+ '@path': '$.traits'
+ }
+ }
+ },
+ perform: (_, { payload }) => {
+ window._sva.setVisitorTraits(payload.traits)
+ }
+}
+
+export default action
diff --git a/packages/browser-destinations/destinations/survicate/src/index.ts b/packages/browser-destinations/destinations/survicate/src/index.ts
new file mode 100644
index 0000000000..0db23f0542
--- /dev/null
+++ b/packages/browser-destinations/destinations/survicate/src/index.ts
@@ -0,0 +1,72 @@
+import type { Settings } from './generated-types'
+import type { BrowserDestinationDefinition } from '@segment/browser-destination-runtime/types'
+import { browserDestination } from '@segment/browser-destination-runtime/shim'
+import { defaultValues } from '@segment/actions-core'
+import trackEvent from './trackEvent'
+import identifyUser from './identifyUser'
+import identifyGroup from './identifyGroup'
+import { Survicate } from './types'
+
+declare global {
+ interface Window {
+ _sva: Survicate
+ }
+}
+
+export const destination: BrowserDestinationDefinition = {
+ name: 'Survicate (Actions)',
+ slug: 'actions-survicate',
+ mode: 'device',
+ description: 'Send user traits to Survicate and trigger surveys with Segment events',
+
+ presets: [
+ {
+ name: 'Track Event',
+ subscribe: 'type = "track"',
+ partnerAction: 'trackEvent',
+ mapping: defaultValues(trackEvent.fields),
+ type: 'automatic'
+ },
+ {
+ name: 'Identify User',
+ subscribe: 'type = "identify"',
+ partnerAction: 'identifyUser',
+ mapping: defaultValues(identifyUser.fields),
+ type: 'automatic'
+ },
+ {
+ name: 'Identify Group',
+ subscribe: 'type = "group"',
+ partnerAction: 'identifyGroup',
+ mapping: defaultValues(identifyGroup.fields),
+ type: 'automatic'
+ }
+ ],
+
+ settings: {
+ workspaceKey: {
+ description: 'The workspace key for your Survicate account.',
+ label: 'Workspace Key',
+ type: 'string',
+ required: true
+ }
+ },
+
+ initialize: async ({ settings }, deps) => {
+ try {
+ await deps.loadScript(`https://survey.survicate.com/workspaces/${settings.workspaceKey}/web_surveys.js`)
+ await deps.resolveWhen(() => window._sva != undefined, 100)
+ return window._sva
+ } catch (error) {
+ throw new Error('Failed to load Survicate. ' + error)
+ }
+ },
+
+ actions: {
+ trackEvent,
+ identifyUser,
+ identifyGroup
+ }
+}
+
+export default browserDestination(destination)
diff --git a/packages/browser-destinations/destinations/survicate/src/trackEvent/generated-types.ts b/packages/browser-destinations/destinations/survicate/src/trackEvent/generated-types.ts
new file mode 100644
index 0000000000..95103600f7
--- /dev/null
+++ b/packages/browser-destinations/destinations/survicate/src/trackEvent/generated-types.ts
@@ -0,0 +1,14 @@
+// Generated file. DO NOT MODIFY IT BY HAND.
+
+export interface Payload {
+ /**
+ * The event name
+ */
+ name: string
+ /**
+ * Object containing the properties of the event
+ */
+ properties?: {
+ [k: string]: unknown
+ }
+}
diff --git a/packages/browser-destinations/destinations/survicate/src/trackEvent/index.ts b/packages/browser-destinations/destinations/survicate/src/trackEvent/index.ts
new file mode 100644
index 0000000000..641bf6626f
--- /dev/null
+++ b/packages/browser-destinations/destinations/survicate/src/trackEvent/index.ts
@@ -0,0 +1,37 @@
+import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
+import type { Settings } from '../generated-types'
+import type { Payload } from './generated-types'
+import { Survicate } from 'src/types'
+
+const action: BrowserActionDefinition = {
+ title: 'Track Event',
+ description: 'Invoke survey with Segment Track event',
+ platform: 'web',
+ defaultSubscription: 'type = "track"',
+ fields: {
+ name: {
+ description: 'The event name',
+ label: 'Event name',
+ required: true,
+ type: 'string',
+ default: {
+ '@path': '$.event'
+ }
+ },
+ properties: {
+ type: 'object',
+ required: false,
+ description: 'Object containing the properties of the event',
+ label: 'Event Properties',
+ default: {
+ '@path': '$.properties'
+ }
+ }
+ },
+ perform: (_, { payload: { name, properties } }) => {
+ const segmentProperties = properties || {}
+ window._sva.invokeEvent(`segmentEvent-${name}`, segmentProperties)
+ }
+}
+
+export default action
diff --git a/packages/browser-destinations/destinations/survicate/src/types.ts b/packages/browser-destinations/destinations/survicate/src/types.ts
new file mode 100644
index 0000000000..e74e6fa9ae
--- /dev/null
+++ b/packages/browser-destinations/destinations/survicate/src/types.ts
@@ -0,0 +1,4 @@
+export interface Survicate {
+ invokeEvent: (name: string, properties?: { [k: string]: unknown }) => void
+ setVisitorTraits: (traits: { [k: string]: unknown }) => void
+}
diff --git a/packages/browser-destinations/destinations/survicate/tsconfig.json b/packages/browser-destinations/destinations/survicate/tsconfig.json
new file mode 100644
index 0000000000..c2a7897afd
--- /dev/null
+++ b/packages/browser-destinations/destinations/survicate/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.build.json",
+ "compilerOptions": {
+ "rootDir": "./src",
+ "baseUrl": "."
+ },
+ "include": ["src"],
+ "exclude": ["dist", "**/__tests__"]
+}
diff --git a/packages/browser-destinations/destinations/tiktok-pixel/package.json b/packages/browser-destinations/destinations/tiktok-pixel/package.json
index c33e2f7d1f..7edac1722b 100644
--- a/packages/browser-destinations/destinations/tiktok-pixel/package.json
+++ b/packages/browser-destinations/destinations/tiktok-pixel/package.json
@@ -1,6 +1,6 @@
{
"name": "@segment/analytics-browser-actions-tiktok-pixel",
- "version": "1.10.0",
+ "version": "1.31.0",
"license": "MIT",
"publishConfig": {
"access": "public",
@@ -15,8 +15,8 @@
},
"typings": "./dist/esm",
"dependencies": {
- "@segment/actions-core": "^3.83.0",
- "@segment/browser-destination-runtime": "^1.14.0"
+ "@segment/actions-core": "^3.103.0",
+ "@segment/browser-destination-runtime": "^1.33.0"
},
"peerDependencies": {
"@segment/analytics-next": ">=1.55.0"
diff --git a/packages/browser-destinations/destinations/tiktok-pixel/src/generated-types.ts b/packages/browser-destinations/destinations/tiktok-pixel/src/generated-types.ts
index a410308693..608ad2bb52 100644
--- a/packages/browser-destinations/destinations/tiktok-pixel/src/generated-types.ts
+++ b/packages/browser-destinations/destinations/tiktok-pixel/src/generated-types.ts
@@ -6,7 +6,7 @@ export interface Settings {
*/
pixelCode: string
/**
- * Select "true" to use existing Pixel that is already installed on your website.
+ * Important! Changing this setting may block data collection to Segment if not done correctly. Select "true" to use an existing TikTok Pixel which is already installed on your website. The Pixel MUST be installed on your website when this is set to "true" or all data collection to Segment may fail.
*/
useExistingPixel?: boolean
}
diff --git a/packages/browser-destinations/destinations/tiktok-pixel/src/index.ts b/packages/browser-destinations/destinations/tiktok-pixel/src/index.ts
index 90ef30ce3e..6c5c13a28b 100644
--- a/packages/browser-destinations/destinations/tiktok-pixel/src/index.ts
+++ b/packages/browser-destinations/destinations/tiktok-pixel/src/index.ts
@@ -139,7 +139,8 @@ export const destination: BrowserDestinationDefinition =
useExistingPixel: {
label: 'Use Existing Pixel',
type: 'boolean',
- description: 'Select "true" to use existing Pixel that is already installed on your website.'
+ description:
+ 'Important! Changing this setting may block data collection to Segment if not done correctly. Select "true" to use an existing TikTok Pixel which is already installed on your website. The Pixel MUST be installed on your website when this is set to "true" or all data collection to Segment may fail.'
}
},
initialize: async ({ settings }, deps) => {
diff --git a/packages/browser-destinations/destinations/tiktok-pixel/src/reportWebEvent/__tests__/index.test.ts b/packages/browser-destinations/destinations/tiktok-pixel/src/reportWebEvent/__tests__/index.test.ts
index 43c0567a68..a7acd528c6 100644
--- a/packages/browser-destinations/destinations/tiktok-pixel/src/reportWebEvent/__tests__/index.test.ts
+++ b/packages/browser-destinations/destinations/tiktok-pixel/src/reportWebEvent/__tests__/index.test.ts
@@ -89,6 +89,7 @@ describe('TikTokPixel.reportWebEvent', () => {
messageId: 'ajs-71f386523ee5dfa90c7d0fda28b6b5c6',
type: 'track',
anonymousId: 'anonymousId',
+ userId: 'userId',
event: 'Order Completed',
properties: {
products: [
@@ -125,7 +126,8 @@ describe('TikTokPixel.reportWebEvent', () => {
expect(mockTtp.identify).toHaveBeenCalledWith({
email: 'aaa@aaa.com',
- phone_number: '+12345678900'
+ phone_number: '+12345678900',
+ external_id: 'userId'
})
expect(mockTtp.track).toHaveBeenCalledWith(
'PlaceAnOrder',
@@ -209,6 +211,7 @@ describe('TikTokPixel.reportWebEvent', () => {
messageId: 'ajs-71f386523ee5dfa90c7d0fda28b6b5c6',
type: 'track',
anonymousId: 'anonymousId',
+ userId: 'userId',
event: 'Product Added',
properties: {
product_id: '123',
@@ -235,7 +238,8 @@ describe('TikTokPixel.reportWebEvent', () => {
expect(mockTtp.identify).toHaveBeenCalledWith({
email: 'aaa@aaa.com',
- phone_number: '+12345678900'
+ phone_number: '+12345678900',
+ external_id: 'userId'
})
expect(mockTtp.track).toHaveBeenCalledWith(
'AddToCart',
@@ -316,6 +320,7 @@ describe('TikTokPixel.reportWebEvent', () => {
messageId: 'ajs-71f386523ee5dfa90c7d0fda28b6b5c6',
type: 'page',
anonymousId: 'anonymousId',
+ userId: 'userId',
properties: {
product_id: '123',
category: 'product',
@@ -341,7 +346,8 @@ describe('TikTokPixel.reportWebEvent', () => {
expect(mockTtp.identify).toHaveBeenCalledWith({
email: 'aaa@aaa.com',
- phone_number: '+12345678900'
+ phone_number: '+12345678900',
+ external_id: 'userId'
})
expect(mockTtp.track).toHaveBeenCalledWith(
'ViewContent',
diff --git a/packages/browser-destinations/destinations/tiktok-pixel/src/reportWebEvent/index.ts b/packages/browser-destinations/destinations/tiktok-pixel/src/reportWebEvent/index.ts
index 3c90c34f41..a6a5b404f5 100644
--- a/packages/browser-destinations/destinations/tiktok-pixel/src/reportWebEvent/index.ts
+++ b/packages/browser-destinations/destinations/tiktok-pixel/src/reportWebEvent/index.ts
@@ -136,7 +136,8 @@ const action: BrowserActionDefinition = {
if (payload.email || payload.phone_number) {
ttq.identify({
email: payload.email,
- phone_number: formatPhone(payload.phone_number)
+ phone_number: formatPhone(payload.phone_number),
+ external_id: payload.external_id
})
}
diff --git a/packages/browser-destinations/destinations/tiktok-pixel/src/types.ts b/packages/browser-destinations/destinations/tiktok-pixel/src/types.ts
index 4c7ff44891..c1ceacf601 100644
--- a/packages/browser-destinations/destinations/tiktok-pixel/src/types.ts
+++ b/packages/browser-destinations/destinations/tiktok-pixel/src/types.ts
@@ -1,6 +1,14 @@
export interface TikTokPixel {
page: () => void
- identify: ({ email, phone_number }: { email: string | undefined; phone_number: string | undefined }) => void
+ identify: ({
+ email,
+ phone_number,
+ external_id
+ }: {
+ email: string | undefined
+ phone_number: string | undefined
+ external_id: string | undefined
+ }) => void
track: (
event: string,
{
diff --git a/packages/browser-destinations/destinations/upollo/package.json b/packages/browser-destinations/destinations/upollo/package.json
index 8592d59b9e..57c5bd5efc 100644
--- a/packages/browser-destinations/destinations/upollo/package.json
+++ b/packages/browser-destinations/destinations/upollo/package.json
@@ -1,6 +1,6 @@
{
"name": "@segment/analytics-browser-actions-upollo",
- "version": "1.15.0",
+ "version": "1.34.0",
"license": "MIT",
"publishConfig": {
"access": "public",
@@ -15,8 +15,8 @@
},
"typings": "./dist/esm",
"dependencies": {
- "@segment/actions-core": "^3.83.0",
- "@segment/browser-destination-runtime": "^1.14.0"
+ "@segment/actions-core": "^3.103.0",
+ "@segment/browser-destination-runtime": "^1.33.0"
},
"peerDependencies": {
"@segment/analytics-next": ">=1.55.0"
diff --git a/packages/browser-destinations/destinations/userpilot/package.json b/packages/browser-destinations/destinations/userpilot/package.json
index ba33314c80..e78d5cef82 100644
--- a/packages/browser-destinations/destinations/userpilot/package.json
+++ b/packages/browser-destinations/destinations/userpilot/package.json
@@ -1,6 +1,6 @@
{
"name": "@segment/analytics-browser-actions-userpilot",
- "version": "1.15.0",
+ "version": "1.34.0",
"license": "MIT",
"publishConfig": {
"access": "public",
@@ -15,8 +15,8 @@
},
"typings": "./dist/esm",
"dependencies": {
- "@segment/actions-core": "^3.83.0",
- "@segment/browser-destination-runtime": "^1.14.0"
+ "@segment/actions-core": "^3.103.0",
+ "@segment/browser-destination-runtime": "^1.33.0"
},
"peerDependencies": {
"@segment/analytics-next": ">=1.55.0"
diff --git a/packages/browser-destinations/destinations/userpilot/src/index.ts b/packages/browser-destinations/destinations/userpilot/src/index.ts
index 61e893ce5e..1f4d9e71ef 100644
--- a/packages/browser-destinations/destinations/userpilot/src/index.ts
+++ b/packages/browser-destinations/destinations/userpilot/src/index.ts
@@ -31,6 +31,13 @@ export const destination: BrowserDestinationDefinition = {
mapping: defaultValues(identifyUser.fields),
type: 'automatic'
},
+ {
+ name: 'Identify Company',
+ subscribe: 'type = "group"',
+ partnerAction: 'identifyCompany',
+ mapping: defaultValues(identifyCompany.fields),
+ type: 'automatic'
+ },
{
name: 'Track Event',
subscribe: 'type = "track"',
diff --git a/packages/browser-destinations/destinations/vwo/package.json b/packages/browser-destinations/destinations/vwo/package.json
index d7914cffdd..408e916fe4 100644
--- a/packages/browser-destinations/destinations/vwo/package.json
+++ b/packages/browser-destinations/destinations/vwo/package.json
@@ -1,6 +1,6 @@
{
"name": "@segment/analytics-browser-actions-vwo",
- "version": "1.16.0",
+ "version": "1.35.0",
"license": "MIT",
"publishConfig": {
"access": "public",
@@ -15,8 +15,8 @@
},
"typings": "./dist/esm",
"dependencies": {
- "@segment/actions-core": "^3.83.0",
- "@segment/browser-destination-runtime": "^1.14.0"
+ "@segment/actions-core": "^3.103.0",
+ "@segment/browser-destination-runtime": "^1.33.0"
},
"peerDependencies": {
"@segment/analytics-next": ">=1.55.0"
diff --git a/packages/browser-destinations/destinations/wisepops/package.json b/packages/browser-destinations/destinations/wisepops/package.json
index ff7ed4f1fc..a4432e9e8c 100644
--- a/packages/browser-destinations/destinations/wisepops/package.json
+++ b/packages/browser-destinations/destinations/wisepops/package.json
@@ -1,6 +1,6 @@
{
"name": "@segment/analytics-browser-actions-wiseops",
- "version": "1.15.0",
+ "version": "1.34.0",
"license": "MIT",
"publishConfig": {
"access": "public",
@@ -15,8 +15,8 @@
},
"typings": "./dist/esm",
"dependencies": {
- "@segment/actions-core": "^3.83.0",
- "@segment/browser-destination-runtime": "^1.14.0"
+ "@segment/actions-core": "^3.103.0",
+ "@segment/browser-destination-runtime": "^1.33.0"
},
"peerDependencies": {
"@segment/analytics-next": ">=1.55.0"
diff --git a/packages/browser-destinations/package.json b/packages/browser-destinations/package.json
index 938160f6c1..69460a82e1 100644
--- a/packages/browser-destinations/package.json
+++ b/packages/browser-destinations/package.json
@@ -20,7 +20,8 @@
"prepublishOnly": "yarn build",
"test": "jest",
"typecheck": "tsc -p tsconfig.build.json --noEmit",
- "dev": "NODE_ENV=development NODE_OPTIONS=--openssl-legacy-provider concurrently \"webpack serve\" \"webpack -c webpack.config.js --watch\""
+ "dev": "NODE_ENV=development NODE_OPTIONS=--openssl-legacy-provider concurrently \"webpack serve\" \"webpack -c webpack.config.js --watch\"",
+ "size": "size-limit"
},
"dependencies": {
"tslib": "^2.3.1",
@@ -32,14 +33,15 @@
"@babel/plugin-transform-modules-commonjs": "^7.13.8",
"@babel/preset-env": "^7.13.10",
"@babel/preset-typescript": "^7.13.0",
- "@types/gtag.js": "^0.0.13",
+ "@size-limit/preset-big-lib": "^11.0.1",
+ "@types/gtag.js": "^0.0.19",
"@types/jest": "^27.0.0",
- "babel-jest": "^27.3.1",
"compression-webpack-plugin": "^7.1.2",
"concurrently": "^6.3.0",
"globby": "^11.0.2",
"jest": "^27.3.1",
"serve": "^12.0.1",
+ "size-limit": "^11.0.1",
"terser-webpack-plugin": "^5.1.1",
"ts-loader": "^9.2.6",
"webpack": "^5.82.0",
@@ -63,8 +65,13 @@
"@segment/actions-shared": "/../actions-shared/src",
"@segment/browser-destination-runtime/(.*)": "/../browser-destination-runtime/src/$1"
},
+ "globals": {
+ "ts-jest": {
+ "isolatedModules": true
+ }
+ },
"transform": {
- "^.+\\.[t|j]sx?$": "babel-jest"
+ "^.+\\.[t|j]sx?$": "ts-jest"
},
"transformIgnorePatterns": [
"/node_modules/(?!(@segment/analytics-next|@braze/web-sdk/)).+\\.js$"
@@ -73,5 +80,11 @@
"/test/setup-after-env.ts"
],
"forceExit": true
- }
+ },
+ "size-limit": [
+ {
+ "path": "dist/web/*/*.js",
+ "limit": "150 KB"
+ }
+ ]
}
diff --git a/packages/browser-destinations/tsconfig.build.json b/packages/browser-destinations/tsconfig.build.json
index 8c38965829..907c54e8a2 100644
--- a/packages/browser-destinations/tsconfig.build.json
+++ b/packages/browser-destinations/tsconfig.build.json
@@ -10,13 +10,20 @@
"lib": ["es2020", "dom"],
"baseUrl": ".",
"paths": {
- "@segment/actions-core/*": ["../core/src/*"]
+ "@segment/actions-core": ["../core/src"],
+ "@segment/actions-core/*": ["../core/src/*"],
+ "@segment/actions-shared": ["../actions-shared/src"],
+ "@segment/action-destinations": ["../destination-actions/src"],
+ "@segment/destination-subscriptions": ["../destination-subscriptions/src"],
+ "@segment/browser-destination-runtime": ["../browser-destination-runtime/src"],
+ "@segment/browser-destination-runtime/*": ["../browser-destination-runtime/src/*"]
}
},
"exclude": ["**/__tests__/**/*.ts"],
"include": ["src"],
"references": [
{ "path": "../core/tsconfig.build.json" },
- { "path": "../destination-subscriptions/tsconfig.build.json" }
+ { "path": "../destination-subscriptions/tsconfig.build.json" },
+ { "path": "../browser-destination-runtime/tsconfig.build.json" }
]
}
diff --git a/packages/browser-destinations/tsconfig.json b/packages/browser-destinations/tsconfig.json
index af9bff8d07..2d4ba95c55 100644
--- a/packages/browser-destinations/tsconfig.json
+++ b/packages/browser-destinations/tsconfig.json
@@ -1,6 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
+ "allowJs": true, // remove ts-jest warning
"module": "esnext",
"removeComments": false,
"baseUrl": ".",
diff --git a/packages/cli-internal/package.json b/packages/cli-internal/package.json
index 4721224e68..560b93b579 100644
--- a/packages/cli-internal/package.json
+++ b/packages/cli-internal/package.json
@@ -49,7 +49,7 @@
"rimraf": "^3.0.2"
},
"dependencies": {
- "@oclif/command": "^1.8.25",
+ "@oclif/command": "1.8.36",
"@oclif/config": "^1.18.8",
"@oclif/errors": "^1.3.6",
"@oclif/plugin-help": "^3.3",
@@ -90,6 +90,11 @@
},
"jest": {
"preset": "ts-jest",
+ "globals": {
+ "ts-jest": {
+ "isolatedModules": true
+ }
+ },
"testRegex": "((\\.|/)(test))\\.(tsx?|json)$",
"modulePathIgnorePatterns": [
"/dist/"
diff --git a/packages/cli/README.md b/packages/cli/README.md
index e15ae4a488..99646ac445 100644
--- a/packages/cli/README.md
+++ b/packages/cli/README.md
@@ -154,7 +154,7 @@ _See code: [src/commands/serve.ts](https://github.com/segmentio/action-destinati
MIT License
-Copyright (c) 2023 Segment
+Copyright (c) 2024 Segment
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/packages/cli/package.json b/packages/cli/package.json
index c2807198ed..b02ee69e03 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -26,7 +26,7 @@
"scripts": {
"postpack": "rm -f oclif.manifest.json",
"prepack": "yarn build && oclif-dev manifest && oclif-dev readme",
- "build": "yarn clean && yarn tsc -b tsconfig.build.json",
+ "build": "yarn tsc -b tsconfig.build.json",
"clean": "tsc -b tsconfig.build.json --clean",
"postclean": "rm -rf dist",
"create:destination": "./bin/run init",
@@ -52,7 +52,7 @@
"rimraf": "^3.0.2"
},
"dependencies": {
- "@oclif/command": "^1.8.25",
+ "@oclif/command": "1.8.36",
"@oclif/config": "^1.18.8",
"@oclif/errors": "^1.3.6",
"@oclif/plugin-help": "^3.3",
diff --git a/packages/cli/src/__tests__/init.test.ts b/packages/cli/src/__tests__/init.test.ts
index 2a47b109ce..05d21100b4 100644
--- a/packages/cli/src/__tests__/init.test.ts
+++ b/packages/cli/src/__tests__/init.test.ts
@@ -14,6 +14,8 @@ import * as path from 'path'
import * as prompt from '../lib/prompt'
import * as rimraf from 'rimraf'
+jest.setTimeout(10000)
+
describe('cli init command', () => {
const testDir = path.join('.', 'testResults')
beforeAll(() => {
diff --git a/packages/cli/src/commands/generate/action.ts b/packages/cli/src/commands/generate/action.ts
index 78a2d11fe9..3b938e782f 100644
--- a/packages/cli/src/commands/generate/action.ts
+++ b/packages/cli/src/commands/generate/action.ts
@@ -21,7 +21,8 @@ export default class GenerateAction extends Command {
`$ ./bin/run generate:action postToChannel server --directory=./destinations/slack`
]
- static flags = {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ static flags: flags.Input = {
help: flags.help({ char: 'h' }),
force: flags.boolean({ char: 'f' }),
title: flags.string({ char: 't', description: 'the display name of the action' }),
@@ -44,7 +45,7 @@ export default class GenerateAction extends Command {
return integrationDirs
}
- parseArgs() {
+ parseArgs(): flags.Output {
return this.parse(GenerateAction)
}
diff --git a/packages/cli/src/commands/generate/types.ts b/packages/cli/src/commands/generate/types.ts
index 407bf245f0..33ef5ce66e 100644
--- a/packages/cli/src/commands/generate/types.ts
+++ b/packages/cli/src/commands/generate/types.ts
@@ -11,7 +11,8 @@ import path from 'path'
import prettier from 'prettier'
import { loadDestination, hasOauthAuthentication } from '../../lib/destinations'
import { RESERVED_FIELD_NAMES } from '../../constants'
-import { AudienceDestinationDefinition } from '@segment/actions-core/destination-kit'
+import { AudienceDestinationDefinition, ActionHookType } from '@segment/actions-core/destination-kit'
+import { ActionHookDefinition, hookTypeStrings } from '@segment/actions-core/destination-kit'
const pretterOptions = prettier.resolveConfig.sync(process.cwd())
@@ -131,7 +132,51 @@ export default class GenerateTypes extends Command {
// TODO how to load directory structure consistently?
for (const [slug, action] of Object.entries(destination.actions)) {
- const types = await generateTypes(action.fields, 'Payload')
+ const fields = action.fields
+
+ let types = await generateTypes(fields, 'Payload')
+
+ if (action.hooks) {
+ const hooks: ActionHookDefinition = action.hooks
+ let hookBundle = ''
+ const hookFields: Record = {}
+ for (const [hookName, hook] of Object.entries(hooks)) {
+ if (!hookTypeStrings.includes(hookName as ActionHookType)) {
+ throw new Error(`Hook name ${hookName} is not a valid ActionHookType`)
+ }
+
+ const inputs = hook.inputFields
+ const outputs = hook.outputTypes
+ if (!inputs && !outputs) {
+ continue
+ }
+
+ const hookSchema = {
+ type: 'object',
+ required: true,
+ properties: {
+ inputs: {
+ label: `${hookName} hook inputs`,
+ type: 'object',
+ properties: inputs
+ },
+ outputs: {
+ label: `${hookName} hook outputs`,
+ type: 'object',
+ properties: outputs
+ }
+ }
+ }
+ hookFields[hookName] = hookSchema
+ }
+ hookBundle = await generateTypes(
+ hookFields,
+ 'HookBundle',
+ `// Generated bundle for hooks. DO NOT MODIFY IT BY HAND.`
+ )
+ types += hookBundle
+ }
+
if (fs.pathExistsSync(path.join(parentDir, `${slug}`))) {
fs.writeFileSync(path.join(parentDir, slug, 'generated-types.ts'), types)
} else {
@@ -141,11 +186,11 @@ export default class GenerateTypes extends Command {
}
}
-async function generateTypes(fields: Record = {}, name: string) {
+async function generateTypes(fields: Record = {}, name: string, bannerComment?: string) {
const schema = prepareSchema(fields)
return compile(schema, name, {
- bannerComment: '// Generated file. DO NOT MODIFY IT BY HAND.',
+ bannerComment: bannerComment ?? '// Generated file. DO NOT MODIFY IT BY HAND.',
style: pretterOptions ?? undefined
})
}
diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts
index cbdd7f48fb..d423ca0144 100644
--- a/packages/cli/src/commands/init.ts
+++ b/packages/cli/src/commands/init.ts
@@ -42,7 +42,7 @@ export default class Init extends Command {
}
]
- parseFlags() {
+ parseFlags(): flags.Output {
return this.parse(Init)
}
diff --git a/packages/cli/src/commands/scaffold.ts b/packages/cli/src/commands/scaffold.ts
index c3e2ff25a7..5b0a4c4ca2 100644
--- a/packages/cli/src/commands/scaffold.ts
+++ b/packages/cli/src/commands/scaffold.ts
@@ -58,7 +58,7 @@ export default class Init extends Command {
return integrationDirs
}
- parseFlags() {
+ parseFlags(): flags.Output {
return this.parse(Init)
}
diff --git a/packages/cli/src/commands/serve.ts b/packages/cli/src/commands/serve.ts
index 58f4c41c8e..9da645660d 100644
--- a/packages/cli/src/commands/serve.ts
+++ b/packages/cli/src/commands/serve.ts
@@ -57,7 +57,7 @@ export default class Serve extends Command {
})
const { selectedDestination } = await autoPrompt<{ selectedDestination: { name: string } }>(flags, {
- type: 'select',
+ type: 'autocomplete',
name: 'selectedDestination',
message: 'Which destination?',
choices: integrationDirs.map((integrationPath) => {
diff --git a/packages/cli/src/lib/server.ts b/packages/cli/src/lib/server.ts
index 7cc195ae96..8a68fa4aa9 100644
--- a/packages/cli/src/lib/server.ts
+++ b/packages/cli/src/lib/server.ts
@@ -17,7 +17,12 @@ import {
import asyncHandler from './async-handler'
import getExchanges from './summarize-http'
import { AggregateAjvError } from '../../../ajv-human-errors/src/aggregate-ajv-error'
-import { AudienceDestinationConfigurationWithCreateGet } from '@segment/actions-core/destination-kit'
+import {
+ ActionHookType,
+ ActionHookResponse,
+ AudienceDestinationConfigurationWithCreateGet,
+ RequestFn
+} from '@segment/actions-core/destination-kit'
interface ResponseError extends Error {
status?: number
}
@@ -291,12 +296,14 @@ function setupRoutes(def: DestinationDefinition | null): void {
settings: req.body.settings || {},
audienceSettings: req.body.payload?.context?.personas?.audience_settings || {},
mapping: mapping || req.body.payload || {},
- auth: req.body.auth || {}
+ auth: req.body.auth || {},
+ features: req.body.features || {}
}
if (Array.isArray(eventParams.data)) {
// If no mapping or default mapping is provided, default to using the first payload across all events.
eventParams.mapping = mapping || eventParams.data[0] || {}
+ eventParams.audienceSettings = req.body.payload[0]?.context?.personas?.audience_settings || {}
await action.executeBatch(eventParams)
} else {
await action.execute(eventParams)
@@ -343,6 +350,78 @@ function setupRoutes(def: DestinationDefinition | null): void {
)
}
}
+
+ if (definition.hooks) {
+ for (const hookName in definition.hooks) {
+ router.post(
+ `/${actionSlug}/hooks/${hookName}`,
+ asyncHandler(async (req: express.Request, res: express.Response) => {
+ try {
+ const data = {
+ settings: req.body.settings || {},
+ payload: req.body.payload || {},
+ page: req.body.page || 1,
+ auth: req.body.auth || {},
+ audienceSettings: req.body.audienceSettings || {},
+ hookInputs: req.body.hookInputs || {},
+ hookOutputs: req.body.hookOutputs || {}
+ }
+
+ const action = destination.actions[actionSlug]
+ const result: ActionHookResponse = await action.executeHook(hookName as ActionHookType, data)
+
+ if (result.error) {
+ throw result.error
+ }
+
+ return res.status(200).json(result)
+ } catch (err) {
+ return res.status(500).json([err])
+ }
+ })
+ )
+
+ const inputFields = definition.hooks?.[hookName as ActionHookType]?.inputFields
+ const dynamicInputs: Record = {}
+ if (inputFields) {
+ for (const fieldKey in inputFields) {
+ const field = inputFields[fieldKey]
+ if (field.dynamic && typeof field.dynamic === 'function') {
+ dynamicInputs[fieldKey] = field.dynamic
+ }
+ }
+ }
+
+ for (const fieldKey in dynamicInputs) {
+ router.post(
+ `/${actionSlug}/hooks/${hookName}/dynamic/${fieldKey}`,
+ asyncHandler(async (req: express.Request, res: express.Response) => {
+ try {
+ const data = {
+ settings: req.body.settings || {},
+ payload: req.body.payload || {},
+ page: req.body.page || 1,
+ auth: req.body.auth || {},
+ audienceSettings: req.body.audienceSettings || {},
+ hookInputs: req.body.hookInputs || {}
+ }
+ const action = destination.actions[actionSlug]
+ const dynamicFn = dynamicInputs[fieldKey] as RequestFn
+ const result = await action.executeDynamicField(fieldKey, data, dynamicFn)
+
+ if (result.error) {
+ throw result.error
+ }
+
+ return res.status(200).json(result)
+ } catch (err) {
+ return res.status(500).json([err])
+ }
+ })
+ )
+ }
+ }
+ }
}
app.use(router)
diff --git a/packages/cli/templates/destinations/browser/README.md b/packages/cli/templates/destinations/browser/README.md
index f7df5cc2b9..08682bb533 100644
--- a/packages/cli/templates/destinations/browser/README.md
+++ b/packages/cli/templates/destinations/browser/README.md
@@ -6,7 +6,7 @@ The {{name}} browser action destination for use with @segment/analytics-next.
MIT License
-Copyright (c) 2023 Segment
+Copyright (c) 2024 Segment
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/packages/core/README.md b/packages/core/README.md
index 1b09dd4b44..a62c077dbe 100644
--- a/packages/core/README.md
+++ b/packages/core/README.md
@@ -1,12 +1,12 @@
# @segment/actions-core
-The core runtime engine for actions, including mapping-kit transforms.
+The core runtime engine for actions, including mapping-kit transforms
## License
MIT License
-Copyright (c) 2023 Segment
+Copyright (c) 2024 Segment
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/packages/core/package.json b/packages/core/package.json
index 9b6eccbbc5..f2feccaca2 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,7 +1,7 @@
{
"name": "@segment/actions-core",
"description": "Core runtime for Destinations Actions.",
- "version": "3.83.0",
+ "version": "3.103.0",
"repository": {
"type": "git",
"url": "https://github.com/segmentio/fab-5-engine",
@@ -80,9 +80,9 @@
},
"dependencies": {
"@lukeed/uuid": "^2.0.0",
- "@segment/action-emitters": "^1.1.2",
- "@segment/ajv-human-errors": "^2.11.3",
- "@segment/destination-subscriptions": "^3.28.3",
+ "@segment/action-emitters": "^1.3.6",
+ "@segment/ajv-human-errors": "^2.12.0",
+ "@segment/destination-subscriptions": "^3.33.0",
"@types/node": "^18.11.15",
"abort-controller": "^3.0.0",
"aggregate-error": "^3.1.0",
@@ -95,6 +95,11 @@
},
"jest": {
"preset": "ts-jest",
+ "globals": {
+ "ts-jest": {
+ "isolatedModules": true
+ }
+ },
"testEnvironment": "node",
"modulePathIgnorePatterns": [
"/dist/"
diff --git a/packages/core/src/__tests__/batching.test.ts b/packages/core/src/__tests__/batching.test.ts
index 247da06bf5..bd44e4f6cf 100644
--- a/packages/core/src/__tests__/batching.test.ts
+++ b/packages/core/src/__tests__/batching.test.ts
@@ -75,7 +75,9 @@ describe('Batching', () => {
test('basic happy path', async () => {
const destination = new Destination(basicBatch)
const res = await destination.onBatch(events, basicBatchSettings)
- expect(res).toEqual(expect.arrayContaining([{ output: 'successfully processed batch of events' }]))
+ expect(res[0]).toMatchObject({
+ output: 'Action Executed'
+ })
})
test('transforms all the payloads based on the subscription mapping', async () => {
@@ -221,7 +223,7 @@ describe('Batching', () => {
await expect(promise).resolves.toMatchInlineSnapshot(`
Array [
Object {
- "output": "successfully processed batch of events",
+ "output": "Action Executed",
},
]
`)
diff --git a/packages/core/src/__tests__/schema-validation.test.ts b/packages/core/src/__tests__/schema-validation.test.ts
index d5a691c4e2..0e6f76841e 100644
--- a/packages/core/src/__tests__/schema-validation.test.ts
+++ b/packages/core/src/__tests__/schema-validation.test.ts
@@ -111,11 +111,12 @@ describe('validateSchema', () => {
expect(payload).toMatchInlineSnapshot(`Object {}`)
})
- it('should not throw when type = hidden', () => {
+ it('should not throw when hidden = true', () => {
const hiddenSchema = fieldsToJsonSchema({
h: {
label: 'h',
- type: 'hidden'
+ type: 'string',
+ unsafe_hidden: true
}
})
diff --git a/packages/core/src/create-test-integration.ts b/packages/core/src/create-test-integration.ts
index 4ec27cc4dd..c47afeea69 100644
--- a/packages/core/src/create-test-integration.ts
+++ b/packages/core/src/create-test-integration.ts
@@ -1,13 +1,13 @@
import { createTestEvent } from './create-test-event'
import { StateContext, Destination, TransactionContext } from './destination-kit'
import { mapValues } from './map-values'
-import type { DestinationDefinition, StatsContext, Logger } from './destination-kit'
+import type { DestinationDefinition, StatsContext, Logger, DataFeedCache, RequestFn } from './destination-kit'
import type { JSONObject } from './json-object'
import type { SegmentEvent } from './segment-event'
import { AuthTokens } from './destination-kit/parse-settings'
import { Features } from './mapping-kit'
import { ExecuteDynamicFieldInput } from './destination-kit/action'
-import { Result } from './destination-kit/types'
+import { DynamicFieldResponse, Result } from './destination-kit/types'
// eslint-disable-next-line @typescript-eslint/no-empty-function
const noop = () => {}
@@ -38,11 +38,12 @@ interface InputData {
auth?: AuthTokens
/**
* The features available in the request based on the customer's sourceID;
- * `features`, `stats`, `logger` , `transactionContext` and `stateContext` are for internal Twilio/Segment use only.
+ * `features`, `stats`, `logger`, `dataFeedCache`, and `transactionContext` and `stateContext` are for internal Twilio/Segment use only.
*/
features?: Features
statsContext?: StatsContext
logger?: Logger
+ dataFeedCache?: DataFeedCache
transactionContext?: TransactionContext
stateContext?: StateContext
}
@@ -55,8 +56,13 @@ class TestDestination extends Destination) {
- return await super.executeDynamicField(action, fieldKey, data)
+ async testDynamicField(
+ action: string,
+ fieldKey: string,
+ data: ExecuteDynamicFieldInput,
+ dynamicFn?: RequestFn
+ ) {
+ return await super.executeDynamicField(action, fieldKey, data, dynamicFn)
}
/** Testing method that runs an action e2e while allowing slightly more flexible inputs */
@@ -71,6 +77,7 @@ class TestDestination extends Destination
@@ -92,6 +99,7 @@ class TestDestination extends Destination extends Destination, 'event'> & { events?: SegmentEvent[] }
@@ -138,6 +147,7 @@ class TestDestination extends Destination = T | Promise
type RequestClient = ReturnType
// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export type RequestFn = (
+export type RequestFn = (
request: RequestClient,
- data: ExecuteInput
+ data: ExecuteInput
) => MaybePromise
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -49,8 +49,23 @@ export interface BaseActionDefinition {
fields: Record
}
+type HookValueTypes = string | boolean | number | Array
+type GenericActionHookValues = Record
+
+type GenericActionHookBundle = {
+ [K in ActionHookType]: {
+ inputs?: GenericActionHookValues
+ outputs?: GenericActionHookValues
+ }
+}
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export interface ActionDefinition extends BaseActionDefinition {
+export interface ActionDefinition<
+ Settings,
+ Payload = any,
+ AudienceSettings = any,
+ GeneratedActionHookBundle extends GenericActionHookBundle = any
+> extends BaseActionDefinition {
/**
* A way to "register" dynamic fields.
* This is likely going to change as we productionalize the data model and definition object
@@ -64,6 +79,68 @@ export interface ActionDefinition
+
+ /** Hooks are triggered at some point in a mappings lifecycle. They may perform a request with the
+ * destination using the provided inputs and return a response. The response may then optionally be stored
+ * in the mapping for later use in the action.
+ */
+ hooks?: {
+ [K in ActionHookType]: ActionHookDefinition<
+ Settings,
+ Payload,
+ AudienceSettings,
+ GeneratedActionHookBundle[K]['outputs'],
+ GeneratedActionHookBundle[K]['inputs']
+ >
+ }
+}
+
+export const hookTypeStrings = ['onMappingSave'] as const
+/**
+ * The supported actions hooks.
+ * on-mapping-save: Called when a mapping is saved by the user. The return from this method is then stored in the mapping.
+ */
+export type ActionHookType = typeof hookTypeStrings[number]
+export interface ActionHookResponse {
+ /** A user-friendly message to be shown when the hook is successfully executed. */
+ successMessage?: string
+ /** After successfully executing a hook, savedData will be persisted for later use in the action. */
+ savedData?: GeneratedActionHookOutputs
+ error?: {
+ /** A user-friendly message to be shown when the hook errors. */
+ message: string
+ code: string
+ }
+}
+
+export interface ActionHookDefinition<
+ Settings,
+ Payload,
+ AudienceSettings,
+ GeneratedActionHookOutputs,
+ GeneratedActionHookTypesInputs
+> {
+ /** The display title for this hook. */
+ label: string
+ /** A description of what this hook does. */
+ description: string
+ /** The configuration fields that are used when executing the hook. The values will be provided by users in the app. */
+ inputFields?: Record<
+ string,
+ Omit & {
+ dynamic?: RequestFn
+ }
+ >
+ /** The shape of the return from performHook. These values will be available in the generated-types: Payload for use in perform() */
+ outputTypes?: Record
+ /** The operation to perform when this hook is triggered. */
+ performHook: RequestFn<
+ Settings,
+ Payload,
+ ActionHookResponse,
+ AudienceSettings,
+ GeneratedActionHookTypesInputs
+ >
}
export interface ExecuteDynamicFieldInput {
@@ -72,18 +149,24 @@ export interface ExecuteDynamicFieldInput {
+interface ExecuteBundle {
data: Data
settings: T
audienceSettings?: AudienceSettings
mapping: JSONObject
auth: AuthTokens | undefined
+ hookOutputs?: Record
/** For internal Segment/Twilio use only. */
features?: Features | undefined
statsContext?: StatsContext | undefined
logger?: Logger | undefined
+ dataFeedCache?: DataFeedCache | undefined
transactionContext?: TransactionContext
stateContext?: StateContext
}
@@ -96,7 +179,9 @@ export class Action
readonly destinationName: string
readonly schema?: JSONSchema4
+ readonly hookSchemas?: Record
readonly hasBatchSupport: boolean
+ readonly hasHookSupport: boolean
// Payloads may be any type so we use `any` explicitly here.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private extendRequest: RequestExtension | undefined
@@ -113,11 +198,41 @@ export class Action = {}
+ for (const key in hook.inputFields) {
+ const field = hook.inputFields[key]
+
+ if (field.dynamic) {
+ castedInputFields[key] = {
+ ...field,
+ dynamic: true
+ }
+ } else {
+ castedInputFields[key] = {
+ ...field,
+ dynamic: false
+ }
+ }
+ }
+
+ this.hookSchemas[hookName] = fieldsToJsonSchema(castedInputFields)
+ }
+ }
+ }
}
async execute(bundle: ExecuteBundle): Promise {
@@ -138,6 +253,17 @@ export class Action): Promise {
+ async executeBatch(bundle: ExecuteBundle): Promise {
+ const results: Result[] = [{ output: 'Action Executed' }]
+
if (!this.hasBatchSupport) {
throw new IntegrationError('This action does not support batched requests.', 'NotImplemented', 501)
}
@@ -184,7 +314,7 @@ export class Action
+ data: ExecuteDynamicFieldInput,
+ /**
+ * The dynamicFn argument is optional since it is only used by dynamic hook input fields. (For now)
+ */
+ dynamicFn?: RequestFn
): Promise {
- const fn = this.definition.dynamicFields?.[field]
+ let fn
+ if (dynamicFn && typeof dynamicFn === 'function') {
+ fn = dynamicFn
+ } else {
+ fn = this.definition.dynamicFields?.[field]
+ }
+
if (typeof fn !== 'function') {
return Promise.resolve({
choices: [],
@@ -225,6 +371,27 @@ export class Action
+ ): Promise> {
+ if (!this.hasHookSupport) {
+ throw new IntegrationError('This action does not support any hooks.', 'NotImplemented', 501)
+ }
+ const hookFn = this.definition.hooks?.[hookType]?.performHook
+
+ if (!hookFn) {
+ throw new IntegrationError(`Missing implementation for hook: ${hookType}.`, 'NotImplemented', 501)
+ }
+
+ if (this.hookSchemas?.[hookType]) {
+ const schema = this.hookSchemas[hookType]
+ validateSchema(data.hookInputs, schema)
+ }
+
+ return (await this.performRequest(hookFn, data)) as ActionHookResponse
+ }
+
/**
* Perform a request using the definition's request client
* the given request function
diff --git a/packages/core/src/destination-kit/fields-to-jsonschema.ts b/packages/core/src/destination-kit/fields-to-jsonschema.ts
index 80a537f10a..38948485e3 100644
--- a/packages/core/src/destination-kit/fields-to-jsonschema.ts
+++ b/packages/core/src/destination-kit/fields-to-jsonschema.ts
@@ -6,7 +6,6 @@ function toJsonSchemaType(type: FieldTypeName): JSONSchema4TypeName | JSONSchema
case 'string':
case 'text':
case 'password':
- case 'hidden':
return 'string'
case 'datetime':
return ['string', 'number']
diff --git a/packages/core/src/destination-kit/index.ts b/packages/core/src/destination-kit/index.ts
index d5b13ac879..f794968524 100644
--- a/packages/core/src/destination-kit/index.ts
+++ b/packages/core/src/destination-kit/index.ts
@@ -1,7 +1,17 @@
import { validate, parseFql, ErrorCondition } from '@segment/destination-subscriptions'
import { EventEmitterSlug } from '@segment/action-emitters'
import type { JSONSchema4 } from 'json-schema'
-import { Action, ActionDefinition, BaseActionDefinition, RequestFn, ExecuteDynamicFieldInput } from './action'
+import {
+ Action,
+ ActionDefinition,
+ ActionHookDefinition,
+ ActionHookType,
+ hookTypeStrings,
+ ActionHookResponse,
+ BaseActionDefinition,
+ RequestFn,
+ ExecuteDynamicFieldInput
+} from './action'
import { time, duration } from '../time'
import { JSONLikeObject, JSONObject, JSONValue } from '../json-object'
import { SegmentEvent } from '../segment-event'
@@ -9,7 +19,15 @@ import { fieldsToJsonSchema, MinimalInputField } from './fields-to-jsonschema'
import createRequestClient, { RequestClient, ResponseError } from '../create-request-client'
import { validateSchema } from '../schema-validation'
import type { ModifiedResponse } from '../types'
-import type { GlobalSetting, RequestExtension, ExecuteInput, Result, Deletion, DeletionPayload } from './types'
+import type {
+ GlobalSetting,
+ RequestExtension,
+ ExecuteInput,
+ Result,
+ Deletion,
+ DeletionPayload,
+ DynamicFieldResponse
+} from './types'
import type { AllRequestOptions } from '../request-client'
import { ErrorCodes, IntegrationError, InvalidAuthenticationError } from '../errors'
import { AuthTokens, getAuthData, getOAuth2Data, updateOAuthSettings } from './parse-settings'
@@ -17,7 +35,16 @@ import { InputData, Features } from '../mapping-kit'
import { retry } from '../retry'
import { HTTPError } from '..'
-export type { BaseActionDefinition, ActionDefinition, ExecuteInput, RequestFn }
+export type {
+ BaseActionDefinition,
+ ActionDefinition,
+ ActionHookDefinition,
+ ActionHookResponse,
+ ActionHookType,
+ ExecuteInput,
+ RequestFn
+}
+export { hookTypeStrings }
export type { MinimalInputField }
export { fieldsToJsonSchema }
@@ -252,6 +279,7 @@ interface EventInput {
readonly features?: Features
readonly statsContext?: StatsContext
readonly logger?: Logger
+ readonly dataFeedCache?: DataFeedCache
readonly transactionContext?: TransactionContext
readonly stateContext?: StateContext
}
@@ -266,6 +294,7 @@ interface BatchEventInput {
readonly features?: Features
readonly statsContext?: StatsContext
readonly logger?: Logger
+ readonly dataFeedCache?: DataFeedCache
readonly transactionContext?: TransactionContext
readonly stateContext?: StateContext
}
@@ -281,6 +310,7 @@ interface OnEventOptions {
features?: Features
statsContext?: StatsContext
logger?: Logger
+ readonly dataFeedCache?: DataFeedCache
transactionContext?: TransactionContext
stateContext?: StateContext
/** Handler to perform synchronization. If set, the refresh access token method will be synchronized across
@@ -334,6 +364,13 @@ export interface Logger {
withTags(extraTags: any): void
}
+export interface DataFeedCache {
+ setRequestResponse(requestId: string, response: string, expiryInSeconds: number): Promise
+ getRequestResponse(requestId: string): Promise
+ maxResponseSizeBytes: number
+ maxExpirySeconds: number
+}
+
export class Destination {
readonly definition: DestinationDefinition
readonly name: string
@@ -510,6 +547,7 @@ export class Destination {
features,
statsContext,
logger,
+ dataFeedCache,
transactionContext,
stateContext
}: EventInput
@@ -533,6 +571,7 @@ export class Destination {
features,
statsContext,
logger,
+ dataFeedCache,
transactionContext,
stateContext
})
@@ -548,6 +587,7 @@ export class Destination {
features,
statsContext,
logger,
+ dataFeedCache,
transactionContext,
stateContext
}: BatchEventInput
@@ -563,7 +603,7 @@ export class Destination {
audienceSettings = events[0].context?.personas?.audience_settings as AudienceSettings
}
- await action.executeBatch({
+ return action.executeBatch({
mapping,
data: events as unknown as InputData[],
settings,
@@ -572,24 +612,27 @@ export class Destination {
features,
statsContext,
logger,
+ dataFeedCache,
transactionContext,
stateContext
})
-
- return [{ output: 'successfully processed batch of events' }]
}
public async executeDynamicField(
actionSlug: string,
fieldKey: string,
- data: ExecuteDynamicFieldInput
+ data: ExecuteDynamicFieldInput,
+ /**
+ * The dynamicFn argument is optional since it is only used by dynamic hook input fields. (For now)
+ */
+ dynamicFn?: RequestFn
) {
const action = this.actions[actionSlug]
if (!action) {
return []
}
- return action.executeDynamicField(fieldKey, data)
+ return action.executeDynamicField(fieldKey, data, dynamicFn)
}
private async onSubscription(
@@ -608,6 +651,7 @@ export class Destination {
features: options?.features || {},
statsContext: options?.statsContext || ({} as StatsContext),
logger: options?.logger,
+ dataFeedCache: options?.dataFeedCache,
transactionContext: options?.transactionContext,
stateContext: options?.stateContext
}
diff --git a/packages/core/src/destination-kit/types.ts b/packages/core/src/destination-kit/types.ts
index 5c30d8ee03..32215b5ab7 100644
--- a/packages/core/src/destination-kit/types.ts
+++ b/packages/core/src/destination-kit/types.ts
@@ -1,4 +1,4 @@
-import { StateContext, Logger, StatsContext, TransactionContext } from './index'
+import { StateContext, Logger, StatsContext, TransactionContext, DataFeedCache, ActionHookType } from './index'
import type { RequestOptions } from '../request-client'
import type { JSONObject } from '../json-object'
import { AuthTokens } from './parse-settings'
@@ -16,7 +16,13 @@ export interface Result {
data?: JSONObject | null
}
-export interface ExecuteInput {
+export interface ExecuteInput<
+ Settings,
+ Payload,
+ AudienceSettings = unknown,
+ ActionHookInputs = any,
+ ActionHookOutputs = any
+> {
/** The subscription mapping definition */
readonly mapping?: JSONObject
/** The global destination settings */
@@ -25,6 +31,10 @@ export interface ExecuteInput {
readonly audienceSettings?: AudienceSettings
/** The transformed input data, based on `mapping` + `event` (or `events` if batched) */
payload: Payload
+ /** Inputs into an actions hook performHook method */
+ hookInputs?: ActionHookInputs
+ /** Stored outputs from an invokation of an actions hook */
+ hookOutputs?: Partial>
/** The page used in dynamic field requests */
page?: string
/** The data needed in OAuth requests */
@@ -36,6 +46,7 @@ export interface ExecuteInput {
readonly features?: Features
readonly statsContext?: StatsContext
readonly logger?: Logger
+ readonly dataFeedCache?: DataFeedCache
readonly transactionContext?: TransactionContext
readonly stateContext?: StateContext
}
@@ -83,19 +94,10 @@ export interface GlobalSetting {
}
/** The supported field type names */
-export type FieldTypeName =
- | 'string'
- | 'text'
- | 'number'
- | 'integer'
- | 'datetime'
- | 'boolean'
- | 'password'
- | 'object'
- | 'hidden'
-
-/** The shape of an input field definition */
-export interface InputField {
+export type FieldTypeName = 'string' | 'text' | 'number' | 'integer' | 'datetime' | 'boolean' | 'password' | 'object'
+
+/** Properties of an InputField which are involved in creating the generated-types.ts file */
+export interface InputFieldJSONSchema {
/** A short, human-friendly label for the field */
label: string
/** A human-friendly description of the field */
@@ -110,10 +112,6 @@ export interface InputField {
additionalProperties?: boolean
/** An optional default value for the field */
default?: FieldValue
- /** A placeholder display value that suggests what to input */
- placeholder?: string
- /** Whether or not the field supports dynamically fetching options */
- dynamic?: boolean
/**
* A predefined set of options for the setting.
* Only relevant for `type: 'string'` or `type: 'number'`.
@@ -154,7 +152,13 @@ export interface InputField {
| 'uuid' // Universally Unique IDentifier according to RFC4122.
| 'password' // hint to the UI to hide/obfuscate input strings
| 'text' // longer strings
+}
+export interface InputField extends InputFieldJSONSchema {
+ /** A placeholder display value that suggests what to input */
+ placeholder?: string
+ /** Whether or not the field supports dynamically fetching options */
+ dynamic?: boolean
/**
* Determines the UI representation of the object field. Only applies to object types.
* Key Value Editor: Users can specify individual object keys and their mappings, ideal for custom objects.
@@ -178,6 +182,34 @@ export interface InputField {
* locked out from editing an empty field.
*/
readOnly?: boolean
+
+ /**
+ * Determines whether this field will be shown in the UI. This is useful for when some field becomes irrelevant based on
+ * the value of another field.
+ */
+ depends_on?: DependsOnConditions
+}
+
+/**
+ * A single condition defining whether a field should be shown.
+ * fieldKey: The field key in the fields object to look at
+ * operator: The operator to use when comparing the field value
+ * value: The value we expect that field to have, if undefined, we will match based on whether the field contains a value or not
+ */
+export interface Condition {
+ fieldKey: string
+ operator: 'is' | 'is_not'
+ value: Omit | Array> | undefined
+}
+
+/**
+ * If match is not set, it will default to 'all'
+ * If match = 'any', then meeting any of the conditions defined will result in the field being shown.
+ * If match = 'all', then meeting all of the conditions defined will result in the field being shown.
+ */
+export interface DependsOnConditions {
+ match?: 'any' | 'all'
+ conditions: Condition[]
}
export type FieldValue = string | number | boolean | object | Directive
diff --git a/packages/core/src/errors.ts b/packages/core/src/errors.ts
index 07638a9c16..b9ea1b8c93 100644
--- a/packages/core/src/errors.ts
+++ b/packages/core/src/errors.ts
@@ -13,7 +13,7 @@ export class IntegrationError extends CustomError {
/**
* @param message - a human-friendly message to display to users
* @param code - error code/reason
- * @param status - http status code (e.g. 400).
+ * @param status - http status code (e.g. 400)
* - 4xx errors are not automatically retried, except for 408, 423, 429
* - 5xx are automatically retried, except for 501
*/
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index dc08ad1ea0..2e23dfe8be 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -1,6 +1,31 @@
export { Destination, fieldsToJsonSchema } from './destination-kit'
export { getAuthData } from './destination-kit/parse-settings'
export { transform } from './mapping-kit'
+export {
+ ArrayPathDirective,
+ CaseDirective,
+ Directive,
+ DirectiveMetadata,
+ FieldValue,
+ IfDirective,
+ LiteralDirective,
+ PathDirective,
+ PrimitiveValue,
+ ReplaceDirective,
+ TemplateDirective,
+ JSONDirective,
+ getFieldValue,
+ getFieldValueKeys,
+ isArrayPathDirective,
+ isCaseDirective,
+ isDirective,
+ isIfDirective,
+ isLiteralDirective,
+ isPathDirective,
+ isReplaceDirective,
+ isTemplateDirective,
+ isJSONDirective
+} from './mapping-kit/value-keys'
export { createTestEvent } from './create-test-event'
export { createTestIntegration } from './create-test-integration'
export { default as createInstance } from './request-client'
@@ -29,6 +54,7 @@ export { default as fetch, Request, Response, Headers } from './fetch'
export type {
BaseActionDefinition,
ActionDefinition,
+ ActionHookResponse,
BaseDefinition,
DestinationDefinition,
AudienceDestinationDefinition,
diff --git a/packages/core/src/mapping-kit/__tests__/flatten.test.ts b/packages/core/src/mapping-kit/__tests__/flatten.test.ts
new file mode 100644
index 0000000000..b8307d7c64
--- /dev/null
+++ b/packages/core/src/mapping-kit/__tests__/flatten.test.ts
@@ -0,0 +1,134 @@
+import { flattenObject } from '../flatten'
+
+describe('flatten', () => {
+ it('flattens an object', () => {
+ const obj = {
+ a: {
+ b: {
+ c: 1
+ },
+ d: 2
+ }
+ }
+ expect(flattenObject(obj)).toEqual({
+ 'a.b.c': 1,
+ 'a.d': 2
+ })
+ })
+ it('flattens an object with a custom separator', () => {
+ const obj = {
+ a: {
+ b: {
+ c: 1
+ },
+ d: 2
+ }
+ }
+ expect(flattenObject(obj, '', '/')).toEqual({
+ 'a/b/c': 1,
+ 'a/d': 2
+ })
+ })
+ it('flattens an array', () => {
+ const obj = {
+ a: [
+ {
+ b: 1
+ },
+ {
+ c: 2
+ }
+ ]
+ }
+ expect(flattenObject(obj)).toEqual({
+ 'a.0.b': 1,
+ 'a.1.c': 2
+ })
+ })
+ it('flattens a deep nested structure', () => {
+ const obj = {
+ a: {
+ b: {
+ c: {
+ d: {
+ e: {
+ f: {
+ g: {
+ h: {
+ i: 1
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ expect(flattenObject(obj)).toEqual({
+ 'a.b.c.d.e.f.g.h.i': 1
+ })
+ })
+ it('flattens a deep nested structure with deep nested arrays', () => {
+ const obj = {
+ a: {
+ b: {
+ c: [
+ {
+ d: [
+ {
+ e: [
+ {
+ f: [
+ {
+ g: [
+ {
+ h: [
+ {
+ i: 1
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ expect(flattenObject(obj)).toEqual({
+ 'a.b.c.0.d.0.e.0.f.0.g.0.h.0.i': 1
+ })
+ })
+ it('flattens a structure starting with arrays of objects', () => {
+ const obj = [
+ {
+ a: [
+ {
+ b: [
+ {
+ c: 1
+ }
+ ]
+ },
+ {
+ d: [
+ {
+ e: 2
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ expect(flattenObject(obj)).toEqual({
+ '0.a.0.b.0.c': 1,
+ '0.a.1.d.0.e': 2
+ })
+ })
+})
diff --git a/packages/core/src/mapping-kit/__tests__/index.iso.test.ts b/packages/core/src/mapping-kit/__tests__/index.iso.test.ts
index 67f9d24866..44e42dc031 100644
--- a/packages/core/src/mapping-kit/__tests__/index.iso.test.ts
+++ b/packages/core/src/mapping-kit/__tests__/index.iso.test.ts
@@ -473,6 +473,81 @@ describe('@arrayPath', () => {
})
})
+describe('@json', () => {
+ test('encode', () => {
+ const output = transform({ neat: { '@json': { mode: 'encode', value: { '@path': '$.foo' } } } }, { foo: 'bar' })
+ expect(output).toStrictEqual({ neat: '"bar"' })
+ })
+
+ test('encode_object', () => {
+ const output = transform(
+ { neat: { '@json': { mode: 'encode', value: { '@path': '$.foo' } } } },
+ { foo: { bar: 'baz' } }
+ )
+ expect(output).toStrictEqual({ neat: '{"bar":"baz"}' })
+ })
+
+ test('encode_array', () => {
+ const output = transform(
+ { neat: { '@json': { mode: 'encode', value: { '@path': '$.foo' } } } },
+ { foo: ['bar', 'baz'] }
+ )
+ expect(output).toStrictEqual({ neat: '["bar","baz"]' })
+ })
+
+ test('decode', () => {
+ const output = transform({ neat: { '@json': { mode: 'decode', value: { '@path': '$.foo' } } } }, { foo: '"bar"' })
+ expect(output).toStrictEqual({ neat: 'bar' })
+ })
+
+ test('decode_object', () => {
+ const output = transform(
+ { neat: { '@json': { mode: 'decode', value: { '@path': '$.foo' } } } },
+ { foo: '{"bar":"baz"}' }
+ )
+ expect(output).toStrictEqual({ neat: { bar: 'baz' } })
+ })
+
+ test('decode_array', () => {
+ const output = transform(
+ { neat: { '@json': { mode: 'decode', value: { '@path': '$.foo' } } } },
+ { foo: '["bar","baz"]' }
+ )
+ expect(output).toStrictEqual({ neat: ['bar', 'baz'] })
+ })
+
+ test('invalid mode', () => {
+ expect(() => {
+ transform({ neat: { '@json': { mode: 'oops', value: { '@path': '$.foo' } } } }, { foo: 'bar' })
+ }).toThrowError()
+ })
+
+ test('invalid value', () => {
+ const output = transform(
+ { neat: { '@json': { mode: 'encode', value: { '@path': '$.bad' } } } },
+ { foo: { bar: 'baz' } }
+ )
+ expect(output).toStrictEqual({})
+ })
+})
+
+describe('@flatten', () => {
+ test('simple', () => {
+ const output = transform(
+ { neat: { '@flatten': { value: { '@path': '$.foo' }, separator: '.' } } },
+ { foo: { bar: 'baz', aces: { a: 1, b: 2 } } }
+ )
+ expect(output).toStrictEqual({ neat: { bar: 'baz', 'aces.a': 1, 'aces.b': 2 } })
+ })
+ test('array value first', () => {
+ const output = transform(
+ { result: { '@flatten': { value: { '@path': '$.foo' }, separator: '.' } } },
+ { foo: [{ fazz: 'bar', fizz: 'baz' }] }
+ )
+ expect(output).toStrictEqual({ result: { '0.fazz': 'bar', '0.fizz': 'baz' } })
+ })
+})
+
describe('@path', () => {
test('simple', () => {
const output = transform({ neat: { '@path': '$.foo' } }, { foo: 'bar' })
@@ -713,6 +788,74 @@ describe('@replace', () => {
)
expect(output).toStrictEqual('different+things')
})
+ test('replace boolean', () => {
+ const payload = {
+ a: true
+ }
+ const output = transform(
+ {
+ '@replace': {
+ pattern: 'true',
+ replacement: 'granted',
+ value: { '@path': '$.a' }
+ }
+ },
+ payload
+ )
+ expect(output).toStrictEqual('granted')
+ })
+ test('replace number', () => {
+ const payload = {
+ a: 1
+ }
+ const output = transform(
+ {
+ '@replace': {
+ pattern: '1',
+ replacement: 'granted',
+ value: { '@path': '$.a' }
+ }
+ },
+ payload
+ )
+ expect(output).toStrictEqual('granted')
+ })
+ test('replace 2 values', () => {
+ const payload = {
+ a: 'something-great!'
+ }
+ const output = transform(
+ {
+ '@replace': {
+ pattern: '-',
+ replacement: ' ',
+ pattern2: 'great',
+ replacement2: 'awesome',
+ value: { '@path': '$.a' }
+ }
+ },
+ payload
+ )
+ expect(output).toStrictEqual('something awesome!')
+ })
+ test('replace with 2 values but only second one exists', () => {
+ const payload = {
+ a: false
+ }
+ const output = transform(
+ {
+ '@replace': {
+ pattern: 'true',
+ replacement: 'granted',
+ pattern2: 'false',
+ replacement2: 'denied',
+ value: { '@path': '$.a' }
+ }
+ },
+ payload
+ )
+ expect(output).toStrictEqual('denied')
+ })
})
describe('remove undefined values in objects', () => {
diff --git a/packages/core/src/mapping-kit/__tests__/value-keys.test.ts b/packages/core/src/mapping-kit/__tests__/value-keys.test.ts
new file mode 100644
index 0000000000..937ff09964
--- /dev/null
+++ b/packages/core/src/mapping-kit/__tests__/value-keys.test.ts
@@ -0,0 +1,102 @@
+import { getFieldValueKeys } from '../value-keys'
+
+describe('getFieldValueKeys', () => {
+ it('should return empty [] for strings', () => {
+ const value = 'https://webhook.site/very-legit'
+
+ const keys = getFieldValueKeys(value)
+
+ expect(keys).toEqual([])
+ })
+
+ it('should return empty [] for booleans', () => {
+ const value = false
+
+ const keys = getFieldValueKeys(value)
+
+ expect(keys).toEqual([])
+ })
+
+ it('should return empty [] for empty objects', () => {
+ const value = {}
+
+ const keys = getFieldValueKeys(value)
+
+ expect(keys).toEqual([])
+ })
+
+ it('should return correct keys for single @path', () => {
+ const value = {
+ '@path': '$.properties.string'
+ }
+
+ const keys = getFieldValueKeys(value)
+
+ expect(keys).toEqual(['$.properties.string'])
+ })
+
+ it('should return correct keys for single @templates', () => {
+ const value = {
+ value: {
+ '@template': '{{__segment_entities.log-test-1.ENTITIES_TEST.PRODUCTS.NAME}}'
+ }
+ }
+
+ const keys = getFieldValueKeys(value)
+
+ expect(keys).toEqual(['__segment_entities.log-test-1.ENTITIES_TEST.PRODUCTS.NAME'])
+ })
+
+ it('should return correct keys for multiple @templates', () => {
+ const value = {
+ value: {
+ '@template': '{{__segment_entities.log-test-1.ENTITIES_TEST.PRODUCTS.NAME}}-{{test}}'
+ }
+ }
+
+ const keys = getFieldValueKeys(value)
+
+ expect(keys).toEqual(['__segment_entities.log-test-1.ENTITIES_TEST.PRODUCTS.NAME', 'test'])
+ })
+
+ it('should return correct keys for objects', () => {
+ const value = {
+ Category: {
+ '@template': '{{__segment_entities.log-test-1.ENTITIES_TEST.PRODUCTS.CATEGORY}}'
+ },
+ Enriched_ID: 'test',
+ Name: {
+ '@path': '$.properties.string'
+ }
+ }
+
+ const keys = getFieldValueKeys(value)
+
+ expect(keys).toEqual(['__segment_entities.log-test-1.ENTITIES_TEST.PRODUCTS.CATEGORY', '$.properties.string'])
+ })
+
+ it('should return correct keys for @arrayPath (not yet supported for enrichments)', () => {
+ const value = {
+ '@arrayPath': [{ '@template': '{{properties.products}}' }, { productId: { '@template': '{{productId}}' } }]
+ }
+
+ const keys = getFieldValueKeys(value)
+
+ expect(keys).toEqual(['properties.products', 'productId'])
+ })
+
+ it('should return correct keys for @json', () => {
+ const value = {
+ '@json': {
+ mode: 'encode',
+ value: {
+ '@template': '{{properties.products}}'
+ }
+ }
+ }
+
+ const keys = getFieldValueKeys(value)
+
+ expect(keys).toEqual(['properties.products'])
+ })
+})
diff --git a/packages/core/src/mapping-kit/flatten.ts b/packages/core/src/mapping-kit/flatten.ts
new file mode 100644
index 0000000000..fb8a06ed6c
--- /dev/null
+++ b/packages/core/src/mapping-kit/flatten.ts
@@ -0,0 +1,16 @@
+import { JSONLike, JSONLikeObject } from '../json-object'
+import { isArray, isObject } from '../real-type-of'
+
+export const flattenObject = (input: JSONLike, prefix = '', separator = '.'): JSONLikeObject => {
+ //input may be a primitive value, object, or array
+ if (isObject(input) || isArray(input)) {
+ return Object.entries(input).reduce((acc, [key, value]) => {
+ const newKey = prefix ? `${prefix}${separator}${key}` : key
+ return {
+ ...acc,
+ ...flattenObject(value, newKey, separator)
+ }
+ }, {})
+ }
+ return { [prefix]: input }
+}
diff --git a/packages/core/src/mapping-kit/index.ts b/packages/core/src/mapping-kit/index.ts
index ff9f52460e..b4209f0fc0 100644
--- a/packages/core/src/mapping-kit/index.ts
+++ b/packages/core/src/mapping-kit/index.ts
@@ -6,6 +6,7 @@ import { realTypeOf, isObject, isArray } from '../real-type-of'
import { removeUndefined } from '../remove-undefined'
import validate from './validate'
import { arrify } from '../arrify'
+import { flattenObject } from './flatten'
export type InputData = { [key: string]: unknown }
export type Features = { [key: string]: boolean }
@@ -102,6 +103,23 @@ registerDirective('@case', (opts, payload) => {
export const MAX_PATTERN_LENGTH = 10
export const MAX_REPLACEMENT_LENGTH = 10
+
+function performReplace(value: string, pattern: string, replacement: string, flags: string) {
+ if (pattern.length > MAX_PATTERN_LENGTH) {
+ throw new Error(`@replace requires a "pattern" less than ${MAX_PATTERN_LENGTH} characters`)
+ }
+
+ if (replacement.length > MAX_REPLACEMENT_LENGTH) {
+ throw new Error(`@replace requires a "replacement" less than ${MAX_REPLACEMENT_LENGTH} characters`)
+ }
+
+ // We don't want users providing regular expressions for the pattern (for now)
+ // https://stackoverflow.com/questions/F3115150/how-to-escape-regular-expression-special-characters-using-javascript
+ pattern = pattern.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')
+
+ return value.replace(new RegExp(pattern, flags), replacement)
+}
+
registerDirective('@replace', (opts, payload) => {
if (!isObject(opts)) {
throw new Error('@replace requires an object with a "pattern" key')
@@ -117,6 +135,12 @@ registerDirective('@replace', (opts, payload) => {
opts.replacement = ''
}
+ // Assume null/missing replacement means empty for pattern2 if exists
+ if (opts.pattern2 && opts.replacement2 == null) {
+ // Empty replacement string is ok
+ opts.replacement2 = ''
+ }
+
// case sensitive by default if this key is missing
if (opts.ignorecase == null) {
opts.ignorecase = false
@@ -127,12 +151,19 @@ registerDirective('@replace', (opts, payload) => {
opts.global = true
}
- let pattern = opts.pattern
+ const pattern = opts.pattern
const replacement = opts.replacement
const ignorecase = opts.ignorecase
const isGlobal = opts.global
if (opts.value) {
- const value = resolve(opts.value, payload)
+ let value = resolve(opts.value, payload)
+ let new_value = ''
+
+ // We want to be able to replace values that are boolean or numbers
+ if (typeof value === 'boolean' || typeof value === 'number') {
+ value = String(value)
+ }
+
if (
typeof value === 'string' &&
typeof pattern === 'string' &&
@@ -140,17 +171,6 @@ registerDirective('@replace', (opts, payload) => {
typeof ignorecase === 'boolean' &&
typeof isGlobal === 'boolean'
) {
- if (pattern.length > MAX_PATTERN_LENGTH) {
- throw new Error(`@replace requires a "pattern" less than ${MAX_PATTERN_LENGTH} characters`)
- }
-
- if (replacement.length > MAX_REPLACEMENT_LENGTH) {
- throw new Error(`@replace requires a "replacement" less than ${MAX_REPLACEMENT_LENGTH} characters`)
- }
-
- // We don't want users providing regular expressions for the pattern (for now)
- // https://stackoverflow.com/questions/F3115150/how-to-escape-regular-expression-special-characters-using-javascript
- pattern = pattern.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')
let flags = ''
if (isGlobal) {
flags += 'g'
@@ -158,8 +178,16 @@ registerDirective('@replace', (opts, payload) => {
if (ignorecase) {
flags += 'i'
}
- return value.replace(new RegExp(pattern, flags), replacement)
+
+ new_value = performReplace(value, pattern, replacement, flags)
+
+ // If pattern2 exists, replace the new_value with replacement2
+ if (opts.pattern2 && typeof opts.pattern2 === 'string' && typeof opts.replacement2 === 'string') {
+ new_value = performReplace(new_value, opts.pattern2, opts.replacement2, flags)
+ }
}
+
+ return new_value
}
})
@@ -196,6 +224,49 @@ registerDirective('@literal', (value, payload) => {
return resolve(value, payload)
})
+registerDirective('@flatten', (opts, payload) => {
+ if (!isObject(opts)) {
+ throw new Error('@flatten requires an object with a "separator" key')
+ }
+
+ if (!opts.separator) {
+ throw new Error('@flatten requires a "separator" key')
+ }
+
+ const separator = resolve(opts.separator, payload)
+ if (typeof separator !== 'string') {
+ throw new Error('@flatten requires a string separator')
+ }
+
+ const value = resolve(opts.value, payload)
+
+ return flattenObject(value, '', separator)
+})
+
+registerDirective('@json', (opts, payload) => {
+ if (!isObject(opts)) {
+ throw new Error('@json requires an object with a "value" key')
+ }
+
+ if (!opts.mode) {
+ throw new Error('@json requires a "mode" key')
+ }
+
+ if (!opts.value) {
+ throw new Error('@json requires a "value" key')
+ }
+
+ const value = resolve(opts.value, payload)
+ if (opts.mode === 'encode') {
+ return JSON.stringify(value)
+ } else if (opts.mode === 'decode') {
+ if (typeof value === 'string') {
+ return JSON.parse(value)
+ }
+ return value
+ }
+})
+
/**
* Resolves a mapping value/object by applying the input payload based on directives
* @param mapping - the mapping directives or raw values to resolve
@@ -230,7 +301,7 @@ function resolve(mapping: JSONLike, payload: JSONObject): JSONLike {
* @param mapping - the directives and raw values
* @param data - the input data to apply to directives
*/
-export function transform(mapping: JSONLikeObject, data: InputData | undefined = {}): JSONObject {
+function transform(mapping: JSONLikeObject, data: InputData | undefined = {}): JSONObject {
const realType = realTypeOf(data)
if (realType !== 'object') {
throw new Error(`data must be an object, got ${realType}`)
@@ -251,7 +322,7 @@ export function transform(mapping: JSONLikeObject, data: InputData | undefined =
* @param mapping - the directives and raw values
* @param data - the array input data to apply to directives
*/
-export function transformBatch(mapping: JSONLikeObject, data: Array | undefined = []): JSONObject[] {
+function transformBatch(mapping: JSONLikeObject, data: Array | undefined = []): JSONObject[] {
const realType = realTypeOf(data)
if (!isArray(data)) {
throw new Error(`data must be an array, got ${realType}`)
@@ -265,3 +336,5 @@ export function transformBatch(mapping: JSONLikeObject, data: Array |
// Cast because we know there are no `undefined` values after `removeUndefined`
return removeUndefined(resolved) as JSONObject[]
}
+
+export { transform, transformBatch }
diff --git a/packages/core/src/mapping-kit/validate.ts b/packages/core/src/mapping-kit/validate.ts
index 26f96aa70a..71ddcc4af5 100644
--- a/packages/core/src/mapping-kit/validate.ts
+++ b/packages/core/src/mapping-kit/validate.ts
@@ -136,6 +136,16 @@ function validateString(v: unknown, stack: string[] = []) {
return
}
+function validateAllowedStrings(...allowed: string[]) {
+ return (v: unknown, stack: string[] = []) => {
+ validateString(v, stack)
+ const str = v as string
+ if (!allowed.includes(str.toLowerCase())) {
+ throw new ValidationError(`should be one of ${allowed.join(', ')} but it is ${JSON.stringify(str)}`, stack)
+ }
+ }
+}
+
function validateBoolean(v: unknown, stack: string[] = []) {
const type = realTypeOrDirective(v)
if (type !== 'boolean') {
@@ -166,7 +176,7 @@ function validateObject(value: unknown, stack: string[] = []) {
try {
validate(obj[k], [...stack, k])
} catch (e) {
- errors.push(e)
+ errors.push(e as Error)
}
})
@@ -201,7 +211,7 @@ function validateObjectWithFields(input: unknown, fields: ValidateFields, stack:
}
}
} catch (error) {
- errors.push(error)
+ errors.push(error as Error)
}
})
@@ -227,11 +237,12 @@ function directive(names: string[] | string, fn: DirectiveValidator): void {
try {
fn(v, [...stack, name])
} catch (e) {
+ const err: Error = e as Error
if (e instanceof ValidationError || e instanceof AggregateError) {
throw e
}
- throw new ValidationError(e.message, stack)
+ throw new ValidationError(err.message, stack)
}
}
})
@@ -285,6 +296,28 @@ directive('@path', (v, stack) => {
validateDirectiveOrString(v, stack)
})
+directive('@json', (v, stack) => {
+ validateObjectWithFields(
+ v,
+ {
+ value: { required: validateDirectiveOrRaw },
+ mode: { required: validateAllowedStrings('encode', 'decode') }
+ },
+ stack
+ )
+})
+
+directive('@flatten', (v, stack) => {
+ validateObjectWithFields(
+ v,
+ {
+ separator: { optional: validateString },
+ value: { required: validateDirectiveOrRaw }
+ },
+ stack
+ )
+})
+
directive('@template', (v, stack) => {
validateDirectiveOrString(v, stack)
})
diff --git a/packages/core/src/mapping-kit/value-keys.ts b/packages/core/src/mapping-kit/value-keys.ts
new file mode 100644
index 0000000000..35d6895393
--- /dev/null
+++ b/packages/core/src/mapping-kit/value-keys.ts
@@ -0,0 +1,268 @@
+type ValueType = 'enrichment' | 'function' | 'literal' | 'variable'
+
+function isObject(value: any): value is object {
+ return value !== null && typeof value === 'object'
+}
+
+export interface DirectiveMetadata {
+ _metadata?: {
+ label?: string
+ type: ValueType
+ }
+}
+
+export function isDirective(value: FieldValue): value is Directive {
+ return (
+ value !== null &&
+ typeof value === 'object' &&
+ Object.keys(value).some((key) =>
+ ['@if', '@path', '@template', '@literal', '@arrayPath', '@case', '@replace', '@json', '@flatten'].includes(key)
+ )
+ )
+}
+
+export interface LiteralDirective extends DirectiveMetadata {
+ '@literal': PrimitiveValue | Record
+}
+
+export function isLiteralDirective(value: FieldValue): value is LiteralDirective {
+ return isDirective(value) && '@literal' in value
+}
+export interface TemplateDirective extends DirectiveMetadata {
+ '@template': string
+}
+
+export function isTemplateDirective(value: FieldValue): value is TemplateDirective {
+ return isDirective(value) && '@template' in value
+}
+
+export function getFieldValue(value: FieldValue): unknown {
+ if (isTemplateDirective(value)) {
+ return value['@template']
+ }
+
+ return value
+}
+
+export interface PathDirective extends DirectiveMetadata {
+ '@path': string
+}
+
+export function isPathDirective(value: FieldValue): value is PathDirective {
+ return isDirective(value) && '@path' in value
+}
+
+type RequireOnlyOne = {
+ [K in Keys]-?: Partial, undefined>> & Required>
+}[Keys] &
+ Pick>
+
+export interface IfDirective extends DirectiveMetadata {
+ '@if': RequireOnlyOne<
+ {
+ blank?: FieldValue
+ else?: FieldValue
+ exists?: FieldValue
+ then: FieldValue
+ },
+ 'blank' | 'exists'
+ >
+}
+
+export function isIfDirective(value: FieldValue): value is IfDirective {
+ return (
+ isDirective(value) &&
+ '@if' in value &&
+ value['@if'] !== null &&
+ typeof value['@if'] === 'object' &&
+ ('exists' in value['@if'] || 'blank' in value['@if'])
+ )
+}
+
+export interface ArrayPathDirective extends DirectiveMetadata {
+ '@arrayPath': [Directive | string, { [key: string]: FieldValue } | undefined] | [Directive | string]
+}
+
+export function isArrayPathDirective(value: FieldValue): value is ArrayPathDirective {
+ return isDirective(value) && '@arrayPath' in value && Array.isArray(value['@arrayPath'])
+}
+
+export interface CaseDirective extends DirectiveMetadata {
+ '@case': {
+ operator: string
+ value?: FieldValue
+ }
+}
+
+export function isCaseDirective(value: FieldValue): value is CaseDirective {
+ return (
+ isDirective(value) &&
+ '@case' in value &&
+ value['@case'] !== null &&
+ typeof value['@case'] === 'object' &&
+ 'operator' in value['@case']
+ )
+}
+
+export interface ReplaceDirective extends DirectiveMetadata {
+ '@replace': {
+ global: PrimitiveValue
+ ignorecase: PrimitiveValue
+ pattern: string
+ replacement: string
+ pattern2: string
+ replacement2: string
+ value?: FieldValue
+ }
+}
+
+export function isReplaceDirective(value: FieldValue): value is ReplaceDirective {
+ return (
+ isDirective(value) &&
+ '@replace' in value &&
+ value['@replace'] !== null &&
+ typeof value['@replace'] === 'object' &&
+ 'pattern' in value['@replace']
+ )
+}
+
+export interface JSONDirective extends DirectiveMetadata {
+ '@json': {
+ value: FieldValue
+ mode: PrimitiveValue
+ }
+}
+
+export function isJSONDirective(value: FieldValue): value is JSONDirective {
+ return (
+ isDirective(value) &&
+ '@json' in value &&
+ value['@json'] !== null &&
+ typeof value['@json'] === 'object' &&
+ 'value' in value['@json']
+ )
+}
+
+export interface FlattenDirective extends DirectiveMetadata {
+ '@flatten': {
+ value: FieldValue
+ separator: string
+ }
+}
+
+export function isFlattenDirective(value: FieldValue): value is FlattenDirective {
+ return (
+ isDirective(value) &&
+ '@flatten' in value &&
+ value['@flatten'] !== null &&
+ typeof value['@flatten'] === 'object' &&
+ 'value' in value['@flatten']
+ )
+}
+
+type DirectiveKeysToType = {
+ ['@arrayPath']: (input: ArrayPathDirective) => T
+ ['@case']: (input: CaseDirective) => T
+ ['@if']: (input: IfDirective) => T
+ ['@literal']: (input: LiteralDirective) => T
+ ['@path']: (input: PathDirective) => T
+ ['@replace']: (input: ReplaceDirective) => T
+ ['@template']: (input: TemplateDirective) => T
+ ['@json']: (input: JSONDirective) => T
+ ['@flatten']: (input: FlattenDirective) => T
+}
+
+function directiveType(directive: Directive, checker: DirectiveKeysToType): T | null {
+ if (isArrayPathDirective(directive)) {
+ return checker['@arrayPath'](directive)
+ }
+ if (isCaseDirective(directive)) {
+ return checker['@case'](directive)
+ }
+ if (isIfDirective(directive)) {
+ return checker['@if'](directive)
+ }
+ if (isLiteralDirective(directive)) {
+ return checker['@literal'](directive)
+ }
+ if (isPathDirective(directive)) {
+ return checker['@path'](directive)
+ }
+ if (isReplaceDirective(directive)) {
+ return checker['@replace'](directive)
+ }
+ if (isTemplateDirective(directive)) {
+ return checker['@template'](directive)
+ }
+ if (isJSONDirective(directive)) {
+ return checker['@json'](directive)
+ }
+ if (isFlattenDirective(directive)) {
+ return checker['@flatten'](directive)
+ }
+ return null
+}
+
+export type Directive =
+ | ArrayPathDirective
+ | CaseDirective
+ | IfDirective
+ | LiteralDirective
+ | PathDirective
+ | ReplaceDirective
+ | TemplateDirective
+ | JSONDirective
+ | FlattenDirective
+
+export type PrimitiveValue = boolean | number | string | null
+export type FieldValue = Directive | PrimitiveValue | { [key: string]: FieldValue } | FieldValue[] | undefined
+
+/**
+ * @param value
+ * @returns an array containing all keys of nested @directives
+ */
+export function getFieldValueKeys(value: FieldValue): string[] {
+ if (isDirective(value)) {
+ return (
+ directiveType(value, {
+ '@arrayPath': (input: ArrayPathDirective) => input['@arrayPath'].flatMap(getRawKeys),
+ '@case': (input: CaseDirective) => getRawKeys(input['@case'].value),
+ '@if': (input: IfDirective) => [
+ ...getRawKeys(input['@if'].blank),
+ ...getRawKeys(input['@if'].exists),
+ ...getRawKeys(input['@if'].then),
+ ...getRawKeys(input['@if'].else)
+ ],
+ '@literal': (_: LiteralDirective) => [''],
+ '@path': (input: PathDirective) => [input['@path']],
+ '@replace': (input: ReplaceDirective) => getRawKeys(input['@replace'].value),
+ '@template': (input: TemplateDirective) => getTemplateKeys(input['@template']),
+ '@json': (input: JSONDirective) => getRawKeys(input['@json'].value),
+ '@flatten': (input: FlattenDirective) => getRawKeys(input['@flatten'].value)
+ })?.filter((k) => k) ?? []
+ )
+ } else if (isObject(value)) {
+ return Object.values(value).flatMap(getFieldValueKeys)
+ }
+ return []
+}
+
+/**
+ * Function to get raw keys from a FieldValue
+ */
+export function getRawKeys(input: FieldValue): string[] {
+ if (isDirective(input)) {
+ return getFieldValueKeys(input)
+ } else if (isObject(input)) {
+ return Object.values(input).flatMap(getFieldValueKeys)
+ }
+ return []
+}
+
+/**
+ * Function to grab all values between any set of {{}} in a string
+ */
+export function getTemplateKeys(input: string): string[] {
+ const regex = /{{(.*?)}}/g
+ return Array.from(input.matchAll(regex), (m) => m[1])
+}
diff --git a/packages/destination-actions/README.md b/packages/destination-actions/README.md
index 070bac1644..5e689b2887 100644
--- a/packages/destination-actions/README.md
+++ b/packages/destination-actions/README.md
@@ -6,7 +6,7 @@ Destination definitions and their actions
MIT License
-Copyright (c) 2023 Segment
+Copyright (c) 2024 Segment
Permission is hereby granted, free of charge, to any person obtaining a copy
diff --git a/packages/destination-actions/package.json b/packages/destination-actions/package.json
index 39d2b461ec..f891d2c724 100644
--- a/packages/destination-actions/package.json
+++ b/packages/destination-actions/package.json
@@ -1,7 +1,7 @@
{
"name": "@segment/action-destinations",
"description": "Destination Actions engine and definitions.",
- "version": "3.218.0",
+ "version": "3.254.0",
"repository": {
"type": "git",
"url": "https://github.com/segmentio/action-destinations",
@@ -23,11 +23,11 @@
"registry": "https://registry.npmjs.org"
},
"scripts": {
- "build": "yarn clean && yarn tsc -b tsconfig.build.json",
+ "build": "yarn tsc -b tsconfig.build.json",
"clean": "tsc -b tsconfig.build.json --clean",
"postclean": "rm -rf dist",
"prepublishOnly": "yarn build",
- "test": "jest --coverage",
+ "test": "jest",
"typecheck": "tsc -p tsconfig.build.json --noEmit"
},
"devDependencies": {
@@ -39,9 +39,12 @@
},
"dependencies": {
"@amplitude/ua-parser-js": "^0.7.25",
+ "@bufbuild/buf": "^1.28.0",
+ "@bufbuild/protobuf": "^1.4.2",
+ "@bufbuild/protoc-gen-es": "^1.4.2",
"@segment/a1-notation": "^2.1.4",
- "@segment/actions-core": "^3.83.0",
- "@segment/actions-shared": "^1.65.0",
+ "@segment/actions-core": "^3.103.0",
+ "@segment/actions-shared": "^1.84.0",
"@types/node": "^18.11.15",
"ajv-formats": "^2.1.1",
"aws4": "^1.12.0",
@@ -49,12 +52,18 @@
"dayjs": "^1.10.7",
"escape-goat": "^3",
"google-libphonenumber": "^3.2.31",
+ "kafkajs": "^2.2.4",
"liquidjs": "^9.37.0",
"lodash": "^4.17.21",
"ssh2-sftp-client": "^9.1.0"
},
"jest": {
"preset": "ts-jest",
+ "globals": {
+ "ts-jest": {
+ "isolatedModules": true
+ }
+ },
"testEnvironment": "node",
"modulePathIgnorePatterns": [
"/dist/"
diff --git a/packages/destination-actions/src/destinations/absmartly/__tests__/exposure.test.ts b/packages/destination-actions/src/destinations/absmartly/__tests__/exposure.test.ts
index 121fa89cec..a900a07b5d 100644
--- a/packages/destination-actions/src/destinations/absmartly/__tests__/exposure.test.ts
+++ b/packages/destination-actions/src/destinations/absmartly/__tests__/exposure.test.ts
@@ -8,14 +8,14 @@ jest.mock('../event')
describe('sendExposure()', () => {
const settings = { collectorEndpoint: 'http://test.com', environment: 'dev', apiKey: 'testkey' }
const payload: ExposurePayload = {
- publishedAt: '2023-01-01T00:00:00.3Z',
application: 'testapp',
agent: 'test-sdk',
exposure: {
+ publishedAt: 1672531200900,
units: [{ type: 'anonymousId', value: 'testid' }],
- exposures: [{ experiment: 'testexp', variant: 'testvar' }],
+ exposures: [{ experiment: 'testexp', variant: 'testvar', exposedAt: 1672531200300 }],
goals: [],
- attributes: [{ name: 'testattr', value: 'testval', setAt: 1238128318 }]
+ attributes: [{ name: 'testattr', value: 'testval', setAt: 1672531200200 }]
}
}
@@ -25,6 +25,7 @@ describe('sendExposure()', () => {
expect(() =>
sendExposure(
request,
+ 1672531300000,
{
...payload,
exposure: { ...payload.exposure, units: null }
@@ -35,6 +36,7 @@ describe('sendExposure()', () => {
expect(() =>
sendExposure(
request,
+ 1672531300000,
{
...payload,
exposure: { ...payload.exposure, units: [] }
@@ -50,6 +52,7 @@ describe('sendExposure()', () => {
expect(() =>
sendExposure(
request,
+ 1672531300000,
{
...payload,
exposure: { ...payload.exposure, exposures: null }
@@ -60,6 +63,7 @@ describe('sendExposure()', () => {
expect(() =>
sendExposure(
request,
+ 1672531300000,
{
...payload,
exposure: { ...payload.exposure, exposures: [] }
@@ -75,6 +79,7 @@ describe('sendExposure()', () => {
expect(() =>
sendExposure(
request,
+ 1672531300000,
{
...payload,
exposure: { ...payload.exposure, goals: [{}] }
@@ -84,31 +89,21 @@ describe('sendExposure()', () => {
).toThrowError(PayloadValidationError)
})
- it('should throw on invalid publishedAt', async () => {
+ it('should pass-through the exposure payload with adjusted timestamps', async () => {
const request = jest.fn()
- expect(() => sendExposure(request, { ...payload, publishedAt: 0 }, settings)).toThrowError(PayloadValidationError)
- expect(() =>
- sendExposure(
- request,
- {
- ...payload,
- publishedAt: 'invalid date'
- },
- settings
- )
- ).toThrowError(PayloadValidationError)
- })
-
- it('should pass-through the exposure payload with adjusted publishedAt', async () => {
- const request = jest.fn()
-
- await sendExposure(request, payload, settings)
+ await sendExposure(request, 1672531300000, payload, settings)
expect(sendEvent).toHaveBeenCalledWith(
request,
settings,
- { ...payload.exposure, publishedAt: 1672531200300 },
+ {
+ ...payload.exposure,
+ historic: true,
+ publishedAt: 1672531300000,
+ exposures: [{ ...payload.exposure.exposures[0], exposedAt: 1672531299400 }],
+ attributes: [{ ...payload.exposure.attributes[0], setAt: 1672531299300 }]
+ },
payload.agent,
payload.application
)
diff --git a/packages/destination-actions/src/destinations/absmartly/__tests__/goal.test.ts b/packages/destination-actions/src/destinations/absmartly/__tests__/goal.test.ts
index 8eff72495d..e41618247c 100644
--- a/packages/destination-actions/src/destinations/absmartly/__tests__/goal.test.ts
+++ b/packages/destination-actions/src/destinations/absmartly/__tests__/goal.test.ts
@@ -12,8 +12,6 @@ describe('sendGoal()', () => {
anonymousId: 'testid'
},
name: 'testgoal',
- publishedAt: '2023-01-01T00:00:00.3Z',
- achievedAt: '2023-01-01T00:00:00.000000Z',
application: 'testapp',
agent: 'test-sdk',
properties: {
@@ -24,10 +22,13 @@ describe('sendGoal()', () => {
it('should throw on missing name', async () => {
const request = jest.fn()
- expect(() => sendGoal(request, { ...payload, name: '' }, settings)).toThrowError(PayloadValidationError)
+ expect(() => sendGoal(request, 1672531300000, { ...payload, name: '' }, settings)).toThrowError(
+ PayloadValidationError
+ )
expect(() =>
sendGoal(
request,
+ 1672531300000,
{
...payload,
name: null
@@ -38,6 +39,7 @@ describe('sendGoal()', () => {
expect(() =>
sendGoal(
request,
+ 1672531300000,
{
...payload,
name: undefined
@@ -47,44 +49,13 @@ describe('sendGoal()', () => {
).toThrowError(PayloadValidationError)
})
- it('should throw on invalid publishedAt', async () => {
- const request = jest.fn()
-
- expect(() => sendGoal(request, { ...payload, publishedAt: 0 }, settings)).toThrowError(PayloadValidationError)
- expect(() =>
- sendGoal(
- request,
- {
- ...payload,
- publishedAt: 'invalid date'
- },
- settings
- )
- ).toThrowError(PayloadValidationError)
- })
-
- it('should throw on invalid achievedAt', async () => {
- const request = jest.fn()
-
- expect(() => sendGoal(request, { ...payload, achievedAt: 0 }, settings)).toThrowError(PayloadValidationError)
- expect(() =>
- sendGoal(
- request,
- {
- ...payload,
- achievedAt: 'invalid date'
- },
- settings
- )
- ).toThrowError(PayloadValidationError)
- })
-
it('should throw on invalid properties', async () => {
const request = jest.fn()
expect(() =>
sendGoal(
request,
+ 1672531300000,
{
...payload,
properties: 'bleh'
@@ -95,6 +66,7 @@ describe('sendGoal()', () => {
expect(() =>
sendGoal(
request,
+ 1672531300000,
{
...payload,
properties: 0
@@ -107,18 +79,19 @@ describe('sendGoal()', () => {
it('should send event with correct format', async () => {
const request = jest.fn()
- await sendGoal(request, payload, settings)
+ await sendGoal(request, 1672531300000, payload, settings)
expect(sendEvent).toHaveBeenCalledWith(
request,
settings,
{
- publishedAt: 1672531200300,
+ historic: true,
+ publishedAt: 1672531300000,
units: mapUnits(payload),
goals: [
{
name: payload.name,
- achievedAt: 1672531200000,
+ achievedAt: 1672531300000,
properties: payload.properties ?? null
}
]
diff --git a/packages/destination-actions/src/destinations/absmartly/__tests__/timestamp.test.ts b/packages/destination-actions/src/destinations/absmartly/__tests__/timestamp.test.ts
index 8fac1e2770..d040998298 100644
--- a/packages/destination-actions/src/destinations/absmartly/__tests__/timestamp.test.ts
+++ b/packages/destination-actions/src/destinations/absmartly/__tests__/timestamp.test.ts
@@ -36,4 +36,11 @@ describe('unixTimestampOf()', () => {
expect(unixTimestampOf('2023-01-01T00:00:00.00345Z')).toBe(1672531200003)
expect(unixTimestampOf('2023-01-01T00:00:00.003456Z')).toBe(1672531200003)
})
+
+ it('should convert Date to number representing Unix timestamp in milliseconds', async () => {
+ expect(unixTimestampOf(new Date('2000-01-01T00:00:00Z'))).toBe(946684800000)
+ expect(unixTimestampOf(new Date('2023-01-01T00:00:00.003Z'))).toBe(1672531200003)
+ expect(unixTimestampOf(new Date('2023-01-01T00:00:00.00345Z'))).toBe(1672531200003)
+ expect(unixTimestampOf(new Date('2023-01-01T00:00:00.003456Z'))).toBe(1672531200003)
+ })
})
diff --git a/packages/destination-actions/src/destinations/absmartly/event.ts b/packages/destination-actions/src/destinations/absmartly/event.ts
index a45074afc4..cf64f4544d 100644
--- a/packages/destination-actions/src/destinations/absmartly/event.ts
+++ b/packages/destination-actions/src/destinations/absmartly/event.ts
@@ -1,4 +1,4 @@
-import { InputField, JSONObject, ModifiedResponse, RequestClient } from '@segment/actions-core'
+import { InputField, ModifiedResponse, RequestClient } from '@segment/actions-core'
import { Settings } from './generated-types'
import { PublishRequestUnit } from './unit'
import { PublishRequestAttribute } from './attribute'
@@ -6,30 +6,26 @@ import { PublishRequestGoal } from './goal'
import { Data } from 'ws'
export interface PublishRequestEvent {
+ historic?: boolean
publishedAt: number
units: PublishRequestUnit[]
goals?: PublishRequestGoal[]
- exposures?: JSONObject[]
+ exposures?: {
+ name: string
+ variant: number
+ exposedAt: number
+ assigned: boolean
+ eligible: boolean
+ }[]
attributes?: PublishRequestAttribute[]
}
export interface DefaultPayload {
- publishedAt: string | number
agent?: string
application?: string
}
export const defaultEventFields: Record = {
- publishedAt: {
- label: 'Event Sent Time',
- type: 'datetime',
- required: true,
- description:
- 'Exact timestamp when the event was sent (measured by the client clock). Must be an ISO 8601 date-time string, or a Unix timestamp (milliseconds) number',
- default: {
- '@path': '$.sentAt'
- }
- },
agent: {
label: 'Agent',
type: 'string',
diff --git a/packages/destination-actions/src/destinations/absmartly/exposure.ts b/packages/destination-actions/src/destinations/absmartly/exposure.ts
index ddfe383a07..110f4f2cda 100644
--- a/packages/destination-actions/src/destinations/absmartly/exposure.ts
+++ b/packages/destination-actions/src/destinations/absmartly/exposure.ts
@@ -1,4 +1,10 @@
-import { InputField, ModifiedResponse, PayloadValidationError, RequestClient } from '@segment/actions-core'
+import {
+ InputField,
+ JSONPrimitive,
+ ModifiedResponse,
+ PayloadValidationError,
+ RequestClient
+} from '@segment/actions-core'
import { defaultEventFields, DefaultPayload, PublishRequestEvent, sendEvent } from './event'
import { Settings } from './generated-types'
import { isValidTimestamp, unixTimestampOf } from './timestamp'
@@ -23,11 +29,18 @@ export const defaultExposureFields: Record = {
...defaultEventFields
}
-function isValidExposure(exposure?: PublishRequestEvent | Record): exposure is PublishRequestEvent {
+function isValidExposureRequest(
+ exposure?: PublishRequestEvent | Record
+): exposure is PublishRequestEvent {
if (exposure == null || typeof exposure != 'object') {
return false
}
+ const publishedAt = exposure['publishedAt'] as JSONPrimitive
+ if (!isValidTimestamp(publishedAt)) {
+ return false
+ }
+
const units = exposure['units']
if (!Array.isArray(units) || units.length == 0) {
return false
@@ -38,6 +51,10 @@ function isValidExposure(exposure?: PublishRequestEvent | Record typeof x['exposedAt'] !== 'number' || !isValidTimestamp(x['exposedAt']))) {
+ return false
+ }
+
const goals = exposure['goals']
if (goals != null && (!Array.isArray(goals) || goals.length > 0)) {
return false
@@ -53,32 +70,40 @@ function isValidExposure(exposure?: PublishRequestEvent | Record> {
- if (!isValidTimestamp(payload.publishedAt)) {
- throw new PayloadValidationError(
- 'Exposure `publishedAt` is required to be an ISO 8601 date-time string, or a Unix timestamp (milliseconds) number'
- )
- }
-
- const exposure = payload.exposure
- if (exposure == null || typeof exposure != 'object') {
+ const exposureRequest = payload.exposure as unknown as PublishRequestEvent
+ if (exposureRequest == null || typeof exposureRequest != 'object') {
throw new PayloadValidationError('Field `exposure` is required to be an object when tracking exposures')
}
- if (!isValidExposure(exposure)) {
+ if (!isValidExposureRequest(exposureRequest)) {
throw new PayloadValidationError(
'Field `exposure` is malformed or contains goals. Ensure you are sending a valid ABsmartly exposure payload without goals.'
)
}
+ const offset = timestamp - unixTimestampOf(exposureRequest.publishedAt)
+ const exposures = exposureRequest.exposures?.map((x) => ({
+ ...x,
+ exposedAt: x.exposedAt + offset
+ }))
+ const attributes = exposureRequest.attributes?.map((x) => ({
+ ...x,
+ setAt: x.setAt + offset
+ }))
+
return sendEvent(
request,
settings,
{
- ...exposure,
- publishedAt: unixTimestampOf(payload.publishedAt)
+ ...exposureRequest,
+ historic: true,
+ publishedAt: timestamp,
+ exposures,
+ attributes
},
payload.agent,
payload.application
diff --git a/packages/destination-actions/src/destinations/absmartly/goal.ts b/packages/destination-actions/src/destinations/absmartly/goal.ts
index 1481e4d994..7fc22e29ad 100644
--- a/packages/destination-actions/src/destinations/absmartly/goal.ts
+++ b/packages/destination-actions/src/destinations/absmartly/goal.ts
@@ -2,7 +2,7 @@ import { mapUnits, Units } from './unit'
import { InputField, ModifiedResponse, PayloadValidationError, RequestClient } from '@segment/actions-core'
import { sendEvent, PublishRequestEvent, defaultEventFields, DefaultPayload } from './event'
import { Settings } from './generated-types'
-import { isValidTimestamp, unixTimestampOf } from './timestamp'
+import { unixTimestampOf } from './timestamp'
import { Data } from 'ws'
export interface PublishRequestGoal {
@@ -13,7 +13,6 @@ export interface PublishRequestGoal {
export interface GoalPayload extends Units, DefaultPayload {
name: string
- achievedAt: string | number
properties?: null | Record
}
@@ -43,16 +42,6 @@ export const defaultGoalFields: Record = {
'@path': '$.event'
}
},
- achievedAt: {
- label: 'Goal Achievement Time',
- type: 'datetime',
- required: true,
- description:
- 'Exact timestamp when the goal was achieved (measured by the client clock). Must be an ISO 8601 date-time string, or a Unix timestamp (milliseconds) number',
- default: {
- '@path': '$.originalTimestamp'
- }
- },
properties: {
label: 'Goal Properties',
type: 'object',
@@ -67,6 +56,7 @@ export const defaultGoalFields: Record = {
export function sendGoal(
request: RequestClient,
+ timestamp: number,
payload: GoalPayload,
settings: Settings
): Promise> {
@@ -74,29 +64,18 @@ export function sendGoal(
throw new PayloadValidationError('Goal `name` is required to be a non-empty string')
}
- if (!isValidTimestamp(payload.publishedAt)) {
- throw new PayloadValidationError(
- 'Goal `publishedAt` is required to be an ISO 8601 date-time string, or a Unix timestamp (milliseconds) number'
- )
- }
-
- if (!isValidTimestamp(payload.achievedAt)) {
- throw new PayloadValidationError(
- 'Goal `achievedAt` is required to be an ISO 8601 date-time string, or a Unix timestamp (milliseconds) number'
- )
- }
-
if (payload.properties != null && typeof payload.properties != 'object') {
throw new PayloadValidationError('Goal `properties` if present is required to be an object')
}
const event: PublishRequestEvent = {
- publishedAt: unixTimestampOf(payload.publishedAt),
+ historic: true,
+ publishedAt: unixTimestampOf(timestamp),
units: mapUnits(payload),
goals: [
{
name: payload.name,
- achievedAt: unixTimestampOf(payload.achievedAt),
+ achievedAt: unixTimestampOf(timestamp),
properties: payload.properties ?? null
}
]
diff --git a/packages/destination-actions/src/destinations/absmartly/segment.ts b/packages/destination-actions/src/destinations/absmartly/segment.ts
new file mode 100644
index 0000000000..00fdffbfeb
--- /dev/null
+++ b/packages/destination-actions/src/destinations/absmartly/segment.ts
@@ -0,0 +1,11 @@
+export interface RequestData {
+ rawData: {
+ timestamp: string
+ type: string
+ receivedAt: string
+ sentAt: string
+ }
+ rawMapping: Record
+ settings: Settings
+ payload: Payload
+}
diff --git a/packages/destination-actions/src/destinations/absmartly/timestamp.ts b/packages/destination-actions/src/destinations/absmartly/timestamp.ts
index d9a195fb98..204a7d5f54 100644
--- a/packages/destination-actions/src/destinations/absmartly/timestamp.ts
+++ b/packages/destination-actions/src/destinations/absmartly/timestamp.ts
@@ -9,7 +9,8 @@ export function isValidTimestamp(timestamp: JSONPrimitive): boolean {
return false
}
-export function unixTimestampOf(timestamp: JSONPrimitive): number {
+export function unixTimestampOf(timestamp: JSONPrimitive | Date): number {
if (typeof timestamp === 'number') return timestamp
+ if (timestamp instanceof Date) return timestamp.getTime()
return Date.parse(timestamp as string)
}
diff --git a/packages/destination-actions/src/destinations/absmartly/trackExposure/__tests__/index.test.ts b/packages/destination-actions/src/destinations/absmartly/trackExposure/__tests__/index.test.ts
index b826793df4..1d1c8850d6 100644
--- a/packages/destination-actions/src/destinations/absmartly/trackExposure/__tests__/index.test.ts
+++ b/packages/destination-actions/src/destinations/absmartly/trackExposure/__tests__/index.test.ts
@@ -1,6 +1,8 @@
import nock from 'nock'
import { createTestEvent, createTestIntegration, SegmentEvent } from '@segment/actions-core'
import Destination from '../../index'
+import { unixTimestampOf } from '../../timestamp'
+import { PublishRequestEvent } from '../../event'
const testDestination = createTestIntegration(Destination)
@@ -17,7 +19,7 @@ describe('ABsmartly.trackExposure', () => {
anonymousId: 'anon-123',
properties: {
exposure: {
- publishedAt: 123,
+ publishedAt: 1602531300000,
units: [{ type: 'anonymousId', uid: 'anon-123' }],
exposures: [
{
@@ -50,15 +52,19 @@ describe('ABsmartly.trackExposure', () => {
useDefaultMappings: true
})
+ const timestamp = unixTimestampOf(exposureEvent.timestamp!)
+ const exposureRequest = exposureEvent.properties.exposure as PublishRequestEvent
+
expect(responses.length).toBe(1)
expect(responses[0].status).toBe(200)
expect(await responses[0].request.json()).toStrictEqual({
- publishedAt: 1672531200100,
+ historic: true,
+ publishedAt: timestamp,
units: [{ type: 'anonymousId', uid: 'anon-123' }],
exposures: [
{
assigned: true,
- exposedAt: 1602531200000,
+ exposedAt: timestamp - (exposureRequest.publishedAt - exposureRequest.exposures?.[0].exposedAt),
id: 10,
name: 'test_experiment'
}
@@ -67,7 +73,7 @@ describe('ABsmartly.trackExposure', () => {
{
name: 'test',
value: 'test',
- setAt: 1602530000000
+ setAt: timestamp - (exposureRequest.publishedAt - exposureRequest.attributes?.[0].setAt)
}
]
})
diff --git a/packages/destination-actions/src/destinations/absmartly/trackExposure/generated-types.ts b/packages/destination-actions/src/destinations/absmartly/trackExposure/generated-types.ts
index cfade7555e..d95f8bd3cf 100644
--- a/packages/destination-actions/src/destinations/absmartly/trackExposure/generated-types.ts
+++ b/packages/destination-actions/src/destinations/absmartly/trackExposure/generated-types.ts
@@ -7,10 +7,6 @@ export interface Payload {
exposure: {
[k: string]: unknown
}
- /**
- * Exact timestamp when the event was sent (measured by the client clock). Must be an ISO 8601 date-time string, or a Unix timestamp (milliseconds) number
- */
- publishedAt: string | number
/**
* Optional agent identifier that originated the event. Used to identify which SDK generated the event.
*/
diff --git a/packages/destination-actions/src/destinations/absmartly/trackExposure/index.ts b/packages/destination-actions/src/destinations/absmartly/trackExposure/index.ts
index 0c09c0d7ba..a0f1ea827c 100644
--- a/packages/destination-actions/src/destinations/absmartly/trackExposure/index.ts
+++ b/packages/destination-actions/src/destinations/absmartly/trackExposure/index.ts
@@ -1,7 +1,9 @@
import type { ActionDefinition } from '@segment/actions-core'
import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'
-import { defaultExposureFields, sendExposure } from '../exposure'
+import { defaultExposureFields, ExposurePayload, sendExposure } from '../exposure'
+import { RequestData } from '../segment'
+import { unixTimestampOf } from '../timestamp'
const fields = { ...defaultExposureFields }
@@ -10,8 +12,12 @@ const action: ActionDefinition = {
description: 'Send an experiment exposure event to ABsmartly',
fields: fields,
defaultSubscription: 'type = "track" and event = "Experiment Viewed"',
- perform: (request, { payload, settings }) => {
- return sendExposure(request, payload, settings)
+ perform: (request, data) => {
+ const requestData = data as RequestData
+ const timestamp = unixTimestampOf(requestData.rawData.timestamp)
+ const payload = requestData.payload
+ const settings = requestData.settings
+ return sendExposure(request, timestamp, payload, settings)
}
}
diff --git a/packages/destination-actions/src/destinations/absmartly/trackGoal/__tests__/index.test.ts b/packages/destination-actions/src/destinations/absmartly/trackGoal/__tests__/index.test.ts
index c17abf3591..a13fc75625 100644
--- a/packages/destination-actions/src/destinations/absmartly/trackGoal/__tests__/index.test.ts
+++ b/packages/destination-actions/src/destinations/absmartly/trackGoal/__tests__/index.test.ts
@@ -1,6 +1,7 @@
import nock from 'nock'
import { createTestEvent, createTestIntegration, SegmentEvent } from '@segment/actions-core'
import Destination from '../../index'
+import { unixTimestampOf } from '../../timestamp'
const testDestination = createTestIntegration(Destination)
@@ -32,17 +33,20 @@ describe('ABsmartly.trackGoal', () => {
useDefaultMappings: true
})
+ const timestamp = unixTimestampOf(baseEvent.timestamp!)
+
expect(responses.length).toBe(1)
expect(responses[0].status).toBe(200)
expect(await responses[0].request.json()).toStrictEqual({
- publishedAt: 1672531200100,
+ historic: true,
+ publishedAt: timestamp,
units: [
{ type: 'anonymousId', uid: 'anon-123' },
{ type: 'userId', uid: '123' }
],
goals: [
{
- achievedAt: 1672531200000,
+ achievedAt: timestamp,
name: 'Order Completed',
properties: baseEvent.properties
}
diff --git a/packages/destination-actions/src/destinations/absmartly/trackGoal/generated-types.ts b/packages/destination-actions/src/destinations/absmartly/trackGoal/generated-types.ts
index 13983dd560..26b0465b49 100644
--- a/packages/destination-actions/src/destinations/absmartly/trackGoal/generated-types.ts
+++ b/packages/destination-actions/src/destinations/absmartly/trackGoal/generated-types.ts
@@ -11,20 +11,12 @@ export interface Payload {
* The name of the goal to track
*/
name: string
- /**
- * Exact timestamp when the goal was achieved (measured by the client clock). Must be an ISO 8601 date-time string, or a Unix timestamp (milliseconds) number
- */
- achievedAt: string | number
/**
* Custom properties of the goal
*/
properties: {
[k: string]: unknown
}
- /**
- * Exact timestamp when the event was sent (measured by the client clock). Must be an ISO 8601 date-time string, or a Unix timestamp (milliseconds) number
- */
- publishedAt: string | number
/**
* Optional agent identifier that originated the event. Used to identify which SDK generated the event.
*/
diff --git a/packages/destination-actions/src/destinations/absmartly/trackGoal/index.ts b/packages/destination-actions/src/destinations/absmartly/trackGoal/index.ts
index 8ade0cec0a..324e747c53 100644
--- a/packages/destination-actions/src/destinations/absmartly/trackGoal/index.ts
+++ b/packages/destination-actions/src/destinations/absmartly/trackGoal/index.ts
@@ -1,7 +1,9 @@
import type { ActionDefinition } from '@segment/actions-core'
import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'
-import { defaultGoalFields, sendGoal } from '../goal'
+import { defaultGoalFields, GoalPayload, sendGoal } from '../goal'
+import { RequestData } from '../segment'
+import { unixTimestampOf } from '../timestamp'
const fields = { ...defaultGoalFields }
@@ -10,8 +12,12 @@ const action: ActionDefinition = {
description: 'Send a goal event to ABsmartly',
fields: fields,
defaultSubscription: 'type = "track" and event != "Experiment Viewed"',
- perform: (request, { payload, settings }) => {
- return sendGoal(request, payload, settings)
+ perform: (request, data) => {
+ const requestData = data as RequestData
+ const timestamp = unixTimestampOf(requestData.rawData.timestamp)
+ const payload = requestData.payload
+ const settings = requestData.settings
+ return sendGoal(request, timestamp, payload, settings)
}
}
diff --git a/packages/destination-actions/src/destinations/accoil-analytics/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/accoil-analytics/__tests__/__snapshots__/snapshot.test.ts.snap
new file mode 100644
index 0000000000..eea73bf7d7
--- /dev/null
+++ b/packages/destination-actions/src/destinations/accoil-analytics/__tests__/__snapshots__/snapshot.test.ts.snap
@@ -0,0 +1,13 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Testing snapshot for actions-accoil-analytics destination: postToAccoil action - all fields 1`] = `
+Object {
+ "testType": "MlZ)Z",
+}
+`;
+
+exports[`Testing snapshot for actions-accoil-analytics destination: postToAccoil action - required fields 1`] = `
+Object {
+ "testType": "MlZ)Z",
+}
+`;
diff --git a/packages/destination-actions/src/destinations/accoil-analytics/__tests__/index.test.ts b/packages/destination-actions/src/destinations/accoil-analytics/__tests__/index.test.ts
new file mode 100644
index 0000000000..161b8d83fc
--- /dev/null
+++ b/packages/destination-actions/src/destinations/accoil-analytics/__tests__/index.test.ts
@@ -0,0 +1,19 @@
+import nock from 'nock'
+import { createTestIntegration } from '@segment/actions-core'
+import Definition from '../index'
+
+const testDestination = createTestIntegration(Definition)
+
+describe('Accoil Analytics', () => {
+ describe('testAuthentication', () => {
+ it('should Test Auth Header', async () => {
+ nock('https://in.accoil.com')
+ .post('/segment')
+ .reply(400, { message: "API Key should start with 'Basic' and be followed by a space and your API key." })
+
+ // This should match your authentication.fields
+ const authData = { api_key: 'secret' }
+ await expect(testDestination.testAuthentication(authData)).rejects.toThrowError()
+ })
+ })
+})
diff --git a/packages/destination-actions/src/destinations/accoil-analytics/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/accoil-analytics/__tests__/snapshot.test.ts
new file mode 100644
index 0000000000..6e0aa06180
--- /dev/null
+++ b/packages/destination-actions/src/destinations/accoil-analytics/__tests__/snapshot.test.ts
@@ -0,0 +1,77 @@
+import { createTestEvent, createTestIntegration } from '@segment/actions-core'
+import { generateTestData } from '../../../lib/test-data'
+import destination from '../index'
+import nock from 'nock'
+
+const testDestination = createTestIntegration(destination)
+const destinationSlug = 'actions-accoil-analytics'
+
+describe(`Testing snapshot for ${destinationSlug} destination:`, () => {
+ for (const actionSlug in destination.actions) {
+ it(`${actionSlug} action - required fields`, async () => {
+ const seedName = `${destinationSlug}#${actionSlug}`
+ const action = destination.actions[actionSlug]
+ const [eventData, settingsData] = generateTestData(seedName, destination, action, true)
+
+ nock(/.*/).persist().get(/.*/).reply(200)
+ nock(/.*/).persist().post(/.*/).reply(200)
+ nock(/.*/).persist().put(/.*/).reply(200)
+
+ const event = createTestEvent({
+ properties: eventData
+ })
+
+ const responses = await testDestination.testAction(actionSlug, {
+ event: event,
+ mapping: event.properties,
+ settings: settingsData,
+ auth: undefined
+ })
+
+ const request = responses[0].request
+ const rawBody = await request.text()
+
+ try {
+ const json = JSON.parse(rawBody)
+ expect(json).toMatchSnapshot()
+ return
+ } catch (err) {
+ expect(rawBody).toMatchSnapshot()
+ }
+
+ expect(request.headers).toMatchSnapshot()
+ })
+
+ it(`${actionSlug} action - all fields`, async () => {
+ const seedName = `${destinationSlug}#${actionSlug}`
+ const action = destination.actions[actionSlug]
+ const [eventData, settingsData] = generateTestData(seedName, destination, action, false)
+
+ nock(/.*/).persist().get(/.*/).reply(200)
+ nock(/.*/).persist().post(/.*/).reply(200)
+ nock(/.*/).persist().put(/.*/).reply(200)
+
+ const event = createTestEvent({
+ properties: eventData
+ })
+
+ const responses = await testDestination.testAction(actionSlug, {
+ event: event,
+ mapping: event.properties,
+ settings: settingsData,
+ auth: undefined
+ })
+
+ const request = responses[0].request
+ const rawBody = await request.text()
+
+ try {
+ const json = JSON.parse(rawBody)
+ expect(json).toMatchSnapshot()
+ return
+ } catch (err) {
+ expect(rawBody).toMatchSnapshot()
+ }
+ })
+ }
+})
diff --git a/packages/destination-actions/src/destinations/accoil-analytics/generated-types.ts b/packages/destination-actions/src/destinations/accoil-analytics/generated-types.ts
new file mode 100644
index 0000000000..16b0e17a0d
--- /dev/null
+++ b/packages/destination-actions/src/destinations/accoil-analytics/generated-types.ts
@@ -0,0 +1,8 @@
+// Generated file. DO NOT MODIFY IT BY HAND.
+
+export interface Settings {
+ /**
+ * Your Accoil.com API Key. You can find your API Key in your Accoil.com account settings.
+ */
+ api_key: string
+}
diff --git a/packages/destination-actions/src/destinations/accoil-analytics/index.ts b/packages/destination-actions/src/destinations/accoil-analytics/index.ts
new file mode 100644
index 0000000000..a2f32a6dd9
--- /dev/null
+++ b/packages/destination-actions/src/destinations/accoil-analytics/index.ts
@@ -0,0 +1,68 @@
+import { defaultValues, DestinationDefinition } from '@segment/actions-core'
+import type { Settings } from './generated-types'
+
+import postToAccoil from './postToAccoil'
+
+const destination: DestinationDefinition = {
+ name: 'Accoil Analytics',
+ slug: 'actions-accoil-analytics',
+ mode: 'cloud',
+
+ authentication: {
+ scheme: 'custom',
+ fields: {
+ api_key: {
+ label: 'API Key',
+ description: 'Your Accoil.com API Key. You can find your API Key in your Accoil.com account settings.',
+ type: 'password',
+ required: true
+ }
+ },
+ testAuthentication: async (request, { settings }) => {
+ const AUTH_KEY = Buffer.from(`${settings.api_key}:`).toString('base64')
+ return await request(`https://in.accoil.com/segment`, {
+ method: 'post',
+ headers: {
+ Authorization: `Basic ${AUTH_KEY}`
+ },
+ json: {}
+ })
+ }
+ },
+ presets: [
+ {
+ name: 'Track Event',
+ subscribe: 'type = "track"',
+ partnerAction: 'postToAccoil',
+ mapping: defaultValues(postToAccoil.fields),
+ type: 'automatic'
+ },
+ {
+ name: 'Group',
+ subscribe: 'type = "group"',
+ partnerAction: 'postToAccoil',
+ mapping: defaultValues(postToAccoil.fields),
+ type: 'automatic'
+ },
+ {
+ name: 'Identify User',
+ subscribe: 'type = "identify"',
+ partnerAction: 'postToAccoil',
+ mapping: defaultValues(postToAccoil.fields),
+ type: 'automatic'
+ }
+ ],
+ extendRequest: ({ settings }) => {
+ const AUTH_KEY = Buffer.from(`${settings.api_key}:`).toString('base64')
+ return {
+ headers: {
+ Authorization: `Basic ${AUTH_KEY}`
+ }
+ }
+ },
+ actions: {
+ postToAccoil
+ }
+}
+
+export default destination
diff --git a/packages/destination-actions/src/destinations/accoil-analytics/postToAccoil/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/accoil-analytics/postToAccoil/__tests__/__snapshots__/snapshot.test.ts.snap
new file mode 100644
index 0000000000..44a5f1a1fa
--- /dev/null
+++ b/packages/destination-actions/src/destinations/accoil-analytics/postToAccoil/__tests__/__snapshots__/snapshot.test.ts.snap
@@ -0,0 +1,13 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Testing snapshot for AccoilAnalytics's postToAccoil destination action: all fields 1`] = `
+Object {
+ "testType": "0L3I&x9a",
+}
+`;
+
+exports[`Testing snapshot for AccoilAnalytics's postToAccoil destination action: required fields 1`] = `
+Object {
+ "testType": "0L3I&x9a",
+}
+`;
diff --git a/packages/destination-actions/src/destinations/accoil-analytics/postToAccoil/__tests__/index.test.ts b/packages/destination-actions/src/destinations/accoil-analytics/postToAccoil/__tests__/index.test.ts
new file mode 100644
index 0000000000..6479b97f35
--- /dev/null
+++ b/packages/destination-actions/src/destinations/accoil-analytics/postToAccoil/__tests__/index.test.ts
@@ -0,0 +1,34 @@
+import nock from 'nock'
+import { createTestEvent, createTestIntegration } from '@segment/actions-core'
+import Destination from '../../index'
+
+const testDestination = createTestIntegration(Destination)
+
+describe('AccoilAnalytics.postToAccoil', () => {
+ // TODO: Test your action
+ const event1 = createTestEvent()
+
+ it('should validate api keys', async () => {
+ nock('https://in.accoil.com')
+ .post('/segment')
+ .reply(400, { message: "API Key should start with 'Basic' and be followed by a space and your API key." })
+ try {
+ await testDestination.testAuthentication({ api_key: 'secret' })
+ } catch (err: any) {
+ console.log('THIS IS ERROR', err)
+ expect(err.message).toContain('Credentials are invalid: 400 Bad Request')
+ }
+ })
+
+ it('should send data upstream', async () => {
+ nock('https://in.accoil.com').post('/segment').reply(202, {})
+
+ const response = await testDestination.testAction('postToAccoil', {
+ event: event1,
+ useDefaultMappings: true
+ })
+ expect(response.length).toBe(1)
+ expect(new URL(response[0].url).pathname).toBe('/segment')
+ expect(response[0].status).toBe(202)
+ })
+})
diff --git a/packages/destination-actions/src/destinations/accoil-analytics/postToAccoil/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/accoil-analytics/postToAccoil/__tests__/snapshot.test.ts
new file mode 100644
index 0000000000..a15bf157f1
--- /dev/null
+++ b/packages/destination-actions/src/destinations/accoil-analytics/postToAccoil/__tests__/snapshot.test.ts
@@ -0,0 +1,75 @@
+import { createTestEvent, createTestIntegration } from '@segment/actions-core'
+import { generateTestData } from '../../../../lib/test-data'
+import destination from '../../index'
+import nock from 'nock'
+
+const testDestination = createTestIntegration(destination)
+const actionSlug = 'postToAccoil'
+const destinationSlug = 'AccoilAnalytics'
+const seedName = `${destinationSlug}#${actionSlug}`
+
+describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination action:`, () => {
+ it('required fields', async () => {
+ const action = destination.actions[actionSlug]
+ const [eventData, settingsData] = generateTestData(seedName, destination, action, true)
+
+ nock(/.*/).persist().get(/.*/).reply(200)
+ nock(/.*/).persist().post(/.*/).reply(200)
+ nock(/.*/).persist().put(/.*/).reply(200)
+
+ const event = createTestEvent({
+ properties: eventData
+ })
+
+ const responses = await testDestination.testAction(actionSlug, {
+ event: event,
+ mapping: event.properties,
+ settings: settingsData,
+ auth: undefined
+ })
+
+ const request = responses[0].request
+ const rawBody = await request.text()
+
+ try {
+ const json = JSON.parse(rawBody)
+ expect(json).toMatchSnapshot()
+ return
+ } catch (err) {
+ expect(rawBody).toMatchSnapshot()
+ }
+
+ expect(request.headers).toMatchSnapshot()
+ })
+
+ it('all fields', async () => {
+ const action = destination.actions[actionSlug]
+ const [eventData, settingsData] = generateTestData(seedName, destination, action, false)
+
+ nock(/.*/).persist().get(/.*/).reply(200)
+ nock(/.*/).persist().post(/.*/).reply(200)
+ nock(/.*/).persist().put(/.*/).reply(200)
+
+ const event = createTestEvent({
+ properties: eventData
+ })
+
+ const responses = await testDestination.testAction(actionSlug, {
+ event: event,
+ mapping: event.properties,
+ settings: settingsData,
+ auth: undefined
+ })
+
+ const request = responses[0].request
+ const rawBody = await request.text()
+
+ try {
+ const json = JSON.parse(rawBody)
+ expect(json).toMatchSnapshot()
+ return
+ } catch (err) {
+ expect(rawBody).toMatchSnapshot()
+ }
+ })
+})
diff --git a/packages/destination-actions/src/destinations/accoil-analytics/postToAccoil/generated-types.ts b/packages/destination-actions/src/destinations/accoil-analytics/postToAccoil/generated-types.ts
new file mode 100644
index 0000000000..9cd068e3be
--- /dev/null
+++ b/packages/destination-actions/src/destinations/accoil-analytics/postToAccoil/generated-types.ts
@@ -0,0 +1,10 @@
+// Generated file. DO NOT MODIFY IT BY HAND.
+
+export interface Payload {
+ /**
+ * Segment Event Payload
+ */
+ segmentEventData: {
+ [k: string]: unknown
+ }
+}
diff --git a/packages/destination-actions/src/destinations/accoil-analytics/postToAccoil/index.ts b/packages/destination-actions/src/destinations/accoil-analytics/postToAccoil/index.ts
new file mode 100644
index 0000000000..6f5c06672c
--- /dev/null
+++ b/packages/destination-actions/src/destinations/accoil-analytics/postToAccoil/index.ts
@@ -0,0 +1,30 @@
+import type { ActionDefinition } from '@segment/actions-core'
+import type { Settings } from '../generated-types'
+import type { Payload } from './generated-types'
+
+const action: ActionDefinition = {
+ title: 'Post to Accoil',
+ description: 'Send Data to Accoil Analytics',
+ defaultSubscription: 'type = "track"',
+
+ fields: {
+ segmentEventData: {
+ label: 'Event Payload',
+ description: 'Segment Event Payload',
+ type: 'object',
+ unsafe_hidden: true,
+ required: true,
+ default: {
+ '@path': '$.'
+ }
+ }
+ },
+ perform: (request, { payload }) => {
+ return request(`https://in.accoil.com/segment`, {
+ method: 'post',
+ json: payload.segmentEventData
+ })
+ }
+}
+
+export default action
diff --git a/packages/destination-actions/src/destinations/acoustic-s3tc/generated-types.ts b/packages/destination-actions/src/destinations/acoustic-s3tc/generated-types.ts
index 2b64c2dd5b..8724bef796 100644
--- a/packages/destination-actions/src/destinations/acoustic-s3tc/generated-types.ts
+++ b/packages/destination-actions/src/destinations/acoustic-s3tc/generated-types.ts
@@ -24,7 +24,7 @@ export interface Settings {
/**
*
*
- * Last-Modified: 09.19.2023 10.30.43
+ * Last-Modified: 02.01.2024 10.30.43
*
*
*/
diff --git a/packages/destination-actions/src/destinations/acoustic-s3tc/index.ts b/packages/destination-actions/src/destinations/acoustic-s3tc/index.ts
index 2269e619ef..ec4ef23059 100644
--- a/packages/destination-actions/src/destinations/acoustic-s3tc/index.ts
+++ b/packages/destination-actions/src/destinations/acoustic-s3tc/index.ts
@@ -3,7 +3,7 @@ import { Settings } from './generated-types'
import receiveEvents from './receiveEvents/index'
const mod = `
-Last-Modified: 09.19.2023 10.30.43
+Last-Modified: 02.01.2024 10.30.43
`
//August 2023, refactor for S3Cache
@@ -15,7 +15,7 @@ const presets: DestinationDefinition['presets'] = [
partnerAction: 'receiveEvents',
mapping: {
...defaultValues(receiveEvents.fields),
- email: {
+ uniqueRecipientId: {
'@if': {
exists: { '@path': '$.properties.email' },
then: { '@path': '$.properties.email' },
@@ -31,7 +31,7 @@ const presets: DestinationDefinition['presets'] = [
partnerAction: 'receiveEvents',
mapping: {
...defaultValues(receiveEvents.fields),
- email: {
+ uniqueRecipientId: {
'@if': {
exists: { '@path': '$.traits.email' },
then: { '@path': '$.traits.email' },
diff --git a/packages/destination-actions/src/destinations/acoustic-s3tc/receiveEvents/__tests__/__snapshots__/eventprocessing.test.ts.snap b/packages/destination-actions/src/destinations/acoustic-s3tc/receiveEvents/__tests__/__snapshots__/eventprocessing.test.ts.snap
index 7f7b6be567..18874136b8 100644
--- a/packages/destination-actions/src/destinations/acoustic-s3tc/receiveEvents/__tests__/__snapshots__/eventprocessing.test.ts.snap
+++ b/packages/destination-actions/src/destinations/acoustic-s3tc/receiveEvents/__tests__/__snapshots__/eventprocessing.test.ts.snap
@@ -1,68 +1,82 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`addUpdateEvents addUpdateEvents should return expected output 1`] = `
-"EMAIL, EventSource, EventName, EventValue, EventTimestamp
-acmeTest@gmail.com, undefined Event, email, acmeTest@gmail.com, undefined
-acmeTest@gmail.com, undefined Event, action_source, system_generated, undefined
-acmeTest@gmail.com, undefined Event, cart_id, fff7b1597270349875cffad3852067ab, undefined
-acmeTest@gmail.com, undefined Event, category, Shopify (Littledata), undefined
-acmeTest@gmail.com, undefined Event, checkout_id, 26976972210285, undefined
-acmeTest@gmail.com, undefined Event, coupon, HONEY15, undefined
-acmeTest@gmail.com, undefined Event, currency, USD, undefined
-acmeTest@gmail.com, undefined Event, discount, 4.79, undefined
-acmeTest@gmail.com, undefined Event, presentment_amount, 31.98, undefined
-acmeTest@gmail.com, undefined Event, presentment_currency, USD, undefined
-acmeTest@gmail.com, undefined Event, price, 31.98, undefined
-acmeTest@gmail.com, undefined Event, products.0.brand, acme, undefined
-acmeTest@gmail.com, undefined Event, products.0.category, Fragrance, undefined
-acmeTest@gmail.com, undefined Event, products.0.image_url, https://cdn.shopify.com/s/files/1/0023/0021/5405/products/SimplyLavender_Prod_1.jpg?v=1649347142, undefined
-acmeTest@gmail.com, undefined Event, products.0.name, Simply Lavender, undefined
-acmeTest@gmail.com, undefined Event, products.0.presentment_amount, 12.99, undefined
-acmeTest@gmail.com, undefined Event, products.0.presentment_currency, USD, undefined
-acmeTest@gmail.com, undefined Event, products.0.price, 12.99, undefined
-acmeTest@gmail.com, undefined Event, products.0.product_id, 1542783500397, undefined
-acmeTest@gmail.com, undefined Event, products.0.quantity, 1, undefined
-acmeTest@gmail.com, undefined Event, products.0.shopify_product_id, 1542783500397, undefined
-acmeTest@gmail.com, undefined Event, products.0.shopify_variant_id, 14369408221293, undefined
-acmeTest@gmail.com, undefined Event, products.0.sku, NGL, undefined
-acmeTest@gmail.com, undefined Event, products.0.url, https://acme-scents.myshopify.com/products/simply-lavender, undefined
-acmeTest@gmail.com, undefined Event, products.0.variant, Simply Lavender, undefined
-acmeTest@gmail.com, undefined Event, products.1.brand, NEST New York, undefined
-acmeTest@gmail.com, undefined Event, products.1.category, Fragrance, undefined
-acmeTest@gmail.com, undefined Event, products.1.image_url, https://cdn.shopify.com/s/files/1/0023/0021/5405/products/Grapefruit_Prod_1.jpg?v=1649344617, undefined
-acmeTest@gmail.com, undefined Event, products.1.name, Grapefruit, undefined
-acmeTest@gmail.com, undefined Event, products.1.presentment_amount, 18.99, undefined
-acmeTest@gmail.com, undefined Event, products.1.presentment_currency, USD, undefined
-acmeTest@gmail.com, undefined Event, products.1.price, 18.99, undefined
-acmeTest@gmail.com, undefined Event, products.1.product_id, 3979374755949, undefined
-acmeTest@gmail.com, undefined Event, products.1.quantity, 1, undefined
-acmeTest@gmail.com, undefined Event, products.1.shopify_product_id, 3979374755949, undefined
-acmeTest@gmail.com, undefined Event, products.1.shopify_variant_id, 29660017000557, undefined
-acmeTest@gmail.com, undefined Event, products.1.sku, MXV, undefined
-acmeTest@gmail.com, undefined Event, products.1.url, https://acme-scents.myshopify.com/products/grapefruit, undefined
-acmeTest@gmail.com, undefined Event, products.1.variant, Grapefruit, undefined
-acmeTest@gmail.com, undefined Event, sent_from, Littledata app, undefined
-acmeTest@gmail.com, undefined Event, shipping_method, Standard Shipping (5-7 days), undefined
-acmeTest@gmail.com, undefined Event, source_name, web, undefined
-acmeTest@gmail.com, undefined Event, step, 2, undefined
-acmeTest@gmail.com, undefined Event, integration.name, shopify_littledata, undefined
-acmeTest@gmail.com, undefined Event, integration.version, 9.1, undefined
-acmeTest@gmail.com, undefined Event, library.name, analytics-node, undefined
-acmeTest@gmail.com, undefined Event, library.version, 3.5.0, undefined
-acmeTest@gmail.com, undefined Event, traits.address.city, greenville, undefined
-acmeTest@gmail.com, undefined Event, traits.address.country, us, undefined
-acmeTest@gmail.com, undefined Event, traits.address.postalCode, 29609, undefined
-acmeTest@gmail.com, undefined Event, traits.address.state, sc, undefined
-acmeTest@gmail.com, undefined Event, traits.email, acmeTest@gmail.com, undefined
-acmeTest@gmail.com, undefined Event, traits.firstName, james, undefined
-acmeTest@gmail.com, undefined Event, traits.lastName, acmeTest, undefined
-"
+Object {
+ "l": undefined,
+ "propertiesTraitsKV": Object {
+ "action_source": "system_generated",
+ "cart_id": "fff7b1597270349875cffad3852067ab",
+ "category": "Shopify (Littledata)",
+ "checkout_id": 26976972210285,
+ "coupon": "HONEY15",
+ "currency": "USD",
+ "discount": 4.79,
+ "email": "acmeTest@gmail.com",
+ "eventSource": "undefined Event",
+ "integration.name": "shopify_littledata",
+ "integration.version": "9.1",
+ "library.name": "analytics-node",
+ "library.version": "3.5.0",
+ "presentment_amount": "31.98",
+ "presentment_currency": "USD",
+ "price": 31.98,
+ "products.0.brand": "acme",
+ "products.0.category": "Fragrance",
+ "products.0.image_url": "https://cdn.shopify.com/s/files/1/0023/0021/5405/products/SimplyLavender_Prod_1.jpg?v=1649347142",
+ "products.0.name": "Simply Lavender",
+ "products.0.presentment_amount": "12.99",
+ "products.0.presentment_currency": "USD",
+ "products.0.price": 12.99,
+ "products.0.product_id": "1542783500397",
+ "products.0.quantity": 1,
+ "products.0.shopify_product_id": "1542783500397",
+ "products.0.shopify_variant_id": "14369408221293",
+ "products.0.sku": "NGL",
+ "products.0.url": "https://acme-scents.myshopify.com/products/simply-lavender",
+ "products.0.variant": "Simply Lavender",
+ "products.1.brand": "NEST New York",
+ "products.1.category": "Fragrance",
+ "products.1.image_url": "https://cdn.shopify.com/s/files/1/0023/0021/5405/products/Grapefruit_Prod_1.jpg?v=1649344617",
+ "products.1.name": "Grapefruit",
+ "products.1.presentment_amount": "18.99",
+ "products.1.presentment_currency": "USD",
+ "products.1.price": 18.99,
+ "products.1.product_id": "3979374755949",
+ "products.1.quantity": 1,
+ "products.1.shopify_product_id": "3979374755949",
+ "products.1.shopify_variant_id": "29660017000557",
+ "products.1.sku": "MXV",
+ "products.1.url": "https://acme-scents.myshopify.com/products/grapefruit",
+ "products.1.variant": "Grapefruit",
+ "sent_from": "Littledata app",
+ "shipping_method": "Standard Shipping (5-7 days)",
+ "source_name": "web",
+ "step": 2,
+ "timestamp": undefined,
+ "traits.address.city": "greenville",
+ "traits.address.country": "us",
+ "traits.address.postalCode": "29609",
+ "traits.address.state": "sc",
+ "traits.email": "acmeTest@gmail.com",
+ "traits.firstName": "james",
+ "traits.lastName": "acmeTest",
+ "undefined": "Audience Exited",
+ "uniqueRecipient": "acmeTest@gmail.com",
+ },
+}
`;
exports[`addUpdateEvents adds update events to CSV rows 1`] = `
-"EMAIL, EventSource, EventName, EventValue, EventTimestamp
-example@example.com, undefined Event, key1, value1, undefined
-"
+Object {
+ "l": undefined,
+ "propertiesTraitsKV": Object {
+ "eventSource": "undefined Event",
+ "key1": "value1",
+ "timestamp": undefined,
+ "undefined": "Audience Exited",
+ "uniqueRecipient": "example@example.com",
+ },
+}
`;
exports[`parseSections parseSections should match correct outcome 1`] = `
diff --git a/packages/destination-actions/src/destinations/acoustic-s3tc/receiveEvents/__tests__/eventprocessing.test.ts b/packages/destination-actions/src/destinations/acoustic-s3tc/receiveEvents/__tests__/eventprocessing.test.ts
index d43996459e..4223c52cea 100644
--- a/packages/destination-actions/src/destinations/acoustic-s3tc/receiveEvents/__tests__/eventprocessing.test.ts
+++ b/packages/destination-actions/src/destinations/acoustic-s3tc/receiveEvents/__tests__/eventprocessing.test.ts
@@ -186,7 +186,7 @@ describe('addUpdateEvents', () => {
// const retValue = await addUpdateEvents(request,payload,settings,auth,email);
const payload = {
- email: 'acmeTest99@gmail.com',
+ uniqueRecipientId: 'acmeTest99@gmail.com',
type: 'track',
enable_batching: false,
timestamp: '2023-02-12T15:07:21.381Z',
@@ -272,7 +272,7 @@ describe('addUpdateEvents', () => {
describe('addUpdateEvents', () => {
test('adds update events to CSV rows', () => {
const mockPayload = {
- email: 'example@example.com',
+ uniqueRecipientId: 'example@example.com',
type: 'track',
timestamp: '2023-02-07T02:19:23.469Z',
key_value_pairs: {
diff --git a/packages/destination-actions/src/destinations/acoustic-s3tc/receiveEvents/__tests__/index.test.ts b/packages/destination-actions/src/destinations/acoustic-s3tc/receiveEvents/__tests__/index.test.ts
index 474d09c902..f1d1c36c32 100644
--- a/packages/destination-actions/src/destinations/acoustic-s3tc/receiveEvents/__tests__/index.test.ts
+++ b/packages/destination-actions/src/destinations/acoustic-s3tc/receiveEvents/__tests__/index.test.ts
@@ -16,7 +16,7 @@ describe('Send Events Action', () => {
s3_secret: 'secret',
s3_region: 'us-east-1',
s3_bucket_accesspoint_alias: 'my-bucket',
- fileNamePrefix: 'prefix'
+ fileNamePrefix: 'prefix_'
} as Settings
test('perform ValidateSettings call with valid payload and settings', async () => {
diff --git a/packages/destination-actions/src/destinations/acoustic-s3tc/receiveEvents/__tests__/preCheck.test.ts b/packages/destination-actions/src/destinations/acoustic-s3tc/receiveEvents/__tests__/preCheck.test.ts
index e81760ac04..e604e5cf56 100644
--- a/packages/destination-actions/src/destinations/acoustic-s3tc/receiveEvents/__tests__/preCheck.test.ts
+++ b/packages/destination-actions/src/destinations/acoustic-s3tc/receiveEvents/__tests__/preCheck.test.ts
@@ -9,7 +9,7 @@ const validS3Settings = {
s3_secret: 'secret',
s3_region: 'us-east-1',
s3_bucket_accesspoint_alias: 'my-bucket',
- fileNamePrefix: 'prefix'
+ fileNamePrefix: 'prefix_'
}
describe('validateSettings', () => {
diff --git a/packages/destination-actions/src/destinations/acoustic-s3tc/receiveEvents/eventprocessing.ts b/packages/destination-actions/src/destinations/acoustic-s3tc/receiveEvents/eventprocessing.ts
index 05a391c8ab..6520d378f2 100644
--- a/packages/destination-actions/src/destinations/acoustic-s3tc/receiveEvents/eventprocessing.ts
+++ b/packages/destination-actions/src/destinations/acoustic-s3tc/receiveEvents/eventprocessing.ts
@@ -8,7 +8,7 @@ export function parseSections(section: { [key: string]: string }, nestDepth: num
if (nestDepth > 10)
throw new IntegrationError(
- 'Event data exceeds nesting depth. Use Mapping to flatten the data to no more than 3 levels deep',
+ 'Event data exceeds nesting depth. Plese use mapping to flatten the data to no more than three levels deep.',
'NESTING_DEPTH_EXCEEDED',
400
)
@@ -31,30 +31,43 @@ export function parseSections(section: { [key: string]: string }, nestDepth: num
}
}
} catch (e) {
- throw new IntegrationError(
- `Unexpected Exception while parsing Event payload.\n ${e}`,
- 'UNEXPECTED_EVENT_PARSING_EXCEPTION',
- 400
- )
+ const ie = e as IntegrationError
+ if (ie.code === 'NESTING_DEPTH_EXCEEDED')
+ throw new IntegrationError(
+ 'Event data exceeds nesting depth. Use Mapping to flatten data structures to no more than 3 levels deep',
+ 'NESTING_DEPTH_EXCEEDED',
+ 400
+ )
+ else
+ throw new IntegrationError(
+ `Unexpected Exception while parsing Event payload.\n ${e}`,
+ 'UNEXPECTED_EVENT_PARSING_EXCEPTION',
+ 400
+ )
}
return parseResults
}
-export function addUpdateEvents(payload: Payload, email: string) {
- let eventName = ''
- let eventValue = ''
+export function addUpdateEvents(payload: Payload, uniqRecip: string) {
+ //Index Signature type
+ let propertiesTraitsKV: { [key: string]: string } = {}
- //Header
- let csvRows = 'EMAIL, EventSource, EventName, EventValue, EventTimestamp\n'
+ propertiesTraitsKV = {
+ ...propertiesTraitsKV,
+ ...{ ['uniqueRecipient']: uniqRecip }
+ }
- //Event Source
- const eventSource = get(payload, 'type', 'Null') + ' Event'
+ propertiesTraitsKV = {
+ ...propertiesTraitsKV,
+ ...{ ['eventSource']: get(payload, 'type', 'Null') + ' Event' }
+ }
//Timestamp
// "timestamp": "2023-02-07T02:19:23.469Z"`
- const timestamp = get(payload, 'timestamp', 'Null')
-
- let propertiesTraitsKV: { [key: string]: string } = {}
+ propertiesTraitsKV = {
+ ...propertiesTraitsKV,
+ ...{ ['timestamp']: get(payload, 'timestamp', 'Null') as string }
+ }
if (payload.key_value_pairs)
propertiesTraitsKV = {
@@ -84,17 +97,16 @@ export function addUpdateEvents(payload: Payload, email: string) {
...parseSections(payload.context as { [key: string]: string }, 0)
}
- let ak = ''
- let av = ''
-
const getValue = (o: object, part: string) => Object.entries(o).find(([k, _v]) => k.includes(part))?.[1] as string
const getKey = (o: object, part: string) => Object.entries(o).find(([k, _v]) => k.includes(part))?.[0] as string
+ let ak, av
+
if (getValue(propertiesTraitsKV, 'computation_class')?.toLowerCase() === 'audience') {
ak = getValue(propertiesTraitsKV, 'computation_key')
av = getValue(propertiesTraitsKV, `${ak}`)
- //Clean out already parsed attributes, reduce redundant attributes
+ //reduce redundant attributes
let x = getKey(propertiesTraitsKV, 'computation_class')
delete propertiesTraitsKV[`${x}`]
x = getKey(propertiesTraitsKV, 'computation_key')
@@ -106,32 +118,23 @@ export function addUpdateEvents(payload: Payload, email: string) {
ak = getValue(propertiesTraitsKV, 'audience_key')
av = getValue(propertiesTraitsKV, `${ak}`)
- //Clean out already parsed attributes, reduce redundant attributes
+ //reduce redundant attributes
const x = getKey(propertiesTraitsKV, 'audience_key')
delete propertiesTraitsKV[`${x}`]
delete propertiesTraitsKV[`${ak}`]
}
- if (av !== '') {
- let audiStatus = av
-
- eventValue = audiStatus
- audiStatus = audiStatus.toString().toLowerCase()
- if (audiStatus === 'true') eventValue = 'Audience Entered'
- if (audiStatus === 'false') eventValue = 'Audience Exited'
-
- eventName = ak
+ if (av !== null) {
+ if (av) av = 'Audience Entered'
+ if (!av) av = 'Audience Exited'
- //Initial Row
- csvRows += `${email}, ${eventSource}, ${eventName}, ${eventValue}, ${timestamp}\n`
+ propertiesTraitsKV = {
+ ...propertiesTraitsKV,
+ ...{ [`${ak}`]: av } //as { [key: string]: string }
+ }
}
- //Add the rest of the CSV rows
- for (const e in propertiesTraitsKV) {
- const eventName = e
- const eventValue = propertiesTraitsKV[e]
+ const l = propertiesTraitsKV.length
- csvRows += `${email}, ${eventSource}, ${eventName}, ${eventValue}, ${timestamp}\n`
- }
- return csvRows
+ return { propertiesTraitsKV, l }
}
diff --git a/packages/destination-actions/src/destinations/acoustic-s3tc/receiveEvents/generated-types.ts b/packages/destination-actions/src/destinations/acoustic-s3tc/receiveEvents/generated-types.ts
index 1c7f27d121..7e6a81fa00 100644
--- a/packages/destination-actions/src/destinations/acoustic-s3tc/receiveEvents/generated-types.ts
+++ b/packages/destination-actions/src/destinations/acoustic-s3tc/receiveEvents/generated-types.ts
@@ -32,9 +32,9 @@ export interface Payload {
[k: string]: unknown
}
/**
- * Do Not Modify - Email is required
+ * The field to be used to uniquely identify the Recipient in Acoustic. This field is required with Email preferred but not required.
*/
- email: string
+ uniqueRecipientId: string
/**
* Do Not Modify - The type of event. e.g. track or identify, this field is required
*/
diff --git a/packages/destination-actions/src/destinations/acoustic-s3tc/receiveEvents/index.ts b/packages/destination-actions/src/destinations/acoustic-s3tc/receiveEvents/index.ts
index 46c56e8b92..17de04d6ef 100644
--- a/packages/destination-actions/src/destinations/acoustic-s3tc/receiveEvents/index.ts
+++ b/packages/destination-actions/src/destinations/acoustic-s3tc/receiveEvents/index.ts
@@ -42,11 +42,12 @@ const action: ActionDefinition = {
'If the data is present in a Traits section, use this to map the attributes of a Traits Section (optional) ',
type: 'object'
},
- email: {
- label: 'Email',
- description: 'Do Not Modify - Email is required',
+ uniqueRecipientId: {
+ label: 'UniqueRecipientId',
+ description:
+ 'The field to be used to uniquely identify the Recipient in Acoustic. This field is required with Email preferred but not required.',
type: 'string',
- format: 'email',
+ format: 'text',
required: true,
default: {
'@if': {
@@ -76,10 +77,14 @@ const action: ActionDefinition = {
}
},
perform: async (request, { settings, payload }) => {
- const email = get(payload, 'email', '')
+ const uniqRecip = get(payload, 'uniqueRecipientId', '')
- if (!email) {
- throw new IntegrationError('Email Not Found, invalid Event received.', 'INVALID_EVENT_HAS_NO_EMAIL', 400)
+ if (!uniqRecip) {
+ throw new IntegrationError(
+ 'Unique Recipient Id Not Found, invalid Event received.',
+ 'INVALID_EVENT_HAS_NO_UNIQUERECIPIENTID',
+ 400
+ )
}
if (!payload.context && !payload.traits && !payload.properties)
@@ -92,10 +97,13 @@ const action: ActionDefinition = {
validateSettings(settings)
//Parse Event-Payload into an Update
- const csvRows = addUpdateEvents(payload, email)
+ const parsed = addUpdateEvents(payload, uniqRecip)
+ let jsonData = ''
+
+ jsonData = JSON.stringify(parsed.propertiesTraitsKV)
//Set File Store Name
- const fileName = settings.fileNamePrefix + `${new Date().toISOString().replace(/(\.|-|:)/g, '_')}` + '.csv'
+ const fileName = settings.fileNamePrefix + `${new Date().toISOString().replace(/(\.|-|:)/g, '_')}` + '.json'
const method = 'PUT'
const opts = await generateS3RequestOptions(
@@ -103,7 +111,7 @@ const action: ActionDefinition = {
settings.s3_region,
fileName,
method,
- csvRows,
+ jsonData,
settings.s3_access_key,
settings.s3_secret
)
diff --git a/packages/destination-actions/src/destinations/acoustic-s3tc/receiveEvents/preCheck.ts b/packages/destination-actions/src/destinations/acoustic-s3tc/receiveEvents/preCheck.ts
index 0f38e40e38..9248a1cce6 100644
--- a/packages/destination-actions/src/destinations/acoustic-s3tc/receiveEvents/preCheck.ts
+++ b/packages/destination-actions/src/destinations/acoustic-s3tc/receiveEvents/preCheck.ts
@@ -19,7 +19,25 @@ function validateSettings(settings: Settings) {
}
if (!settings.fileNamePrefix) {
- throw new IntegrationError('Missing Customer Prefix', 'MISSING_CUSTOMER_PREFIX', 400)
+ throw new IntegrationError(
+ 'Missing Customer Prefix. Customer Prefix should be of the form of 4-5 characters followed by an underscore character',
+ 'MISSING_CUSTOMER_PREFIX',
+ 400
+ )
+ }
+ if (!settings.fileNamePrefix.endsWith('_')) {
+ throw new IntegrationError(
+ 'Invalid Customer Prefix. Customer Prefix must end with an underscore character "_". ',
+ 'INVALID_CUSTOMER_PREFIX',
+ 400
+ )
+ }
+ if (settings.fileNamePrefix === 'customer_org_') {
+ throw new IntegrationError(
+ 'Unedited Customer Prefix. Customer Prefix remains as the default value, must edit and provide a valid Customer prefix (4-5 characters) followed by an underscore',
+ 'INVALID_CUSTOMER_PREFIX',
+ 400
+ )
}
}
diff --git a/packages/destination-actions/src/destinations/aggregations-io/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/aggregations-io/__tests__/__snapshots__/snapshot.test.ts.snap
new file mode 100644
index 0000000000..55e6b402f4
--- /dev/null
+++ b/packages/destination-actions/src/destinations/aggregations-io/__tests__/__snapshots__/snapshot.test.ts.snap
@@ -0,0 +1,15 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Testing snapshot for aggregations-io destination: send action - all fields 1`] = `
+Array [
+ Object {
+ "testType": "!80aBDHS1i",
+ },
+]
+`;
+
+exports[`Testing snapshot for aggregations-io destination: send action - required fields 1`] = `
+Array [
+ null,
+]
+`;
diff --git a/packages/destination-actions/src/destinations/aggregations-io/__tests__/index.test.ts b/packages/destination-actions/src/destinations/aggregations-io/__tests__/index.test.ts
new file mode 100644
index 0000000000..eb9c1c30e6
--- /dev/null
+++ b/packages/destination-actions/src/destinations/aggregations-io/__tests__/index.test.ts
@@ -0,0 +1,24 @@
+import nock from 'nock'
+import { createTestIntegration } from '@segment/actions-core'
+import Definition from '../index'
+
+const testDestination = createTestIntegration(Definition)
+const fakeApiKey = 'super-secret-key'
+const fakeIngestId = 'abc456'
+describe('Aggregations Io', () => {
+ describe('testAuthentication', () => {
+ it('should validate authentication inputs', async () => {
+ nock('https://app.aggregations.io')
+ .get(`/api/v1/organization/ping-w?ingest_id=${fakeIngestId}&schema=ARRAY_OF_EVENTS`)
+ .reply(200)
+ .matchHeader('x-api-token', fakeApiKey)
+
+ const authData = {
+ api_key: fakeApiKey,
+ ingest_id: fakeIngestId
+ }
+
+ await expect(testDestination.testAuthentication(authData)).resolves.not.toThrowError()
+ })
+ })
+})
diff --git a/packages/destination-actions/src/destinations/aggregations-io/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/aggregations-io/__tests__/snapshot.test.ts
new file mode 100644
index 0000000000..e428638194
--- /dev/null
+++ b/packages/destination-actions/src/destinations/aggregations-io/__tests__/snapshot.test.ts
@@ -0,0 +1,77 @@
+import { createTestEvent, createTestIntegration } from '@segment/actions-core'
+import { generateTestData } from '../../../lib/test-data'
+import destination from '../index'
+import nock from 'nock'
+
+const testDestination = createTestIntegration(destination)
+const destinationSlug = 'aggregations-io'
+
+describe(`Testing snapshot for ${destinationSlug} destination:`, () => {
+ for (const actionSlug in destination.actions) {
+ it(`${actionSlug} action - required fields`, async () => {
+ const seedName = `${destinationSlug}#${actionSlug}`
+ const action = destination.actions[actionSlug]
+ const [eventData, settingsData] = generateTestData(seedName, destination, action, true)
+
+ nock(/.*/).persist().get(/.*/).reply(200)
+ nock(/.*/).persist().post(/.*/).reply(200)
+ nock(/.*/).persist().put(/.*/).reply(200)
+
+ const event = createTestEvent({
+ properties: eventData
+ })
+
+ const responses = await testDestination.testAction(actionSlug, {
+ event: event,
+ mapping: event.properties,
+ settings: settingsData,
+ auth: undefined
+ })
+
+ const request = responses[0].request
+ const rawBody = await request.text()
+
+ try {
+ const json = JSON.parse(rawBody)
+ expect(json).toMatchSnapshot()
+ return
+ } catch (err) {
+ expect(rawBody).toMatchSnapshot()
+ }
+
+ expect(request.headers).toMatchSnapshot()
+ })
+
+ it(`${actionSlug} action - all fields`, async () => {
+ const seedName = `${destinationSlug}#${actionSlug}`
+ const action = destination.actions[actionSlug]
+ const [eventData, settingsData] = generateTestData(seedName, destination, action, false)
+
+ nock(/.*/).persist().get(/.*/).reply(200)
+ nock(/.*/).persist().post(/.*/).reply(200)
+ nock(/.*/).persist().put(/.*/).reply(200)
+
+ const event = createTestEvent({
+ properties: eventData
+ })
+
+ const responses = await testDestination.testAction(actionSlug, {
+ event: event,
+ mapping: event.properties,
+ settings: settingsData,
+ auth: undefined
+ })
+
+ const request = responses[0].request
+ const rawBody = await request.text()
+
+ try {
+ const json = JSON.parse(rawBody)
+ expect(json).toMatchSnapshot()
+ return
+ } catch (err) {
+ expect(rawBody).toMatchSnapshot()
+ }
+ })
+ }
+})
diff --git a/packages/destination-actions/src/destinations/aggregations-io/generated-types.ts b/packages/destination-actions/src/destinations/aggregations-io/generated-types.ts
new file mode 100644
index 0000000000..d1632123d8
--- /dev/null
+++ b/packages/destination-actions/src/destinations/aggregations-io/generated-types.ts
@@ -0,0 +1,12 @@
+// Generated file. DO NOT MODIFY IT BY HAND.
+
+export interface Settings {
+ /**
+ * Your Aggregations.io API Key. This key requires Write permissions.
+ */
+ api_key: string
+ /**
+ * The ID of the ingest you want to send data to. This ingest should be set up as "Array of JSON Objects". Find your ID on the Aggregations.io Organization page.
+ */
+ ingest_id: string
+}
diff --git a/packages/destination-actions/src/destinations/aggregations-io/index.ts b/packages/destination-actions/src/destinations/aggregations-io/index.ts
new file mode 100644
index 0000000000..b936462198
--- /dev/null
+++ b/packages/destination-actions/src/destinations/aggregations-io/index.ts
@@ -0,0 +1,59 @@
+import type { DestinationDefinition } from '@segment/actions-core'
+import { InvalidAuthenticationError } from '@segment/actions-core'
+import type { Settings } from './generated-types'
+import send from './send'
+import { AggregationsAuthError } from './types'
+
+const destination: DestinationDefinition = {
+ name: 'Aggregations.io (Actions)',
+ slug: 'actions-aggregations-io',
+ mode: 'cloud',
+ description: 'Send Segment events to Aggregations.io',
+ authentication: {
+ scheme: 'custom',
+ fields: {
+ api_key: {
+ label: 'API Key',
+ description: 'Your Aggregations.io API Key. This key requires Write permissions.',
+ type: 'password',
+ required: true
+ },
+ ingest_id: {
+ label: 'Ingest Id',
+ description:
+ 'The ID of the ingest you want to send data to. This ingest should be set up as "Array of JSON Objects". Find your ID on the Aggregations.io Organization page.',
+ type: 'string',
+ required: true
+ }
+ },
+ testAuthentication: async (request, { settings }) => {
+ try {
+ return await request(
+ `https://app.aggregations.io/api/v1/organization/ping-w?ingest_id=${settings.ingest_id}&schema=ARRAY_OF_EVENTS`,
+ {
+ method: 'get',
+ headers: {
+ 'x-api-token': settings.api_key
+ }
+ }
+ )
+ } catch (e: any) {
+ const error = e as AggregationsAuthError
+ if (error.response.data) {
+ const { message } = error.response.data
+ throw new InvalidAuthenticationError(message)
+ }
+ throw new InvalidAuthenticationError('Error Validating Credentials')
+ }
+ }
+ },
+ extendRequest({ settings }) {
+ return {
+ headers: { 'x-api-token': settings.api_key }
+ }
+ },
+ actions: {
+ send
+ }
+}
+export default destination
diff --git a/packages/destination-actions/src/destinations/aggregations-io/send/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/aggregations-io/send/__tests__/__snapshots__/snapshot.test.ts.snap
new file mode 100644
index 0000000000..544859c0fe
--- /dev/null
+++ b/packages/destination-actions/src/destinations/aggregations-io/send/__tests__/__snapshots__/snapshot.test.ts.snap
@@ -0,0 +1,15 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Testing snapshot for AggregationsIo's send destination action: all fields 1`] = `
+Array [
+ Object {
+ "testType": "#^vP0",
+ },
+]
+`;
+
+exports[`Testing snapshot for AggregationsIo's send destination action: required fields 1`] = `
+Array [
+ null,
+]
+`;
diff --git a/packages/destination-actions/src/destinations/aggregations-io/send/__tests__/index.test.ts b/packages/destination-actions/src/destinations/aggregations-io/send/__tests__/index.test.ts
new file mode 100644
index 0000000000..3cf1749059
--- /dev/null
+++ b/packages/destination-actions/src/destinations/aggregations-io/send/__tests__/index.test.ts
@@ -0,0 +1,42 @@
+import nock from 'nock'
+import { createTestEvent, createTestIntegration } from '@segment/actions-core'
+import Destination from '../../index'
+
+const testDestination = createTestIntegration(Destination)
+const testIngestId = 'abc123'
+const testApiKey = 'super-secret-key'
+const ingestUrl = 'https://ingest.aggregations.io'
+describe('AggregationsIo.send', () => {
+ const event1 = createTestEvent()
+ const event2 = createTestEvent()
+
+ it('should work for single event', async () => {
+ nock(ingestUrl).post(`/${testIngestId}`).reply(200).matchHeader('x-api-token', testApiKey)
+ const response = await testDestination.testAction('send', {
+ event: event1,
+ settings: {
+ api_key: testApiKey,
+ ingest_id: testIngestId
+ },
+ useDefaultMappings: true
+ })
+ expect(response.length).toBe(1)
+ expect(new URL(response[0].url).pathname).toBe('/' + testIngestId)
+ expect(response[0].status).toBe(200)
+ })
+
+ it('should work for batched events', async () => {
+ nock(ingestUrl).post(`/${testIngestId}`).reply(200).matchHeader('x-api-token', testApiKey)
+ const response = await testDestination.testBatchAction('send', {
+ events: [event1, event2],
+ settings: {
+ api_key: testApiKey,
+ ingest_id: testIngestId
+ },
+ useDefaultMappings: true
+ })
+ expect(response.length).toBe(1)
+ expect(new URL(response[0].url).pathname).toBe('/' + testIngestId)
+ expect(response[0].status).toBe(200)
+ })
+})
diff --git a/packages/destination-actions/src/destinations/aggregations-io/send/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/aggregations-io/send/__tests__/snapshot.test.ts
new file mode 100644
index 0000000000..7c3d8a1178
--- /dev/null
+++ b/packages/destination-actions/src/destinations/aggregations-io/send/__tests__/snapshot.test.ts
@@ -0,0 +1,75 @@
+import { createTestEvent, createTestIntegration } from '@segment/actions-core'
+import { generateTestData } from '../../../../lib/test-data'
+import destination from '../../index'
+import nock from 'nock'
+
+const testDestination = createTestIntegration(destination)
+const actionSlug = 'send'
+const destinationSlug = 'AggregationsIo'
+const seedName = `${destinationSlug}#${actionSlug}`
+
+describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination action:`, () => {
+ it('required fields', async () => {
+ const action = destination.actions[actionSlug]
+ const [eventData, settingsData] = generateTestData(seedName, destination, action, true)
+
+ nock(/.*/).persist().get(/.*/).reply(200)
+ nock(/.*/).persist().post(/.*/).reply(200)
+ nock(/.*/).persist().put(/.*/).reply(200)
+
+ const event = createTestEvent({
+ properties: eventData
+ })
+
+ const responses = await testDestination.testAction(actionSlug, {
+ event: event,
+ mapping: event.properties,
+ settings: settingsData,
+ auth: undefined
+ })
+
+ const request = responses[0].request
+ const rawBody = await request.text()
+
+ try {
+ const json = JSON.parse(rawBody)
+ expect(json).toMatchSnapshot()
+ return
+ } catch (err) {
+ expect(rawBody).toMatchSnapshot()
+ }
+
+ expect(request.headers).toMatchSnapshot()
+ })
+
+ it('all fields', async () => {
+ const action = destination.actions[actionSlug]
+ const [eventData, settingsData] = generateTestData(seedName, destination, action, false)
+
+ nock(/.*/).persist().get(/.*/).reply(200)
+ nock(/.*/).persist().post(/.*/).reply(200)
+ nock(/.*/).persist().put(/.*/).reply(200)
+
+ const event = createTestEvent({
+ properties: eventData
+ })
+
+ const responses = await testDestination.testAction(actionSlug, {
+ event: event,
+ mapping: event.properties,
+ settings: settingsData,
+ auth: undefined
+ })
+
+ const request = responses[0].request
+ const rawBody = await request.text()
+
+ try {
+ const json = JSON.parse(rawBody)
+ expect(json).toMatchSnapshot()
+ return
+ } catch (err) {
+ expect(rawBody).toMatchSnapshot()
+ }
+ })
+})
diff --git a/packages/destination-actions/src/destinations/aggregations-io/send/generated-types.ts b/packages/destination-actions/src/destinations/aggregations-io/send/generated-types.ts
new file mode 100644
index 0000000000..ded4cc4ef5
--- /dev/null
+++ b/packages/destination-actions/src/destinations/aggregations-io/send/generated-types.ts
@@ -0,0 +1,18 @@
+// Generated file. DO NOT MODIFY IT BY HAND.
+
+export interface Payload {
+ /**
+ * Payload to deliver (JSON-encoded).
+ */
+ data?: {
+ [k: string]: unknown
+ }
+ /**
+ * Enabling sending batches of events to Aggregations.io.
+ */
+ enable_batching: boolean
+ /**
+ * Maximum number of events to include in each batch. Actual batch sizes may be lower. If you know your events are large, you may want to tune your batch size down to meet API requirements.
+ */
+ batch_size?: number
+}
diff --git a/packages/destination-actions/src/destinations/aggregations-io/send/index.ts b/packages/destination-actions/src/destinations/aggregations-io/send/index.ts
new file mode 100644
index 0000000000..21448226f6
--- /dev/null
+++ b/packages/destination-actions/src/destinations/aggregations-io/send/index.ts
@@ -0,0 +1,47 @@
+import { ActionDefinition } from '@segment/actions-core'
+import type { Settings } from '../generated-types'
+import type { Payload } from './generated-types'
+
+const action: ActionDefinition = {
+ title: 'Send Events',
+ description: 'Send events to Aggregations.io.',
+ fields: {
+ data: {
+ label: 'Data',
+ description: 'Payload to deliver (JSON-encoded).',
+ type: 'object',
+ default: { '@path': '$.' }
+ },
+ enable_batching: {
+ label: 'Enable Batching',
+ description: 'Enabling sending batches of events to Aggregations.io.',
+ type: 'boolean',
+ required: true,
+ default: true
+ },
+ batch_size: {
+ label: 'Batch Size',
+ description:
+ 'Maximum number of events to include in each batch. Actual batch sizes may be lower. If you know your events are large, you may want to tune your batch size down to meet API requirements.',
+ type: 'number',
+ required: false,
+ default: 300,
+ readOnly: true,
+ unsafe_hidden: true
+ }
+ },
+ perform: (request, { settings, payload }) => {
+ return request('https://ingest.aggregations.io/' + settings.ingest_id, {
+ method: 'POST',
+ json: [payload.data]
+ })
+ },
+ performBatch: (request, { settings, payload }) => {
+ return request('https://ingest.aggregations.io/' + settings.ingest_id, {
+ method: 'POST',
+ json: payload.map((x) => x.data)
+ })
+ }
+}
+
+export default action
diff --git a/packages/destination-actions/src/destinations/aggregations-io/types.ts b/packages/destination-actions/src/destinations/aggregations-io/types.ts
new file mode 100644
index 0000000000..19b41df033
--- /dev/null
+++ b/packages/destination-actions/src/destinations/aggregations-io/types.ts
@@ -0,0 +1,9 @@
+import { HTTPError } from '@segment/actions-core'
+
+export class AggregationsAuthError extends HTTPError {
+ response: Response & {
+ data: {
+ message: string
+ }
+ }
+}
diff --git a/packages/destination-actions/src/destinations/airship/registerAndAssociate/generated-types.ts b/packages/destination-actions/src/destinations/airship/registerAndAssociate/generated-types.ts
index af70c38e51..ec79526a6f 100644
--- a/packages/destination-actions/src/destinations/airship/registerAndAssociate/generated-types.ts
+++ b/packages/destination-actions/src/destinations/airship/registerAndAssociate/generated-types.ts
@@ -1,6 +1,14 @@
// Generated file. DO NOT MODIFY IT BY HAND.
export interface Payload {
+ /**
+ * Email (default) or SMS
+ */
+ channel_type?: string
+ /**
+ * A long or short code the app is configured to send from (if using for SMS).
+ */
+ sms_sender?: string
/**
* The identifier assigned in Airship as the Named User
*/
@@ -22,7 +30,7 @@ export interface Payload {
*/
channel_object: {
/**
- * Email address to register (required)
+ * Email address or mobile number to register (required)
*/
address: string
/**
@@ -65,6 +73,10 @@ export interface Payload {
* If an email channel is suppressed, the reason for its suppression. Email channels with any suppression state set will not have any delivery to them fulfilled. If a more specific reason is not known, use imported. Possible values: spam_complaint, bounce, imported
*/
suppression_state?: string
+ /**
+ * The date-time when a user gave explicit permission to receive SMS messages.
+ */
+ sms_opted_in?: string
[k: string]: unknown
}
}
diff --git a/packages/destination-actions/src/destinations/airship/registerAndAssociate/index.ts b/packages/destination-actions/src/destinations/airship/registerAndAssociate/index.ts
index 5a8dfee5b0..d69b752737 100644
--- a/packages/destination-actions/src/destinations/airship/registerAndAssociate/index.ts
+++ b/packages/destination-actions/src/destinations/airship/registerAndAssociate/index.ts
@@ -1,13 +1,30 @@
import { ActionDefinition } from '@segment/actions-core'
import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'
-import { register, associate_named_user, getChannelId } from '../utilities'
+import { register, associate_named_user, getChannelId, EMAIL, SMS } from '../utilities'
const action: ActionDefinition = {
title: 'Register And Associate',
- description: 'Register an Email address and associate it with a Named User ID.',
- defaultSubscription: 'type = "track" and event="Email Address Registered"',
+ description: 'Register an Email address or SMS number and associate it with a Named User ID.',
+ defaultSubscription: 'type = "track" and event="Address Registered"',
fields: {
+ channel_type: {
+ label: 'Channel Type',
+ description: 'Email (default) or SMS',
+ type: 'string',
+ choices: [
+ { label: 'Email', value: EMAIL },
+ { label: 'SMS', value: SMS }
+ ],
+ default: EMAIL,
+ required: false
+ },
+ sms_sender: {
+ label: 'SMS Sender',
+ description: 'A long or short code the app is configured to send from (if using for SMS).',
+ type: 'string',
+ required: false
+ },
named_user_id: {
label: 'Airship Named User ID',
description: 'The identifier assigned in Airship as the Named User',
@@ -60,8 +77,8 @@ const action: ActionDefinition = {
required: true,
properties: {
address: {
- label: 'Email Address',
- description: 'Email address to register (required)',
+ label: 'Email Address or MSISDN',
+ description: 'Email address or mobile number to register (required)',
type: 'string',
required: true
},
@@ -126,10 +143,16 @@ const action: ActionDefinition = {
'If an email channel is suppressed, the reason for its suppression. Email channels with any suppression state set will not have any delivery to them fulfilled. If a more specific reason is not known, use imported. Possible values: spam_complaint, bounce, imported',
type: 'string',
required: false
+ },
+ sms_opted_in: {
+ label: 'SMS Opt In Date-Time',
+ description: 'The date-time when a user gave explicit permission to receive SMS messages.',
+ type: 'string',
+ required: false
}
},
default: {
- address: { '@path': '$.properties.email' },
+ address: { '@path': '$.properties.address' },
new_address: { '@path': '$.properties.new_email' },
commercial_opted_in: { '@path': '$.properties.commercial_opted_in' },
commercial_opted_out: { '@path': '$.properties.commercial_opted_out' },
@@ -139,7 +162,8 @@ const action: ActionDefinition = {
open_tracking_opted_out: { '@path': '$.properties.open_tracking_opted_out' },
transactional_opted_in: { '@path': '$.properties.transactional_opted_in' },
transactional_opted_out: { '@path': '$.properties.transactional_opted_out' },
- suppression_state: { '@path': '$.context.suppression_state' }
+ suppression_state: { '@path': '$.context.suppression_state' },
+ sms_opted_in: { '@path': '$.properties.sms_opted_in' }
}
}
},
@@ -170,9 +194,13 @@ const action: ActionDefinition = {
const data = JSON.parse(response_content)
const channel_id = data.channel_id
+ let channel_type = EMAIL.toLowerCase()
+ if (payload.channel_type) {
+ channel_type = payload.channel_type.toLowerCase()
+ }
if (payload.named_user_id && payload.named_user_id.length > 0) {
- // If there's a Named User ID to associate with the email address, do it here
- return await associate_named_user(request, settings, channel_id, payload.named_user_id)
+ // If there's a Named User ID to associate with the address, do it here
+ return await associate_named_user(request, settings, channel_id, payload.named_user_id, channel_type)
} else {
// If not, simply return the registration request, success or failure, for Segment to handle as per policy
return register_response
diff --git a/packages/destination-actions/src/destinations/airship/utilities.ts b/packages/destination-actions/src/destinations/airship/utilities.ts
index 9a0bcf79e0..694243fae4 100644
--- a/packages/destination-actions/src/destinations/airship/utilities.ts
+++ b/packages/destination-actions/src/destinations/airship/utilities.ts
@@ -4,7 +4,9 @@ import { Payload as CustomEventsPayload } from './customEvents/generated-types'
import { Payload as AttributesPayload } from './setAttributes/generated-types'
import { Payload as TagsPayload } from './manageTags/generated-types'
import { Payload as RegisterPayload } from './registerAndAssociate/generated-types'
-import { timezone } from '../segment/segment-properties'
+
+export const EMAIL = 'email'
+export const SMS = 'sms'
// exported Action function
export function register(
@@ -14,6 +16,8 @@ export function register(
old_channel: string | null
) {
let address_to_use = payload.channel_object.address
+ const channel_type = payload.channel_type ?? EMAIL
+
const endpoint = map_endpoint(settings.endpoint)
let register_uri = `${endpoint}/api/channels/email`
if (old_channel && payload.channel_object.new_address) {
@@ -24,78 +28,127 @@ export function register(
if (payload.locale && payload.locale.length > 0) {
country_language = _extract_country_language(payload.locale)
}
- const register_payload: {
- channel: {
- commercial_opted_in?: string
- commercial_opted_out?: string
- click_tracking_opted_in?: string
- click_tracking_opted_out?: string
- open_tracking_opted_in?: string
- open_tracking_opted_out?: string
- transactional_opted_in?: string
- transactional_opted_out?: string
- suppression_state?: string
- type: string
- address: string
+
+ let locale_country = ''
+ let locale_language = ''
+ if (Array.isArray(country_language) && country_language.length === 2) {
+ locale_language = country_language[0]
+ locale_country = country_language[1]
+ }
+
+ // set up email_email_register_payload
+ if (channel_type == SMS) {
+ register_uri = `${endpoint}/api/channels/sms`
+ const sms_register_payload: {
+ msisdn: string
+ sender?: string
+ opted_in?: string
timezone?: string
locale_language?: string
locale_country?: string
+ } = {
+ msisdn: address_to_use
}
- } = {
- channel: {
- type: 'email',
- address: address_to_use
+ if (payload.channel_object.sms_opted_in) {
+ sms_register_payload.opted_in = _parse_and_format_date(payload.channel_object.sms_opted_in)
}
+ if (payload.sms_sender) {
+ sms_register_payload.sender = payload.sms_sender
+ }
+ // additional properties
+ if (locale_language) {
+ sms_register_payload.locale_language = locale_language
+ }
+ if (locale_country) {
+ sms_register_payload.locale_country = locale_country
+ }
+ if (payload.timezone) {
+ sms_register_payload.timezone = payload.timezone
+ }
+ return do_request(request, register_uri, sms_register_payload)
+ } else {
+ const email_register_payload: {
+ channel: {
+ commercial_opted_in?: string
+ commercial_opted_out?: string
+ click_tracking_opted_in?: string
+ click_tracking_opted_out?: string
+ open_tracking_opted_in?: string
+ open_tracking_opted_out?: string
+ transactional_opted_in?: string
+ transactional_opted_out?: string
+ suppression_state?: string
+ type?: string
+ address: string
+ timezone?: string
+ locale_language?: string
+ locale_country?: string
+ sms_opted_in?: string
+ sms_sender?: string
+ }
+ } = {
+ channel: {
+ type: channel_type,
+ address: address_to_use
+ }
+ }
+ email_register_payload.channel.type = 'email'
+ // handle and format all optional date params
+ if (payload.channel_object.suppression_state) {
+ email_register_payload.channel.suppression_state = payload.channel_object.suppression_state
+ }
+ if (payload.channel_object.commercial_opted_in) {
+ email_register_payload.channel.commercial_opted_in = _parse_and_format_date(
+ payload.channel_object.commercial_opted_in
+ )
+ }
+ if (payload.channel_object.commercial_opted_out) {
+ email_register_payload.channel.commercial_opted_out = _parse_and_format_date(
+ payload.channel_object.commercial_opted_out
+ )
+ }
+ if (payload.channel_object.click_tracking_opted_in) {
+ email_register_payload.channel.commercial_opted_in = _parse_and_format_date(
+ payload.channel_object.click_tracking_opted_in
+ )
+ }
+ if (payload.channel_object.click_tracking_opted_out) {
+ email_register_payload.channel.click_tracking_opted_in = _parse_and_format_date(
+ payload.channel_object.click_tracking_opted_out
+ )
+ }
+ if (payload.channel_object.open_tracking_opted_in) {
+ email_register_payload.channel.open_tracking_opted_in = _parse_and_format_date(
+ payload.channel_object.open_tracking_opted_in
+ )
+ }
+ if (payload.channel_object.open_tracking_opted_out) {
+ email_register_payload.channel.open_tracking_opted_out = _parse_and_format_date(
+ payload.channel_object.open_tracking_opted_out
+ )
+ }
+ if (payload.channel_object.transactional_opted_in) {
+ email_register_payload.channel.transactional_opted_in = _parse_and_format_date(
+ payload.channel_object.transactional_opted_in
+ )
+ }
+ if (payload.channel_object.transactional_opted_out) {
+ email_register_payload.channel.transactional_opted_out = _parse_and_format_date(
+ payload.channel_object.transactional_opted_out
+ )
+ }
+ // additional properties
+ if (locale_language) {
+ email_register_payload.channel.locale_language = locale_language
+ }
+ if (locale_country) {
+ email_register_payload.channel.locale_country = locale_country
+ }
+ if (payload.timezone) {
+ email_register_payload.channel.timezone = payload.timezone
+ }
+ return do_request(request, register_uri, email_register_payload)
}
- if (Array.isArray(country_language) && country_language.length === 2) {
- payload.channel_object.locale_language = country_language[0]
- payload.channel_object.locale_country = country_language[1]
- }
- if (timezone) {
- payload.channel_object.timezone = payload.timezone
- }
- // handle and format all optional date params
- if (payload.channel_object.commercial_opted_in) {
- register_payload.channel.commercial_opted_in = _parse_and_format_date(payload.channel_object.commercial_opted_in)
- }
- if (payload.channel_object.commercial_opted_out) {
- register_payload.channel.commercial_opted_out = _parse_and_format_date(payload.channel_object.commercial_opted_out)
- }
- if (payload.channel_object.click_tracking_opted_in) {
- register_payload.channel.commercial_opted_in = _parse_and_format_date(
- payload.channel_object.click_tracking_opted_in
- )
- }
- if (payload.channel_object.click_tracking_opted_out) {
- register_payload.channel.click_tracking_opted_in = _parse_and_format_date(
- payload.channel_object.click_tracking_opted_out
- )
- }
- if (payload.channel_object.open_tracking_opted_in) {
- register_payload.channel.open_tracking_opted_in = _parse_and_format_date(
- payload.channel_object.open_tracking_opted_in
- )
- }
- if (payload.channel_object.open_tracking_opted_out) {
- register_payload.channel.open_tracking_opted_out = _parse_and_format_date(
- payload.channel_object.open_tracking_opted_out
- )
- }
- if (payload.channel_object.transactional_opted_in) {
- register_payload.channel.transactional_opted_in = _parse_and_format_date(
- payload.channel_object.transactional_opted_in
- )
- }
- if (payload.channel_object.transactional_opted_out) {
- register_payload.channel.transactional_opted_out = _parse_and_format_date(
- payload.channel_object.transactional_opted_out
- )
- }
- if (payload.channel_object.suppression_state) {
- register_payload.channel.suppression_state = payload.channel_object.suppression_state
- }
-
- return do_request(request, register_uri, register_payload)
}
// exported Action function
@@ -103,14 +156,15 @@ export function associate_named_user(
request: RequestClient,
settings: Settings,
channel_id: string,
- named_user_id: string
+ named_user_id: string,
+ channel_type: string
) {
const endpoint = map_endpoint(settings.endpoint)
const uri = `${endpoint}/api/named_users/associate`
const associate_payload = {
channel_id: channel_id,
- device_type: 'email',
+ device_type: `${channel_type}`,
named_user_id: named_user_id
}
@@ -230,8 +284,8 @@ function _build_attribute(attribute_key: string, attribute_value: any, occurred:
adjustedDate = _parse_date(attribute_value)
}
- if (['home_phone', 'work_phone', 'mobile_phone'].includes(attribute_key) && typeof(attribute_value) == "string") {
- attribute_value = parseInt(attribute_value.replace(/[^0-9]/g, ""))
+ if (['home_phone', 'work_phone', 'mobile_phone'].includes(attribute_key) && typeof attribute_value == 'string') {
+ attribute_value = parseInt(attribute_value.replace(/[^0-9]/g, ''))
}
const attribute: {
@@ -257,7 +311,6 @@ function _build_attribute(attribute_key: string, attribute_value: any, occurred:
return attribute
}
-
function _build_tags_object(payload: TagsPayload): object {
/*
This function takes a Group event and builds a Tag payload. It assumes values are booleans
@@ -274,6 +327,12 @@ function _build_tags_object(payload: TagsPayload): object {
} else {
tags_to_remove.push(k)
}
+ } else if (typeof v == 'string' && ['true', 'false'].includes(v.toString().toLowerCase())) {
+ if (v.toLowerCase() === 'true') {
+ tags_to_add.push(k)
+ } else {
+ tags_to_remove.push(k)
+ }
}
}
const airship_payload: { audience: {}; add?: {}; remove?: {} } = { audience: {} }
diff --git a/packages/destination-actions/src/destinations/algolia-insights/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/algolia-insights/__tests__/__snapshots__/snapshot.test.ts.snap
index 256e571a12..ca624207d3 100644
--- a/packages/destination-actions/src/destinations/algolia-insights/__tests__/__snapshots__/snapshot.test.ts.snap
+++ b/packages/destination-actions/src/destinations/algolia-insights/__tests__/__snapshots__/snapshot.test.ts.snap
@@ -4,9 +4,19 @@ exports[`Testing snapshot for actions-algolia-insights destination: conversionEv
Object {
"events": Array [
Object {
- "eventName": "Conversion Event",
- "eventType": "conversion",
+ "currency": "HTG",
+ "eventName": "U[ABpE$k",
+ "eventSubtype": "purchase",
+ "eventType": "view",
"index": "U[ABpE$k",
+ "objectData": Array [
+ Object {
+ "discount": -52282070788997.12,
+ "price": -52282070788997.12,
+ "quantity": -52282070788997.12,
+ "queryID": "U[ABpE$k",
+ },
+ ],
"objectIDs": Array [
"U[ABpE$k",
],
@@ -14,6 +24,7 @@ Object {
"testType": "U[ABpE$k",
"timestamp": null,
"userToken": "U[ABpE$k",
+ "value": -52282070788997.12,
},
],
}
@@ -24,6 +35,7 @@ Object {
"events": Array [
Object {
"eventName": "Conversion Event",
+ "eventSubtype": "purchase",
"eventType": "conversion",
"index": "U[ABpE$k",
"objectIDs": Array [
@@ -39,8 +51,8 @@ exports[`Testing snapshot for actions-algolia-insights destination: productAdded
Object {
"events": Array [
Object {
- "eventName": "Add to cart",
- "eventType": "conversion",
+ "eventName": "g)$f*TeM",
+ "eventType": "view",
"index": "g)$f*TeM",
"objectIDs": Array [
"g)$f*TeM",
@@ -74,8 +86,8 @@ exports[`Testing snapshot for actions-algolia-insights destination: productClick
Object {
"events": Array [
Object {
- "eventName": "Product Clicked",
- "eventType": "click",
+ "eventName": "LLjxSD^^GnH",
+ "eventType": "conversion",
"index": "LLjxSD^^GnH",
"objectIDs": Array [
"LLjxSD^^GnH",
@@ -112,8 +124,8 @@ exports[`Testing snapshot for actions-algolia-insights destination: productListF
Object {
"events": Array [
Object {
- "eventName": "Product List Filtered",
- "eventType": "click",
+ "eventName": "6O0djra",
+ "eventType": "view",
"filters": Array [
"6O0djra:6O0djra",
],
@@ -147,7 +159,7 @@ exports[`Testing snapshot for actions-algolia-insights destination: productViewe
Object {
"events": Array [
Object {
- "eventName": "Product Viewed",
+ "eventName": "BLFCPcmz",
"eventType": "view",
"index": "BLFCPcmz",
"objectIDs": Array [
diff --git a/packages/destination-actions/src/destinations/algolia-insights/algolia-insight-api.ts b/packages/destination-actions/src/destinations/algolia-insights/algolia-insight-api.ts
index 7bd0ada893..b910f83a49 100644
--- a/packages/destination-actions/src/destinations/algolia-insights/algolia-insight-api.ts
+++ b/packages/destination-actions/src/destinations/algolia-insights/algolia-insight-api.ts
@@ -4,33 +4,43 @@ export const AlgoliaBehaviourURL = BaseAlgoliaInsightsURL + '/1/events'
export const algoliaApiPermissionsUrl = (settings: Settings) =>
`https://${settings.appId}.algolia.net/1/keys/${settings.apiKey}`
+export type AlgoliaEventType = 'view' | 'click' | 'conversion'
+
+export type AlgoliaEventSubtype = 'addToCart' | 'purchase'
+
type EventCommon = {
eventName: string
index: string
userToken: string
timestamp?: number
queryID?: string
+ eventType: AlgoliaEventType
}
export type AlgoliaProductViewedEvent = EventCommon & {
- eventType: 'view'
objectIDs: string[]
}
export type AlgoliaProductClickedEvent = EventCommon & {
- eventType: 'click'
positions?: number[]
objectIDs: string[]
}
export type AlgoliaFilterClickedEvent = EventCommon & {
- eventType: 'click'
filters: string[]
}
export type AlgoliaConversionEvent = EventCommon & {
- eventType: 'conversion'
+ eventSubtype?: AlgoliaEventSubtype
objectIDs: string[]
+ objectData?: {
+ queryID?: string
+ price?: number | string
+ discount?: number | string
+ quantity?: number
+ }[]
+ value?: number
+ currency?: string
}
export type AlgoliaApiPermissions = {
diff --git a/packages/destination-actions/src/destinations/algolia-insights/conversionEvents/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/algolia-insights/conversionEvents/__tests__/__snapshots__/snapshot.test.ts.snap
index c2e852456e..4107c80fc1 100644
--- a/packages/destination-actions/src/destinations/algolia-insights/conversionEvents/__tests__/__snapshots__/snapshot.test.ts.snap
+++ b/packages/destination-actions/src/destinations/algolia-insights/conversionEvents/__tests__/__snapshots__/snapshot.test.ts.snap
@@ -4,9 +4,19 @@ exports[`Testing snapshot for AlgoliaInsights's conversionEvents destination act
Object {
"events": Array [
Object {
- "eventName": "Conversion Event",
- "eventType": "conversion",
+ "currency": "CUC",
+ "eventName": ")j)vR5%1AP*epuo8A%R",
+ "eventSubtype": "addToCart",
+ "eventType": "click",
"index": ")j)vR5%1AP*epuo8A%R",
+ "objectData": Array [
+ Object {
+ "discount": 76163635352698.88,
+ "price": 76163635352698.88,
+ "quantity": 76163635352698.88,
+ "queryID": ")j)vR5%1AP*epuo8A%R",
+ },
+ ],
"objectIDs": Array [
")j)vR5%1AP*epuo8A%R",
],
@@ -14,6 +24,7 @@ Object {
"testType": ")j)vR5%1AP*epuo8A%R",
"timestamp": null,
"userToken": ")j)vR5%1AP*epuo8A%R",
+ "value": 76163635352698.88,
},
],
}
@@ -24,6 +35,7 @@ Object {
"events": Array [
Object {
"eventName": "Conversion Event",
+ "eventSubtype": "purchase",
"eventType": "conversion",
"index": ")j)vR5%1AP*epuo8A%R",
"objectIDs": Array [
diff --git a/packages/destination-actions/src/destinations/algolia-insights/conversionEvents/__tests__/index.test.ts b/packages/destination-actions/src/destinations/algolia-insights/conversionEvents/__tests__/index.test.ts
index ca0ad248bf..a8727e353d 100644
--- a/packages/destination-actions/src/destinations/algolia-insights/conversionEvents/__tests__/index.test.ts
+++ b/packages/destination-actions/src/destinations/algolia-insights/conversionEvents/__tests__/index.test.ts
@@ -53,6 +53,7 @@ describe('AlgoliaInsights.conversionEvents', () => {
expect(algoliaEvent.eventName).toBe('Conversion Event')
expect(algoliaEvent.eventType).toBe('conversion')
+ expect(algoliaEvent.eventSubtype).toBe('purchase')
expect(algoliaEvent.index).toBe(event.properties?.search_index)
expect(algoliaEvent.userToken).toBe(event.userId)
expect(algoliaEvent.objectIDs).toContain('9876')
@@ -103,4 +104,177 @@ describe('AlgoliaInsights.conversionEvents', () => {
const algoliaEvent = await testAlgoliaDestination(event)
expect(algoliaEvent.queryID).toBe(event.properties?.query_id)
})
+
+ it('should pass value if present', async () => {
+ const event = createTestEvent({
+ type: 'track',
+ event: 'Order Completed',
+ properties: {
+ query_id: '1234',
+ search_index: 'fashion_1',
+ products: [
+ {
+ product_id: '9876',
+ product_name: 'skirt 1'
+ },
+ {
+ product_id: '5432',
+ product_name: 'skirt 2'
+ }
+ ],
+ value: 200
+ }
+ })
+ const algoliaEvent = await testAlgoliaDestination(event)
+ expect(algoliaEvent.value).toBe(200)
+ })
+
+ it('should pass currency if present', async () => {
+ const event = createTestEvent({
+ type: 'track',
+ event: 'Order Completed',
+ properties: {
+ query_id: '1234',
+ search_index: 'fashion_1',
+ products: [
+ {
+ product_id: '9876',
+ product_name: 'skirt 1'
+ },
+ {
+ product_id: '5432',
+ product_name: 'skirt 2'
+ }
+ ],
+ currency: 'AUD'
+ }
+ })
+ const algoliaEvent = await testAlgoliaDestination(event)
+ expect(algoliaEvent.currency).toBe('AUD')
+ })
+
+ describe('should pass product price data if present', () => {
+ it('all products contain all price properties', async () => {
+ const event = createTestEvent({
+ type: 'track',
+ event: 'Order Completed',
+ properties: {
+ query_id: '1234',
+ search_index: 'fashion_1',
+ products: [
+ {
+ product_id: '9876',
+ product_name: 'skirt 1',
+ price: 105.99,
+ discount: 22.99,
+ quantity: 5
+ },
+ {
+ product_id: '5432',
+ product_name: 'skirt 2',
+ price: 0.6,
+ discount: 0.1,
+ quantity: 2
+ }
+ ]
+ }
+ })
+ const algoliaEvent = await testAlgoliaDestination(event)
+ expect(algoliaEvent.objectIDs).toEqual(['9876', '5432'])
+ expect(algoliaEvent.objectData).toEqual([
+ { price: 105.99, discount: 22.99, quantity: 5 },
+ { price: 0.6, discount: 0.1, quantity: 2 }
+ ])
+ })
+
+ it('some products contain some price properties', async () => {
+ const event = createTestEvent({
+ type: 'track',
+ event: 'Order Completed',
+ properties: {
+ query_id: '1234',
+ search_index: 'fashion_1',
+ products: [
+ {
+ product_id: '9876',
+ product_name: 'skirt 1',
+ price: 105.99,
+ quantity: 5
+ },
+ {
+ product_id: '9212',
+ product_name: 'dress 1',
+ price: 299.99,
+ discount: 12.99
+ }
+ ]
+ }
+ })
+ const algoliaEvent = await testAlgoliaDestination(event)
+ expect(algoliaEvent.objectIDs).toEqual(['9876', '9212'])
+ expect(algoliaEvent.objectData).toEqual([
+ { price: 105.99, quantity: 5 },
+ { price: 299.99, discount: 12.99 }
+ ])
+ })
+
+ it('some products contain no price properties', async () => {
+ const event = createTestEvent({
+ type: 'track',
+ event: 'Order Completed',
+ properties: {
+ query_id: '1234',
+ search_index: 'fashion_1',
+ products: [
+ {
+ product_id: '9876',
+ product_name: 'skirt 1',
+ price: 105.99,
+ discount: 22.99,
+ quantity: 5
+ },
+ {
+ product_id: '5432',
+ product_name: 'skirt 2'
+ },
+ {
+ product_id: '9212',
+ product_name: 'dress 1',
+ price: 299.99,
+ discount: 12.99
+ }
+ ]
+ }
+ })
+ const algoliaEvent = await testAlgoliaDestination(event)
+ expect(algoliaEvent.objectIDs).toEqual(['9876', '5432', '9212'])
+ expect(algoliaEvent.objectData).toEqual([
+ { price: 105.99, discount: 22.99, quantity: 5 },
+ {},
+ { price: 299.99, discount: 12.99 }
+ ])
+ })
+
+ it('no products contain price properties', async () => {
+ const event = createTestEvent({
+ type: 'track',
+ event: 'Order Completed',
+ properties: {
+ query_id: '1234',
+ search_index: 'fashion_1',
+ products: [
+ {
+ product_id: '9876'
+ },
+ {
+ product_id: '5432'
+ }
+ ]
+ }
+ })
+ const algoliaEvent = await testAlgoliaDestination(event)
+ expect(algoliaEvent.objectIDs).toEqual(['9876', '5432'])
+ expect(algoliaEvent.objectData).toBeUndefined()
+ })
+ })
})
diff --git a/packages/destination-actions/src/destinations/algolia-insights/conversionEvents/generated-types.ts b/packages/destination-actions/src/destinations/algolia-insights/conversionEvents/generated-types.ts
index 53bec96a71..3bc33277a3 100644
--- a/packages/destination-actions/src/destinations/algolia-insights/conversionEvents/generated-types.ts
+++ b/packages/destination-actions/src/destinations/algolia-insights/conversionEvents/generated-types.ts
@@ -2,10 +2,18 @@
export interface Payload {
/**
- * Populates the ObjectIds field in the Algolia Insights API. An array of objects representing the purchased items. Each object must contains a product_id field.
+ * Sub-type of the event, "purchase" or "addToCart".
+ */
+ eventSubtype?: string
+ /**
+ * Populates the ObjectIDs field in the Algolia Insights API. An array of objects representing the purchased items. Each object must contain a product_id field.
*/
products: {
product_id: string
+ price?: number
+ quantity?: number
+ discount?: number
+ queryID?: string
}[]
/**
* Name of the targeted search index.
@@ -23,10 +31,26 @@ export interface Payload {
* The timestamp of the event.
*/
timestamp?: string
+ /**
+ * The value of the cart that is being converted.
+ */
+ value?: number
+ /**
+ * Currency of the objects associated with the event in 3-letter ISO 4217 format. Required when `value` or `price` is set.
+ */
+ currency?: string
/**
* Additional fields for this event. This field may be useful for Algolia Insights fields which are not mapped in Segment.
*/
extraProperties?: {
[k: string]: unknown
}
+ /**
+ * The name of the event to send to Algolia. Defaults to 'Conversion Event'
+ */
+ eventName?: string
+ /**
+ * The type of event to send to Algolia. Defaults to 'conversion'
+ */
+ eventType?: string
}
diff --git a/packages/destination-actions/src/destinations/algolia-insights/conversionEvents/index.ts b/packages/destination-actions/src/destinations/algolia-insights/conversionEvents/index.ts
index 1853b38ef4..01f649138d 100644
--- a/packages/destination-actions/src/destinations/algolia-insights/conversionEvents/index.ts
+++ b/packages/destination-actions/src/destinations/algolia-insights/conversionEvents/index.ts
@@ -1,24 +1,68 @@
import type { ActionDefinition, Preset } from '@segment/actions-core'
import { defaultValues } from '@segment/actions-core'
-import { AlgoliaBehaviourURL, AlgoliaConversionEvent } from '../algolia-insight-api'
+import {
+ AlgoliaBehaviourURL,
+ AlgoliaConversionEvent,
+ AlgoliaEventSubtype,
+ AlgoliaEventType
+} from '../algolia-insight-api'
import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'
+const notUndef = (thing: unknown) => typeof thing !== 'undefined'
+
export const conversionEvents: ActionDefinition = {
title: 'Conversion Events',
description:
'In ecommerce, conversions are purchase events often but not always involving multiple products. Outside of a conversion can be any positive signal associated with an index record. Query ID is optional and indicates that the view events is the result of a search query.',
fields: {
+ eventSubtype: {
+ label: 'Event Subtype',
+ description: 'Sub-type of the event, "purchase" or "addToCart".',
+ type: 'string',
+ required: false,
+ choices: [
+ { value: 'purchase', label: 'Purchase' },
+ { value: 'addToCart', label: 'Add To Cart' }
+ ],
+ default: 'purchase'
+ },
products: {
label: 'Product Details',
description:
- 'Populates the ObjectIds field in the Algolia Insights API. An array of objects representing the purchased items. Each object must contains a product_id field.',
+ 'Populates the ObjectIDs field in the Algolia Insights API. An array of objects representing the purchased items. Each object must contain a product_id field.',
type: 'object',
multiple: true,
- properties: { product_id: { label: 'product_id', type: 'string', required: true } },
+ defaultObjectUI: 'keyvalue',
+ properties: {
+ product_id: { label: 'product_id', type: 'string', required: true },
+ price: { label: 'price', type: 'number', required: false },
+ quantity: { label: 'quantity', type: 'number', required: false },
+ discount: { label: 'discount', type: 'number', required: false },
+ queryID: { label: 'queryID', type: 'string', required: false }
+ },
required: true,
default: {
- '@path': '$.properties.products'
+ '@arrayPath': [
+ '$.properties.products',
+ {
+ product_id: {
+ '@path': '$.product_id'
+ },
+ price: {
+ '@path': '$.price'
+ },
+ quantity: {
+ '@path': '$.quantity'
+ },
+ discount: {
+ '@path': '$.discount'
+ },
+ queryID: {
+ '@path': '$.queryID'
+ }
+ }
+ ]
}
},
index: {
@@ -36,14 +80,18 @@ export const conversionEvents: ActionDefinition = {
type: 'string',
required: false,
default: {
- '@path': '$.properties.query_id'
+ '@if': {
+ exists: { '@path': '$.properties.query_id' },
+ then: { '@path': '$.properties.query_id' },
+ else: { '@path': '$.integrations.Algolia Insights (Actions).query_id' }
+ }
}
},
userToken: {
type: 'string',
required: true,
description: 'The ID associated with the user.',
- label: 'userToken',
+ label: 'User Token',
default: {
'@if': {
exists: { '@path': '$.userId' },
@@ -56,11 +104,26 @@ export const conversionEvents: ActionDefinition = {
type: 'string',
required: false,
description: 'The timestamp of the event.',
- label: 'timestamp',
+ label: 'Timestamp',
default: { '@path': '$.timestamp' }
},
+ value: {
+ type: 'number',
+ required: false,
+ description: 'The value of the cart that is being converted.',
+ label: 'Value',
+ default: { '@path': '$.properties.value' }
+ },
+ currency: {
+ type: 'string',
+ required: false,
+ description:
+ 'Currency of the objects associated with the event in 3-letter ISO 4217 format. Required when `value` or `price` is set.',
+ label: 'Currency',
+ default: { '@path': '$.properties.currency' }
+ },
extraProperties: {
- label: 'extraProperties',
+ label: 'Extra Properties',
required: false,
description:
'Additional fields for this event. This field may be useful for Algolia Insights fields which are not mapped in Segment.',
@@ -68,17 +131,50 @@ export const conversionEvents: ActionDefinition = {
default: {
'@path': '$.properties'
}
+ },
+ eventName: {
+ label: 'Event Name',
+ description: "The name of the event to send to Algolia. Defaults to 'Conversion Event'",
+ type: 'string',
+ required: false,
+ default: 'Conversion Event'
+ },
+ eventType: {
+ label: 'Event Type',
+ description: "The type of event to send to Algolia. Defaults to 'conversion'",
+ type: 'string',
+ required: false,
+ default: 'conversion',
+ choices: [
+ { label: 'View', value: 'view' },
+ { label: 'Conversion', value: 'conversion' },
+ { label: 'Click', value: 'click' }
+ ]
}
},
defaultSubscription: 'type = "track" and event = "Order Completed"',
perform: (request, data) => {
+ const objectData = data.payload.products.some(({ queryID, price, discount, quantity }) => {
+ return notUndef(queryID) || notUndef(price) || notUndef(discount) || notUndef(quantity)
+ })
+ ? data.payload.products.map(({ queryID, price, discount, quantity }) => ({
+ queryID,
+ price,
+ discount,
+ quantity
+ }))
+ : undefined
const insightEvent: AlgoliaConversionEvent = {
...data.payload.extraProperties,
- eventName: 'Conversion Event',
- eventType: 'conversion',
+ eventName: data.payload.eventName ?? 'Conversion Event',
+ eventType: (data.payload.eventType as AlgoliaEventType) ?? ('conversion' as AlgoliaEventType),
+ eventSubtype: (data.payload.eventSubtype as AlgoliaEventSubtype) ?? 'purchase',
index: data.payload.index,
queryID: data.payload.queryID,
objectIDs: data.payload.products.map((product) => product.product_id),
+ objectData,
+ value: data.payload.value,
+ currency: data.payload.currency,
userToken: data.payload.userToken,
timestamp: data.payload.timestamp ? new Date(data.payload.timestamp).valueOf() : undefined
}
diff --git a/packages/destination-actions/src/destinations/algolia-insights/index.ts b/packages/destination-actions/src/destinations/algolia-insights/index.ts
index 9cd81bc4a6..80d19c0595 100644
--- a/packages/destination-actions/src/destinations/algolia-insights/index.ts
+++ b/packages/destination-actions/src/destinations/algolia-insights/index.ts
@@ -55,8 +55,14 @@ const destination: DestinationDefinition = {
}
}
},
- // TODO: figure out how to pass multiple presets
presets: [
+ {
+ name: 'Algolia Plugin',
+ subscribe: 'type = "track" or type = "identify" or type = "group" or type = "page" or type = "alias"',
+ partnerAction: 'algoliaPlugin',
+ mapping: {},
+ type: 'automatic'
+ },
productClickPresets,
conversionPresets,
productViewedPresets,
diff --git a/packages/destination-actions/src/destinations/algolia-insights/productAddedEvents/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/algolia-insights/productAddedEvents/__tests__/__snapshots__/snapshot.test.ts.snap
index 1df5766a59..8718475486 100644
--- a/packages/destination-actions/src/destinations/algolia-insights/productAddedEvents/__tests__/__snapshots__/snapshot.test.ts.snap
+++ b/packages/destination-actions/src/destinations/algolia-insights/productAddedEvents/__tests__/__snapshots__/snapshot.test.ts.snap
@@ -4,8 +4,8 @@ exports[`Testing snapshot for AlgoliaInsights's productAddedEvents destination a
Object {
"events": Array [
Object {
- "eventName": "Add to cart",
- "eventType": "conversion",
+ "eventName": "D9W&9sjJ$g9LNBPqU",
+ "eventType": "click",
"index": "D9W&9sjJ$g9LNBPqU",
"objectIDs": Array [
"D9W&9sjJ$g9LNBPqU",
diff --git a/packages/destination-actions/src/destinations/algolia-insights/productAddedEvents/generated-types.ts b/packages/destination-actions/src/destinations/algolia-insights/productAddedEvents/generated-types.ts
index 93a008a3e0..3840498447 100644
--- a/packages/destination-actions/src/destinations/algolia-insights/productAddedEvents/generated-types.ts
+++ b/packages/destination-actions/src/destinations/algolia-insights/productAddedEvents/generated-types.ts
@@ -27,4 +27,12 @@ export interface Payload {
extraProperties?: {
[k: string]: unknown
}
+ /**
+ * The name of the event to be send to Algolia. Defaults to 'Add to cart'
+ */
+ eventName?: string
+ /**
+ * The type of event to send to Algolia. Defaults to 'conversion'
+ */
+ eventType?: string
}
diff --git a/packages/destination-actions/src/destinations/algolia-insights/productAddedEvents/index.ts b/packages/destination-actions/src/destinations/algolia-insights/productAddedEvents/index.ts
index c338b6604e..1808889278 100644
--- a/packages/destination-actions/src/destinations/algolia-insights/productAddedEvents/index.ts
+++ b/packages/destination-actions/src/destinations/algolia-insights/productAddedEvents/index.ts
@@ -2,7 +2,7 @@ import type { ActionDefinition, Preset } from '@segment/actions-core'
import { defaultValues } from '@segment/actions-core'
import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'
-import { AlgoliaBehaviourURL, AlgoliaConversionEvent } from '../algolia-insight-api'
+import { AlgoliaBehaviourURL, AlgoliaConversionEvent, AlgoliaEventType } from '../algolia-insight-api'
export const productAddedEvents: ActionDefinition = {
title: 'Product Added Events',
@@ -34,14 +34,18 @@ export const productAddedEvents: ActionDefinition = {
type: 'string',
required: false,
default: {
- '@path': '$.properties.query_id'
+ '@if': {
+ exists: { '@path': '$.properties.query_id' },
+ then: { '@path': '$.properties.query_id' },
+ else: { '@path': '$.integrations.Algolia Insights (Actions).query_id' }
+ }
}
},
userToken: {
type: 'string',
required: true,
description: 'The ID associated with the user.',
- label: 'userToken',
+ label: 'User Token',
default: {
'@if': {
exists: { '@path': '$.userId' },
@@ -54,11 +58,11 @@ export const productAddedEvents: ActionDefinition = {
type: 'string',
required: false,
description: 'The timestamp of the event.',
- label: 'timestamp',
+ label: 'Timestamp',
default: { '@path': '$.timestamp' }
},
extraProperties: {
- label: 'extraProperties',
+ label: 'Extra Properties',
required: false,
description:
'Additional fields for this event. This field may be useful for Algolia Insights fields which are not mapped in Segment.',
@@ -66,14 +70,33 @@ export const productAddedEvents: ActionDefinition = {
default: {
'@path': '$.properties'
}
+ },
+ eventName: {
+ label: 'Event Name',
+ description: "The name of the event to be send to Algolia. Defaults to 'Add to cart'",
+ type: 'string',
+ required: false,
+ default: 'Add to cart'
+ },
+ eventType: {
+ label: 'Event Type',
+ description: "The type of event to send to Algolia. Defaults to 'conversion'",
+ type: 'string',
+ required: false,
+ default: 'conversion',
+ choices: [
+ { label: 'view', value: 'view' },
+ { label: 'conversion', value: 'conversion' },
+ { label: 'click', value: 'click' }
+ ]
}
},
defaultSubscription: 'type = "track" and event = "Product Added"',
perform: (request, data) => {
const insightEvent: AlgoliaConversionEvent = {
...data.payload.extraProperties,
- eventName: 'Add to cart',
- eventType: 'conversion',
+ eventName: data.payload.eventName ?? 'Add to cart',
+ eventType: (data.payload.eventType as AlgoliaEventType) ?? ('conversion' as AlgoliaEventType),
index: data.payload.index,
queryID: data.payload.queryID,
objectIDs: [data.payload.product],
diff --git a/packages/destination-actions/src/destinations/algolia-insights/productClickedEvents/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/algolia-insights/productClickedEvents/__tests__/__snapshots__/snapshot.test.ts.snap
index 6b3056dd63..171113b996 100644
--- a/packages/destination-actions/src/destinations/algolia-insights/productClickedEvents/__tests__/__snapshots__/snapshot.test.ts.snap
+++ b/packages/destination-actions/src/destinations/algolia-insights/productClickedEvents/__tests__/__snapshots__/snapshot.test.ts.snap
@@ -4,8 +4,8 @@ exports[`Testing snapshot for AlgoliaInsights's productClickedEvents destination
Object {
"events": Array [
Object {
- "eventName": "Product Clicked",
- "eventType": "click",
+ "eventName": "tTO6#",
+ "eventType": "view",
"index": "tTO6#",
"objectIDs": Array [
"tTO6#",
diff --git a/packages/destination-actions/src/destinations/algolia-insights/productClickedEvents/generated-types.ts b/packages/destination-actions/src/destinations/algolia-insights/productClickedEvents/generated-types.ts
index ffeb5d4420..d932d1edde 100644
--- a/packages/destination-actions/src/destinations/algolia-insights/productClickedEvents/generated-types.ts
+++ b/packages/destination-actions/src/destinations/algolia-insights/productClickedEvents/generated-types.ts
@@ -31,4 +31,12 @@ export interface Payload {
extraProperties?: {
[k: string]: unknown
}
+ /**
+ * The name of the event to be send to Algolia. Defaults to 'Product Clicked'
+ */
+ eventName?: string
+ /**
+ * The type of event to send to Algolia. Defaults to 'click'
+ */
+ eventType?: string
}
diff --git a/packages/destination-actions/src/destinations/algolia-insights/productClickedEvents/index.ts b/packages/destination-actions/src/destinations/algolia-insights/productClickedEvents/index.ts
index 062f0dac94..13229ba8b4 100644
--- a/packages/destination-actions/src/destinations/algolia-insights/productClickedEvents/index.ts
+++ b/packages/destination-actions/src/destinations/algolia-insights/productClickedEvents/index.ts
@@ -1,6 +1,6 @@
import type { ActionDefinition, Preset } from '@segment/actions-core'
import { defaultValues } from '@segment/actions-core'
-import { AlgoliaBehaviourURL, AlgoliaProductClickedEvent } from '../algolia-insight-api'
+import { AlgoliaBehaviourURL, AlgoliaProductClickedEvent, AlgoliaEventType } from '../algolia-insight-api'
import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'
@@ -32,7 +32,11 @@ export const productClickedEvents: ActionDefinition = {
type: 'string',
required: false,
default: {
- '@path': '$.properties.query_id'
+ '@if': {
+ exists: { '@path': '$.properties.query_id' },
+ then: { '@path': '$.properties.query_id' },
+ else: { '@path': '$.integrations.Algolia Insights (Actions).query_id' }
+ }
}
},
position: {
@@ -48,7 +52,7 @@ export const productClickedEvents: ActionDefinition