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 1bb23da289..47dc9747c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,13 +17,13 @@ jobs: steps: # See nx recipe: https://nx.dev/recipes/ci/monorepo-ci-github-actions - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: persist-credentials: false fetch-depth: 0 # nx recipe - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} registry-url: 'https://registry.npmjs.org' @@ -54,13 +54,13 @@ jobs: node-version: [18.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: persist-credentials: false fetch-depth: 0 # nx recipe - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} registry-url: 'https://registry.npmjs.org' @@ -93,13 +93,13 @@ jobs: node-version: [18.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: persist-credentials: false fetch-depth: 0 # nx recipe - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} registry-url: 'https://registry.npmjs.org' @@ -145,12 +145,12 @@ jobs: node-version: [18.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: persist-credentials: false - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} registry-url: 'https://registry.npmjs.org' @@ -167,6 +167,9 @@ jobs: - name: Build run: NODE_ENV=production yarn build:browser-bundles + - name: Size Limit + run: yarn browser size + # - name: Run Saucelabs Tests # working-directory: packages/browser-destinations-integration-tests # shell: bash diff --git a/.github/workflows/ext.yml b/.github/workflows/ext.yml index db7cf5dfb7..df88550203 100644 --- a/.github/workflows/ext.yml +++ b/.github/workflows/ext.yml @@ -17,8 +17,8 @@ jobs: node-version: [18.x] steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: yarn diff --git a/.github/workflows/git-leak.yml b/.github/workflows/git-leak.yml deleted file mode 100644 index 3b6abe3dc9..0000000000 --- a/.github/workflows/git-leak.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Gitleaks-Action -on: [push] -jobs: - build: - runs-on: self-runner-node - steps: - - name: Trigger to Gitleak - run: | - python -c ' - import json,sys,requests; - github = {"repository": "'${{ github.event.repository.name }}'", "ref": "'${{ github.ref_name }}'"}; - github_request = {"insider_gitleak": github}; - requests.post("'$LambdaWebHook'", json=github_request);' - env: - LambdaWebHook: ${{ secrets.CHECKMARX_LAMBDA_WEBHOOK }} \ No newline at end of file diff --git a/.github/workflows/label-prs.yml b/.github/workflows/label-prs.yml new file mode 100644 index 0000000000..f004e867cd --- /dev/null +++ b/.github/workflows/label-prs.yml @@ -0,0 +1,62 @@ +# 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/github-action/compute-labels.js') + await script({github, context, core}) + # Separating apply labels to separate step to avoid using PAT token auth. + - 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, DRY_RUN } = process.env + if(Boolean(DRY_RUN)){ + core.info(`Would have added labels: ${labelsToAdd}`) + core.info(`Would have removed labels: ${labelsToRemove}`) + return + } + 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..52fe4be475 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -7,6 +7,7 @@ on: push: branches: - main + - release jobs: build-and-publish: @@ -22,12 +23,12 @@ jobs: node-version: [18.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: token: ${{ secrets.GH_PAT }} - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} registry-url: 'https://registry.npmjs.org' @@ -55,4 +56,29 @@ jobs: - name: Publish run: | - yarn lerna publish from-git --yes --allowBranch=main --loglevel=verbose --dist-tag latest + yarn lerna publish from-package --yes --allowBranch=main --loglevel=verbose --dist-tag latest + + release: + needs: build-and-publish # comment when testing locally with https://github.com/nektos/act + + runs-on: ubuntu-20.04 + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Shallow clones should be disabled to ensure the commit history for the repository is available to the action + fetch-tags: true + + - name: Generate Release Tag + id: get-release-tag + run: ./scripts/generate-release-tags.sh + + - name: Create Github Release + id: create-github-release + uses: actions/github-script@v7 + env: + RELEASE_TAG: ${{ steps.get-release-tag.outputs.release-tag }} + with: + script: | + const script = require('./scripts/github-action/create-github-release.js') + await script({github, context, core, exec}) diff --git a/.github/workflows/version-packages.yml b/.github/workflows/version-packages.yml new file mode 100644 index 0000000000..6d8399818f --- /dev/null +++ b/.github/workflows/version-packages.yml @@ -0,0 +1,72 @@ +# This workflow is triggered manually via the GitHub Actions page or API +name: Version Packages +on: + workflow_dispatch: + inputs: + branch: + description: 'Branch to create PR from' + required: true + default: 'release-actions' + +jobs: + build-and-version-packages: + env: + HUSKY: 0 + runs-on: ubuntu-latest + permissions: + pull-requests: write + contents: write + steps: + - uses: actions/checkout@v4 + with: + ref: main + depth: 0 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + registry-url: 'https://registry.npmjs.org' + cache: yarn + + - name: Checkout branch + run: | + git checkout -b ${{ github.event.inputs.branch }} + git push origin ${{ github.event.inputs.branch }} + + - name: Install Dependencies + run: yarn install --frozen-lockfile --ignore-optional + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Build + run: NODE_ENV=production yarn build + + - name: Version Packages + run: yarn lerna version minor --allow-branch ${{ github.event.inputs.branch }} --no-git-tag-version --no-commit-hooks --no-private --yes + + - name: Commit and push + id: commit_and_push + run: | + count=$(git diff --cached --stat) + if [ -z "$count" ]; then + echo "No changes to commit" + echo "SKIP_PR=true" >> $GITHUB_OUTPUT + exit 0 + fi + git add . + git commit -m "chore: version packages" + git push origin ${{ github.event.inputs.branch }} + + - name: Create PR + if: ${{ steps.commit_and_push.outputs.SKIP_PR != 'true' }} + run: | + packages_published=$(git status -s -uno| grep "package.json" |awk '{print $2}'| xargs jq -r '.name + "@" + .version' --argjson null {}) + pr_message="This PR was opened by GithHub Actions. Whenever you're ready to publish the packages, merge this PR." + description="$(printf "%s\n # Packages\n%s" "$pr_message" "$packages_published")" + gh pr create --base main --head ${{ github.event.inputs.branch }} --title "Publish" --body "$description" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ddce3cbbd4..170f25e318 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 658269d2be..3ac9e3e61c 100644 --- a/README.md +++ b/README.md @@ -324,6 +324,31 @@ const destination = { description: "The person's email address", type: 'string', default: { '@path': '$.properties.email_address' } + }, + // an object field example. Defaults should be specified on the top level. + value: { + label: 'Conversion Value', + description: 'The monetary value for a conversion. This is an object with shape: {"currencyCode": USD", "amount": "100"}' + type: 'object' + default: { + currencyCode: { '@path': '$.properties.currency' }, + amount: { '@path': '$.properties.revenue' } + }, + properties: { + currencyCode: { + label: 'Currency Code', + type: 'string', + required: true, + description: 'ISO format' + }, + amount: { + label: 'Amount', + type: 'string', + required: true, + description: 'Value of the conversion in decimal string. Can be dynamically set up or have a fixed value.' + } + } + } } } } @@ -390,6 +415,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. @@ -521,10 +614,13 @@ Additionally, you’ll need to coordinate with Segment’s R&D team for the time ## 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. +At the moment two hooks are available: `onMappingSave` and `retlOnMappingSave`: + +- `onMappingSave`: This hook appears in the MappingEditor page as a separate step. Users fill in the defined input fields and the code in the `performHook` block is triggered when the user saves their mapping. +- `retlOnMappingSave`: This hook appears only for destinations connected to a RETL warehouse source. It is otherwise the same as the `onMappingSave` hook. + **Inputs** Builders may define a set of `inputFields` that are used when performing the request to the destination. @@ -535,7 +631,7 @@ Similar to the `perform` method, the `performHook` method allows builders to tri **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. +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. Outputs can be referenced in the `perform` block with `data.hookOutputs?.?.` ### Example (LinkedIn Conversions API) @@ -613,10 +709,6 @@ const action: ActionDefinition = { } ``` -### `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: @@ -752,7 +844,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/SECURITY.MD b/SECURITY.MD deleted file mode 100644 index e01899c9e2..0000000000 --- a/SECURITY.MD +++ /dev/null @@ -1,7 +0,0 @@ -# Security Policy - -## Reporting a Vulnerability - -To securely report a vulnerability, please [Contact us!](mailto:security@useinsider.com?subject=[GitHub]_Vulnerability!). - - diff --git a/docs/testing.md b/docs/testing.md index 26c8a64d3d..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. diff --git a/lerna.json b/lerna.json index 8c3c8d6092..959ff925a2 100644 --- a/lerna.json +++ b/lerna.json @@ -8,7 +8,7 @@ "npmClientArgs": ["--ignore-engines", "--ignore-optional"] }, "version": { - "allowBranch": "main" + "allowBranch": ["main", "release"] } } } diff --git a/package.json b/package.json index 042bfd9c5b..8c153de99b 100644 --- a/package.json +++ b/package.json @@ -12,26 +12,29 @@ "node": "14 || ^18.12" }, "scripts": { + "alpha": "lerna version prerelease --allow-branch $(git branch --show-current) --preid $(git branch --show-current) --no-push --no-git-tag-version", + "bootstrap": "lerna bootstrap", "browser": "yarn workspace @segment/browser-destinations", - "cloud": "yarn workspace @segment/action-destinations", + "build": "nx run-many -t build && yarn build:browser-bundles", + "build:browser-bundles": "nx build @segment/destinations-manifest && nx build-web @segment/browser-destinations", + "canary": "./scripts/canary.sh", + "clean": "sh scripts/clean.sh", "cli": "yarn workspace @segment/actions-cli", "cli-internal": "yarn workspace @segment/actions-cli-internal", + "cloud": "yarn workspace @segment/action-destinations", "core": "yarn workspace @segment/actions-core", - "bootstrap": "lerna bootstrap", - "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", + "postversion": "bash scripts/postversion.sh", + "prepare": "husky install", + "release": "bash scripts/release.sh", + "shared": "yarn workspace @segment/actions-shared", "subscriptions": "yarn workspace @segment/destination-subscriptions", "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", + "test-partners": "lerna run test --stream --ignore @segment/actions-core --ignore @segment/actions-cli --ignore @segment/ajv-human-errors", "typecheck": "lerna run typecheck --stream", - "alpha": "lerna version prerelease --allow-branch $(git branch --show-current) --preid $(git branch --show-current) --no-push --no-git-tag-version", - "release": "bash scripts/release.sh", - "prepare": "husky install", - "clean": "sh scripts/clean.sh" + "types": "./bin/run generate:types", + "validate": "./bin/run validate" }, "devDependencies": { "@peculiar/webcrypto": "^1.2.3", 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 8bbb55e5de..72cbad9ae2 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.71.0", + "version": "1.102.0", "repository": { "type": "git", "url": "https://github.com/segmentio/action-destinations", @@ -37,11 +37,11 @@ }, "dependencies": { "@amplitude/ua-parser-js": "^0.7.25", - "@segment/actions-core": "^3.90.0", + "@segment/actions-core": "^3.121.0", "cheerio": "^1.0.0-rc.10", "dayjs": "^1.10.7", "escape-goat": "^3", - "liquidjs": "^9.37.0", + "liquidjs": "^10.8.4", "lodash": "^4.17.21" }, "jest": { diff --git a/packages/destination-actions/src/destinations/engage/utils/operationTracking/OperationTracker.test.ts b/packages/actions-shared/src/engage/__tests__/OperationTracker.test.ts similarity index 96% rename from packages/destination-actions/src/destinations/engage/utils/operationTracking/OperationTracker.test.ts rename to packages/actions-shared/src/engage/__tests__/OperationTracker.test.ts index 6d858af8b6..720a5bd2b6 100644 --- a/packages/destination-actions/src/destinations/engage/utils/operationTracking/OperationTracker.test.ts +++ b/packages/actions-shared/src/engage/__tests__/OperationTracker.test.ts @@ -1,10 +1,10 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */ -import { OperationLogger, OperationLoggerContext } from './OperationLogger' -import { OperationStats, OperationStatsContext } from './OperationStats' -import { OperationDecorator, ContextFromDecorator } from './OperationDecorator' -import { TryCatchFinallyHook } from './wrapTryCatchFinallyPromisable' -import { TrackedError } from './TrackedError' +import { OperationLogger, OperationLoggerContext } from '../utils/operationTracking/OperationLogger' +import { OperationStats, OperationStatsContext } from '../utils/operationTracking/OperationStats' +import { OperationDecorator, ContextFromDecorator } from '../utils/operationTracking/OperationDecorator' +import { TryCatchFinallyHook } from '../utils/operationTracking/wrapTryCatchFinallyPromisable' +import { TrackedError } from '../utils/operationTracking/TrackedError' class TestLogger extends OperationLogger { logInfo = jest.fn() diff --git a/packages/destination-actions/src/destinations/engage/utils/AggregateError.ts b/packages/actions-shared/src/engage/utils/AggregateError.ts similarity index 100% rename from packages/destination-actions/src/destinations/engage/utils/AggregateError.ts rename to packages/actions-shared/src/engage/utils/AggregateError.ts diff --git a/packages/destination-actions/src/destinations/engage/utils/EngageActionPerformer.ts b/packages/actions-shared/src/engage/utils/EngageActionPerformer.ts similarity index 99% rename from packages/destination-actions/src/destinations/engage/utils/EngageActionPerformer.ts rename to packages/actions-shared/src/engage/utils/EngageActionPerformer.ts index 62e5594cf1..61c0d2480e 100644 --- a/packages/destination-actions/src/destinations/engage/utils/EngageActionPerformer.ts +++ b/packages/actions-shared/src/engage/utils/EngageActionPerformer.ts @@ -78,6 +78,7 @@ export abstract class EngageActionPerformer +} diff --git a/packages/destination-actions/src/destinations/engage/utils/ResponseError.ts b/packages/actions-shared/src/engage/utils/ResponseError.ts similarity index 99% rename from packages/destination-actions/src/destinations/engage/utils/ResponseError.ts rename to packages/actions-shared/src/engage/utils/ResponseError.ts index d73e4ca0c8..8d6dc3d3a7 100644 --- a/packages/destination-actions/src/destinations/engage/utils/ResponseError.ts +++ b/packages/actions-shared/src/engage/utils/ResponseError.ts @@ -16,6 +16,7 @@ export interface ResponseError extends HTTPError { } code?: string status?: number + retry?: boolean } export interface ErrorDetails { @@ -37,7 +38,6 @@ export function getErrorDetails(error: any): ErrorDetails { const code = respError.code || respError.response?.data?.code // || respError.response?.statusText // e.g. 'Not Found' for 404 - const message = [ respError.name || respError.constructor?.name, respError.message, diff --git a/packages/destination-actions/src/destinations/engage/utils/getProfileApiEndpoint.ts b/packages/actions-shared/src/engage/utils/getProfileApiEndpoint.ts similarity index 100% rename from packages/destination-actions/src/destinations/engage/utils/getProfileApiEndpoint.ts rename to packages/actions-shared/src/engage/utils/getProfileApiEndpoint.ts diff --git a/packages/destination-actions/src/destinations/engage/utils/index.ts b/packages/actions-shared/src/engage/utils/index.ts similarity index 66% rename from packages/destination-actions/src/destinations/engage/utils/index.ts rename to packages/actions-shared/src/engage/utils/index.ts index 7c0d36c97a..d8c0b654ae 100644 --- a/packages/destination-actions/src/destinations/engage/utils/index.ts +++ b/packages/actions-shared/src/engage/utils/index.ts @@ -1,9 +1,14 @@ -export * from './EngageActionPerformer' -export * from './MessageSendPerformer' export * from './operationTracking' +export * from './AggregateError' +export * from './EngageActionPerformer' export * from './EngageLogger' export * from './EngageStats' -export * from './track' +export * from './getProfileApiEndpoint' +export * from './IntegrationErrorWrapper' export * from './isDestinationActionService' +export * from './isRetryableError' +export * from './MessageSendPerformer' +export * from './Profile' export * from './ResponseError' -export * from './IntegrationErrorWrapper' +export * from './testUtils' +export * from './track' diff --git a/packages/destination-actions/src/destinations/engage/utils/isDestinationActionService.ts b/packages/actions-shared/src/engage/utils/isDestinationActionService.ts similarity index 100% rename from packages/destination-actions/src/destinations/engage/utils/isDestinationActionService.ts rename to packages/actions-shared/src/engage/utils/isDestinationActionService.ts diff --git a/packages/destination-actions/src/destinations/engage/utils/isRetryableError.ts b/packages/actions-shared/src/engage/utils/isRetryableError.ts similarity index 100% rename from packages/destination-actions/src/destinations/engage/utils/isRetryableError.ts rename to packages/actions-shared/src/engage/utils/isRetryableError.ts diff --git a/packages/destination-actions/src/destinations/engage/utils/operationTracking/GenericMethodDecorator.ts b/packages/actions-shared/src/engage/utils/operationTracking/GenericMethodDecorator.ts similarity index 100% rename from packages/destination-actions/src/destinations/engage/utils/operationTracking/GenericMethodDecorator.ts rename to packages/actions-shared/src/engage/utils/operationTracking/GenericMethodDecorator.ts diff --git a/packages/destination-actions/src/destinations/engage/utils/operationTracking/OperationDecorator.ts b/packages/actions-shared/src/engage/utils/operationTracking/OperationDecorator.ts similarity index 100% rename from packages/destination-actions/src/destinations/engage/utils/operationTracking/OperationDecorator.ts rename to packages/actions-shared/src/engage/utils/operationTracking/OperationDecorator.ts diff --git a/packages/destination-actions/src/destinations/engage/utils/operationTracking/OperationDuration.ts b/packages/actions-shared/src/engage/utils/operationTracking/OperationDuration.ts similarity index 100% rename from packages/destination-actions/src/destinations/engage/utils/operationTracking/OperationDuration.ts rename to packages/actions-shared/src/engage/utils/operationTracking/OperationDuration.ts diff --git a/packages/destination-actions/src/destinations/engage/utils/operationTracking/OperationErrorHandler.ts b/packages/actions-shared/src/engage/utils/operationTracking/OperationErrorHandler.ts similarity index 100% rename from packages/destination-actions/src/destinations/engage/utils/operationTracking/OperationErrorHandler.ts rename to packages/actions-shared/src/engage/utils/operationTracking/OperationErrorHandler.ts diff --git a/packages/destination-actions/src/destinations/engage/utils/operationTracking/OperationFinallyHooks.ts b/packages/actions-shared/src/engage/utils/operationTracking/OperationFinallyHooks.ts similarity index 100% rename from packages/destination-actions/src/destinations/engage/utils/operationTracking/OperationFinallyHooks.ts rename to packages/actions-shared/src/engage/utils/operationTracking/OperationFinallyHooks.ts diff --git a/packages/destination-actions/src/destinations/engage/utils/operationTracking/OperationLogger.ts b/packages/actions-shared/src/engage/utils/operationTracking/OperationLogger.ts similarity index 100% rename from packages/destination-actions/src/destinations/engage/utils/operationTracking/OperationLogger.ts rename to packages/actions-shared/src/engage/utils/operationTracking/OperationLogger.ts diff --git a/packages/destination-actions/src/destinations/engage/utils/operationTracking/OperationStats.ts b/packages/actions-shared/src/engage/utils/operationTracking/OperationStats.ts similarity index 100% rename from packages/destination-actions/src/destinations/engage/utils/operationTracking/OperationStats.ts rename to packages/actions-shared/src/engage/utils/operationTracking/OperationStats.ts diff --git a/packages/destination-actions/src/destinations/engage/utils/operationTracking/OperationTree.ts b/packages/actions-shared/src/engage/utils/operationTracking/OperationTree.ts similarity index 100% rename from packages/destination-actions/src/destinations/engage/utils/operationTracking/OperationTree.ts rename to packages/actions-shared/src/engage/utils/operationTracking/OperationTree.ts diff --git a/packages/destination-actions/src/destinations/engage/utils/operationTracking/TrackedError.ts b/packages/actions-shared/src/engage/utils/operationTracking/TrackedError.ts similarity index 100% rename from packages/destination-actions/src/destinations/engage/utils/operationTracking/TrackedError.ts rename to packages/actions-shared/src/engage/utils/operationTracking/TrackedError.ts diff --git a/packages/destination-actions/src/destinations/engage/utils/operationTracking/index.ts b/packages/actions-shared/src/engage/utils/operationTracking/index.ts similarity index 100% rename from packages/destination-actions/src/destinations/engage/utils/operationTracking/index.ts rename to packages/actions-shared/src/engage/utils/operationTracking/index.ts diff --git a/packages/destination-actions/src/destinations/engage/utils/operationTracking/wrapTryCatchFinallyPromisable.ts b/packages/actions-shared/src/engage/utils/operationTracking/wrapTryCatchFinallyPromisable.ts similarity index 100% rename from packages/destination-actions/src/destinations/engage/utils/operationTracking/wrapTryCatchFinallyPromisable.ts rename to packages/actions-shared/src/engage/utils/operationTracking/wrapTryCatchFinallyPromisable.ts diff --git a/packages/actions-shared/src/engage/utils/testUtils.ts b/packages/actions-shared/src/engage/utils/testUtils.ts new file mode 100644 index 0000000000..b13da20f41 --- /dev/null +++ b/packages/actions-shared/src/engage/utils/testUtils.ts @@ -0,0 +1,31 @@ +import { Logger } from '@segment/actions-core/destination-kit' + +export function getTestLoggerUtils() { + const loggerMock = { + level: 'error', + name: 'test', + error: jest.fn() as Logger['error'], + info: jest.fn() as Logger['info'] + } as Logger + + function expectLogged(logMethod: Function, ...msgs: string[]) { + expect(logMethod).toHaveBeenCalledWith( + expect.stringMatching(new RegExp(`(.*)${msgs.join('(.*)')}(.*)`)), + expect.anything() + ) + } + + function expectErrorLogged(...msgs: string[]) { + expectLogged(loggerMock.error, ...msgs) + } + + function expectInfoLogged(...msgs: string[]) { + expectLogged(loggerMock.info, ...msgs) + } + + return { + loggerMock, + expectErrorLogged, + expectInfoLogged + } +} diff --git a/packages/destination-actions/src/destinations/engage/utils/track.ts b/packages/actions-shared/src/engage/utils/track.ts similarity index 100% rename from packages/destination-actions/src/destinations/engage/utils/track.ts rename to packages/actions-shared/src/engage/utils/track.ts diff --git a/packages/actions-shared/src/index.ts b/packages/actions-shared/src/index.ts index 5ee2b16aaa..1705cf3334 100644 --- a/packages/actions-shared/src/index.ts +++ b/packages/actions-shared/src/index.ts @@ -6,3 +6,4 @@ export * from './friendbuy/sharedCustomEvent' export * from './friendbuy/sharedPurchase' export * from './friendbuy/sharedSignUp' export * from './friendbuy/util' +export * from './engage/utils' diff --git a/packages/ajv-human-errors/README.md b/packages/ajv-human-errors/README.md index 64cccbf30d..14192b94b1 100644 --- a/packages/ajv-human-errors/README.md +++ b/packages/ajv-human-errors/README.md @@ -24,13 +24,13 @@ The following features of JSON Schema are not yet implemented (but will return t # Install ```console -$ yarn add @segment/ajv-human-error +$ yarn add @segment/ajv-human-errors ``` or ```console -$ npm install @segment/ajv-human-error +$ npm install @segment/ajv-human-errors ``` # Usage @@ -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..bf3b97597a 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.13.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 4ed0779ba4..a7d11c8b66 100644 --- a/packages/browser-destination-runtime/package.json +++ b/packages/browser-destination-runtime/package.json @@ -1,6 +1,6 @@ { "name": "@segment/browser-destination-runtime", - "version": "1.20.0", + "version": "1.50.0", "license": "MIT", "publishConfig": { "access": "public", @@ -62,7 +62,7 @@ } }, "dependencies": { - "@segment/actions-core": "^3.90.0" + "@segment/actions-core": "^3.121.0" }, "devDependencies": { "@segment/analytics-next": "*" 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 index eb893c5ade..d1f3ce5580 100644 --- a/packages/browser-destinations/destinations/1flow/README.md +++ b/packages/browser-destinations/destinations/1flow/README.md @@ -6,7 +6,7 @@ The 1Flow 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/1flow/package.json b/packages/browser-destinations/destinations/1flow/package.json index 787ad3cc69..8092d1ea4f 100644 --- a/packages/browser-destinations/destinations/1flow/package.json +++ b/packages/browser-destinations/destinations/1flow/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-1flow", - "version": "1.2.0", + "version": "1.33.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,7 +15,7 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/browser-destination-runtime": "^1.20.0" + "@segment/browser-destination-runtime": "^1.50.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 index 67bf6a48a2..a7e07e105c 100644 --- a/packages/browser-destinations/destinations/1flow/src/1flow.ts +++ b/packages/browser-destinations/destinations/1flow/src/1flow.ts @@ -18,5 +18,5 @@ export function initScript({ projectApiKey }) { r.setAttribute('data-api-key', k) r.src = t a.appendChild(r) - })(window, document, setTimeout, 'https://cdn-development.1flow.ai/js-sdk/1flow.js', apiKey) + })(window, document, setTimeout, 'https://1flow.app/js/1flow.js', apiKey) } diff --git a/packages/browser-destinations/destinations/1flow/src/api.ts b/packages/browser-destinations/destinations/1flow/src/api.ts index b8279d0f4e..fe3b0a7984 100644 --- a/packages/browser-destinations/destinations/1flow/src/api.ts +++ b/packages/browser-destinations/destinations/1flow/src/api.ts @@ -8,4 +8,4 @@ type _1FlowApi = { type _1FlowFunction = (method: method, ...args: unknown[]) => void -export type _1Flow = _1FlowFunction & _1FlowApi +export type _1flow = _1FlowFunction & _1FlowApi diff --git a/packages/browser-destinations/destinations/1flow/src/identifyUser/generated-types.ts b/packages/browser-destinations/destinations/1flow/src/identifyUser/generated-types.ts index f5b1336f24..87e4058615 100644 --- a/packages/browser-destinations/destinations/1flow/src/identifyUser/generated-types.ts +++ b/packages/browser-destinations/destinations/1flow/src/identifyUser/generated-types.ts @@ -5,30 +5,10 @@ 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 index 80401e35a7..ca8d07e261 100644 --- a/packages/browser-destinations/destinations/1flow/src/identifyUser/index.ts +++ b/packages/browser-destinations/destinations/1flow/src/identifyUser/index.ts @@ -1,9 +1,9 @@ import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types' -import { _1Flow } from '../api' +import { _1flow } from '../api' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -const action: BrowserActionDefinition = { +const action: BrowserActionDefinition = { title: 'Identify User', description: 'Create or update a user in 1Flow.', defaultSubscription: 'type = "identify"', @@ -18,15 +18,6 @@ const action: BrowserActionDefinition = { '@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', @@ -36,53 +27,12 @@ const action: BrowserActionDefinition = { 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 + perform: (_1flow, event) => { + const { userId, traits } = event.payload + _1flow('identify', userId, { + ...traits }) } } diff --git a/packages/browser-destinations/destinations/1flow/src/index.ts b/packages/browser-destinations/destinations/1flow/src/index.ts index 59edc58318..5c5b4e1106 100644 --- a/packages/browser-destinations/destinations/1flow/src/index.ts +++ b/packages/browser-destinations/destinations/1flow/src/index.ts @@ -3,16 +3,16 @@ import type { BrowserDestinationDefinition } from '@segment/browser-destination- import { browserDestination } from '@segment/browser-destination-runtime/shim' import trackEvent from './trackEvent' import { initScript } from './1flow' -import { _1Flow } from './api' +import { _1flow } from './api' import identifyUser from './identifyUser' import { defaultValues } from '@segment/actions-core' declare global { interface Window { - _1Flow: _1Flow + _1flow: _1flow } } -export const destination: BrowserDestinationDefinition = { +export const destination: BrowserDestinationDefinition = { name: '1Flow Web (Actions)', slug: 'actions-1flow', mode: 'device', @@ -46,8 +46,8 @@ export const destination: BrowserDestinationDefinition = { initialize: async ({ settings }, deps) => { const projectApiKey = settings.projectApiKey initScript({ projectApiKey }) - await deps.resolveWhen(() => Object.prototype.hasOwnProperty.call(window, '_1Flow'), 100) - return window._1Flow + await deps.resolveWhen(() => Object.prototype.hasOwnProperty.call(window, '_1flow'), 100) + return window._1flow }, actions: { trackEvent, diff --git a/packages/browser-destinations/destinations/1flow/src/trackEvent/index.ts b/packages/browser-destinations/destinations/1flow/src/trackEvent/index.ts index 0fec9ca043..e8a096058e 100644 --- a/packages/browser-destinations/destinations/1flow/src/trackEvent/index.ts +++ b/packages/browser-destinations/destinations/1flow/src/trackEvent/index.ts @@ -1,9 +1,9 @@ import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types' -import { _1Flow } from '../api' +import { _1flow } from '../api' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -const action: BrowserActionDefinition = { +const action: BrowserActionDefinition = { title: 'Track Event', description: 'Submit an event to 1Flow.', defaultSubscription: 'type = "track"', @@ -46,9 +46,9 @@ const action: BrowserActionDefinition = { } } }, - perform: (_1Flow, event) => { + perform: (_1flow, event) => { const { event_name, userId, anonymousId, properties } = event.payload - _1Flow('track', event_name, { + _1flow('track', event_name, { userId: userId, anonymousId: anonymousId, properties: properties diff --git a/packages/browser-destinations/destinations/adobe-target/package.json b/packages/browser-destinations/destinations/adobe-target/package.json index c0bf813aaf..d98d43608a 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.21.0", + "version": "1.51.0", "license": "MIT", "publishConfig": { "access": "public", @@ -16,7 +16,7 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/browser-destination-runtime": "^1.20.0" + "@segment/browser-destination-runtime": "^1.50.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..20640acdb0 --- /dev/null +++ b/packages/browser-destinations/destinations/algolia-plugins/package.json @@ -0,0 +1,23 @@ +{ + "name": "@segment/analytics-browser-actions-algolia-plugins", + "version": "1.28.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.50.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 c8ce4bae3f..96a07ed7f2 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.21.0", + "version": "1.51.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,7 +15,7 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/browser-destination-runtime": "^1.20.0" + "@segment/browser-destination-runtime": "^1.50.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 ebc9800a62..7e8bb3ffb6 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.24.0", + "version": "1.54.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/analytics-browser-actions-braze": "^1.24.0", - "@segment/browser-destination-runtime": "^1.20.0" + "@segment/analytics-browser-actions-braze": "^1.54.0", + "@segment/browser-destination-runtime": "^1.50.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 1927085ed2..015415662b 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.24.0", + "version": "1.54.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.90.0", - "@segment/browser-destination-runtime": "^1.20.0" + "@segment/actions-core": "^3.121.0", + "@segment/browser-destination-runtime": "^1.50.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/braze/src/index.ts b/packages/browser-destinations/destinations/braze/src/index.ts index 8226e74ace..70e8b099ae 100644 --- a/packages/browser-destinations/destinations/braze/src/index.ts +++ b/packages/browser-destinations/destinations/braze/src/index.ts @@ -111,6 +111,7 @@ export const destination: BrowserDestinationDefinition=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 index 723ab6037b..b47472f8a4 100644 --- a/packages/browser-destinations/destinations/bucket/src/__tests__/index.test.ts +++ b/packages/browser-destinations/destinations/bucket/src/__tests__/index.test.ts @@ -70,11 +70,55 @@ describe('Bucket', () => { analyticsInstance.reset() expect(getBucketCallLog()).toStrictEqual([ - { method: 'init', args: ['testTrackingKey'] }, + { 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({ @@ -86,7 +130,7 @@ describe('Bucket', () => { await instance.load(Context.system(), analyticsInstance) - expect(getBucketCallLog()).toStrictEqual([{ method: 'init', args: ['testTrackingKey'] }]) + expect(getBucketCallLog()).toStrictEqual([{ method: 'init', args: ['testTrackingKey', {}] }]) }) }) @@ -108,7 +152,7 @@ describe('Bucket', () => { await instance.load(Context.system(), analyticsInstance) expect(getBucketCallLog()).toStrictEqual([ - { method: 'init', args: ['testTrackingKey'] }, + { 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 index ed3d76cd3b..87197ad986 100644 --- a/packages/browser-destinations/destinations/bucket/src/generated-types.ts +++ b/packages/browser-destinations/destinations/bucket/src/generated-types.ts @@ -2,7 +2,7 @@ export interface Settings { /** - * Your Bucket App tracking key, found on the tracking page. + * The publishable key for your Bucket environment, found on the tracking page on app.bucket.co. */ 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 index e9e4787fd4..fbba8d8467 100644 --- a/packages/browser-destinations/destinations/bucket/src/group/__tests__/index.test.ts +++ b/packages/browser-destinations/destinations/bucket/src/group/__tests__/index.test.ts @@ -71,7 +71,7 @@ describe('Bucket.company', () => { ) expect(getBucketCallLog()).toStrictEqual([ - { method: 'init', args: ['testTrackingKey'] }, + { method: 'init', args: ['testTrackingKey', {}] }, { method: 'user', args: ['user-id-1', {}, { active: false }] @@ -129,7 +129,7 @@ describe('Bucket.company', () => { ) expect(getBucketCallLog()).toStrictEqual([ - { method: 'init', args: ['testTrackingKey'] }, + { method: 'init', args: ['testTrackingKey', {}] }, { method: 'user', args: ['user-id-1'] @@ -180,7 +180,7 @@ describe('Bucket.company', () => { // 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'] }]) + expect(getBucketCallLog()).toStrictEqual([{ method: 'init', args: ['testTrackingKey', {}] }]) }) }) }) 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 index 255a2c2050..51a7e58239 100644 --- a/packages/browser-destinations/destinations/bucket/src/identifyUser/__tests__/index.test.ts +++ b/packages/browser-destinations/destinations/bucket/src/identifyUser/__tests__/index.test.ts @@ -61,7 +61,7 @@ describe('Bucket.user', () => { ) expect(getBucketCallLog()).toStrictEqual([ - { method: 'init', args: ['testTrackingKey'] }, + { method: 'init', args: ['testTrackingKey', {}] }, { method: 'user', args: [ diff --git a/packages/browser-destinations/destinations/bucket/src/index.ts b/packages/browser-destinations/destinations/bucket/src/index.ts index c463095200..87305ba434 100644 --- a/packages/browser-destinations/destinations/bucket/src/index.ts +++ b/packages/browser-destinations/destinations/bucket/src/index.ts @@ -45,9 +45,10 @@ export const destination: BrowserDestinationDefinition = { ], settings: { + // kept as the legacy `trackingKey` here to avoid needing to migrate installed plugins trackingKey: { - description: 'Your Bucket App tracking key, found on the tracking page.', - label: 'Tracking Key', + description: 'The publishable key for your Bucket environment, found on the tracking page on app.bucket.co.', + label: 'Publishable Key', type: 'string', required: true } @@ -60,10 +61,21 @@ export const destination: BrowserDestinationDefinition = { }, initialize: async ({ settings, analytics }, deps) => { - await deps.loadScript('https://cdn.jsdelivr.net/npm/@bucketco/tracking-sdk@2') + 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) + 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. diff --git a/packages/browser-destinations/destinations/bucket/src/test-utils.ts b/packages/browser-destinations/destinations/bucket/src/test-utils.ts index 3c4cdb28a9..baabbb6e6d 100644 --- a/packages/browser-destinations/destinations/bucket/src/test-utils.ts +++ b/packages/browser-destinations/destinations/bucket/src/test-utils.ts @@ -38,7 +38,9 @@ export function bucketTestHooks() { }) beforeEach(() => { - nock('https://cdn.jsdelivr.net').get('/npm/@bucketco/tracking-sdk@2').reply(200, bucketTestMock) + nock('https://cdn.jsdelivr.net') + .get((uri) => uri.startsWith('/npm/@bucketco/tracking-sdk@')) + .reply(200, bucketTestMock) }) afterEach(function () { @@ -46,8 +48,9 @@ export function bucketTestHooks() { // @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() } + + nock.cleanAll() }) afterAll(() => { 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 index d7f583bc20..5c94ef5aab 100644 --- a/packages/browser-destinations/destinations/bucket/src/trackEvent/__tests__/index.test.ts +++ b/packages/browser-destinations/destinations/bucket/src/trackEvent/__tests__/index.test.ts @@ -64,7 +64,7 @@ describe('trackEvent', () => { ) expect(getBucketCallLog()).toStrictEqual([ - { method: 'init', args: ['testTrackingKey'] }, + { method: 'init', args: ['testTrackingKey', {}] }, { method: 'user', args: ['user-id-1', {}, { active: false }] @@ -109,7 +109,7 @@ describe('trackEvent', () => { ) expect(getBucketCallLog()).toStrictEqual([ - { method: 'init', args: ['testTrackingKey'] }, + { method: 'init', args: ['testTrackingKey', {}] }, { method: 'user', args: ['user-id-1'] @@ -153,7 +153,7 @@ describe('trackEvent', () => { // 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'] }]) + expect(getBucketCallLog()).toStrictEqual([{ method: 'init', args: ['testTrackingKey', {}] }]) }) }) }) 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 f729d428bf..df7c980421 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.8.0", + "version": "1.38.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,7 +15,7 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/browser-destination-runtime": "^1.20.0" + "@segment/browser-destination-runtime": "^1.50.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/commandbar/package.json b/packages/browser-destinations/destinations/commandbar/package.json index 2510649c40..09efd2e1ae 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.21.0", + "version": "1.51.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.90.0", - "@segment/browser-destination-runtime": "^1.20.0" + "@segment/actions-core": "^3.121.0", + "@segment/browser-destination-runtime": "^1.50.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/commandbar/src/init-script.ts b/packages/browser-destinations/destinations/commandbar/src/init-script.ts index d1d148df3d..ac1b750bde 100644 --- a/packages/browser-destinations/destinations/commandbar/src/init-script.ts +++ b/packages/browser-destinations/destinations/commandbar/src/init-script.ts @@ -79,8 +79,5 @@ export function initScript(orgId) { t(''.concat(u, '/latest/').concat(o, '?').concat(d.join('&')), !0) } } - void 0 === Object.assign || 'undefined' == typeof Symbol || void 0 === Symbol.for - ? ((a.__CommandBarBootstrap__ = r), - t('https://polyfill.io/v3/polyfill.min.js?version=3.101.0&callback=__CommandBarBootstrap__&features=' + n)) - : r() + r() } 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 a1fd263c46..ec697bab41 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.8.0", + "version": "1.38.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,7 +15,7 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/browser-destination-runtime": "^1.20.0" + "@segment/browser-destination-runtime": "^1.50.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 d67b7e1d42..259dd43e26 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.21.0", + "version": "1.52.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,9 +15,9 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.90.0", - "@segment/actions-shared": "^1.71.0", - "@segment/browser-destination-runtime": "^1.20.0" + "@segment/actions-core": "^3.121.0", + "@segment/actions-shared": "^1.102.0", + "@segment/browser-destination-runtime": "^1.50.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 7b78395fc1..c179b1239e 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.22.0", + "version": "1.53.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,9 +15,9 @@ }, "typings": "./dist/esm", "dependencies": { - "@fullstory/browser": "^1.4.9", - "@segment/actions-core": "^3.90.0", - "@segment/browser-destination-runtime": "^1.20.0" + "@fullstory/browser": "^2.0.3", + "@segment/actions-core": "^3.121.0", + "@segment/browser-destination-runtime": "^1.50.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 88e5566464..f87081f55b 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.25.0", + "version": "1.58.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.90.0", - "@segment/browser-destination-runtime": "^1.20.0" + "@segment/actions-core": "^3.121.0", + "@segment/browser-destination-runtime": "^1.50.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..edc13ac595 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: { @@ -118,7 +120,16 @@ export const destination: BrowserDestinationDefinition = { { label: 'Granted', value: 'granted' }, { label: 'Denied', value: 'denied' } ], - default: 'granted' + default: 'granted', + depends_on: { + conditions: [ + { + fieldKey: 'enableConsentMode', + operator: 'is', + value: true + } + ] + } }, defaultAnalyticsStorageConsentState: { description: @@ -129,7 +140,56 @@ export const destination: BrowserDestinationDefinition = { { label: 'Granted', value: 'granted' }, { label: 'Denied', value: 'denied' } ], - default: 'granted' + default: 'granted', + depends_on: { + conditions: [ + { + fieldKey: 'enableConsentMode', + operator: 'is', + value: true + } + ] + } + }, + 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, + depends_on: { + conditions: [ + { + fieldKey: 'enableConsentMode', + operator: 'is', + value: true + } + ] + } + }, + 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, + depends_on: { + conditions: [ + { + fieldKey: 'enableConsentMode', + operator: 'is', + value: true + } + ] + } }, waitTimeToUpdateConsentStage: { description: @@ -154,11 +214,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 f220beac22..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,17 @@ 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, @@ -28,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.', @@ -93,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) { @@ -158,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 5bcbe4380a..51f29fb668 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.11.0", + "version": "1.42.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,7 +15,7 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/browser-destination-runtime": "^1.20.0" + "@segment/browser-destination-runtime": "^1.50.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/google-campaign-manager/src/__tests__/index.test.ts b/packages/browser-destinations/destinations/google-campaign-manager/src/__tests__/index.test.ts new file mode 100644 index 0000000000..f8d817df1e --- /dev/null +++ b/packages/browser-destinations/destinations/google-campaign-manager/src/__tests__/index.test.ts @@ -0,0 +1,185 @@ +import { Subscription } from '@segment/browser-destination-runtime/types' +import googleCampaignManager, { destination } from '../index' +import { Analytics, Context } from '@segment/analytics-next' + +const subscriptions: Subscription[] = [ + { + partnerAction: 'salesActivity', + name: 'Sales Activity', + enabled: true, + subscribe: 'type = "track"', + mapping: {} + } +] + +describe('Google Tag for Campaign Manager', () => { + const defaultSettings = { + advertiserId: 'test123', + allowAdPersonalizationSignals: false, + conversionLinker: false + } + beforeEach(async () => { + jest.restoreAllMocks() + + const [googleCampaignManagerPlugin] = await googleCampaignManager({ + ...defaultSettings, + subscriptions + }) + jest.spyOn(destination, 'initialize') + + await googleCampaignManagerPlugin.load(Context.system(), {} as Analytics) + }) + + it('should not update consent if enable_consent mode is denied', async () => { + const settings = { + ...defaultSettings, + enableConsentMode: false + } + + const [event] = await googleCampaignManager({ ...settings, subscriptions }) + await event.load(Context.system(), {} as Analytics) + expect(destination.initialize).toHaveBeenCalled() + + expect(window.dataLayer).toEqual( + expect.arrayContaining([expect.not.objectContaining(Object.assign({}, ['consent', 'default', {}]))]) + ) + }) + + it('should update consent if analytics storage is granted', async () => { + const settings = { + ...defaultSettings, + enableConsentMode: true, + defaultAnalyticsStorageConsentState: 'granted' + } + + const [event] = await googleCampaignManager({ ...settings, subscriptions }) + await event.load(Context.system(), {} as Analytics) + expect(destination.initialize).toHaveBeenCalled() + + expect(window.dataLayer).toEqual( + expect.arrayContaining([ + expect.objectContaining(Object.assign({}, ['consent', 'default', { analytics_storage: 'granted' }])) + ]) + ) + }) + + it('should update consent if analytics storage is denied', async () => { + const settings = { + ...defaultSettings, + enableConsentMode: true, + defaultAnalyticsStorageConsentState: 'denied' + } + + const [event] = await googleCampaignManager({ ...settings, subscriptions }) + await event.load(Context.system(), {} as Analytics) + expect(destination.initialize).toHaveBeenCalled() + + expect(window.dataLayer).toEqual( + expect.arrayContaining([ + expect.objectContaining(Object.assign({}, ['consent', 'default', { analytics_storage: 'denied' }])) + ]) + ) + }) + it('should update consent if Ad storage is granted', async () => { + const settings = { + ...defaultSettings, + enableConsentMode: true, + defaultAdsStorageConsentState: 'granted' + } + + const [event] = await googleCampaignManager({ ...settings, subscriptions }) + await event.load(Context.system(), {} as Analytics) + expect(destination.initialize).toHaveBeenCalled() + + expect(window.dataLayer).toEqual( + expect.arrayContaining([ + expect.objectContaining(Object.assign({}, ['consent', 'default', { ad_storage: 'granted' }])) + ]) + ) + }) + it('should update consent if Ad storage is denied', async () => { + const settings = { + ...defaultSettings, + enableConsentMode: true, + defaultAdsStorageConsentState: 'denied' + } + + const [event] = await googleCampaignManager({ ...settings, subscriptions }) + await event.load(Context.system(), {} as Analytics) + expect(destination.initialize).toHaveBeenCalled() + + expect(window.dataLayer).toEqual( + expect.arrayContaining([ + expect.objectContaining(Object.assign({}, ['consent', 'default', { ad_storage: 'denied' }])) + ]) + ) + }) + it('should update consent if Ad user data is granted', async () => { + const settings = { + ...defaultSettings, + enableConsentMode: true, + adUserDataConsentState: 'granted' + } + + const [event] = await googleCampaignManager({ ...settings, subscriptions }) + await event.load(Context.system(), {} as Analytics) + expect(destination.initialize).toHaveBeenCalled() + + expect(window.dataLayer).toEqual( + expect.arrayContaining([ + expect.objectContaining(Object.assign({}, ['consent', 'default', { ad_user_data: 'granted' }])) + ]) + ) + }) + it('should update consent if Ad user data is denied', async () => { + const settings = { + ...defaultSettings, + enableConsentMode: true, + adUserDataConsentState: 'denied' + } + + const [event] = await googleCampaignManager({ ...settings, subscriptions }) + await event.load(Context.system(), {} as Analytics) + expect(destination.initialize).toHaveBeenCalled() + + expect(window.dataLayer).toEqual( + expect.arrayContaining([ + expect.objectContaining(Object.assign({}, ['consent', 'default', { ad_user_data: 'denied' }])) + ]) + ) + }) + it('should update consent if Ad personalization is granted', async () => { + const settings = { + ...defaultSettings, + enableConsentMode: true, + adPersonalizationConsentState: 'granted' + } + + const [event] = await googleCampaignManager({ ...settings, subscriptions }) + await event.load(Context.system(), {} as Analytics) + expect(destination.initialize).toHaveBeenCalled() + + expect(window.dataLayer).toEqual( + expect.arrayContaining([ + expect.objectContaining(Object.assign({}, ['consent', 'default', { ad_personalization: 'granted' }])) + ]) + ) + }) + it('should update consent if Ad personalization is denied', async () => { + const settings = { + ...defaultSettings, + enableConsentMode: true, + adPersonalizationConsentState: 'denied' + } + + const [event] = await googleCampaignManager({ ...settings, subscriptions }) + await event.load(Context.system(), {} as Analytics) + expect(destination.initialize).toHaveBeenCalled() + + expect(window.dataLayer).toEqual( + expect.arrayContaining([ + expect.objectContaining(Object.assign({}, ['consent', 'default', { ad_personalization: 'denied' }])) + ]) + ) + }) +}) diff --git a/packages/browser-destinations/destinations/google-campaign-manager/src/generated-types.ts b/packages/browser-destinations/destinations/google-campaign-manager/src/generated-types.ts index e7460761ba..70ed9aac4a 100644 --- a/packages/browser-destinations/destinations/google-campaign-manager/src/generated-types.ts +++ b/packages/browser-destinations/destinations/google-campaign-manager/src/generated-types.ts @@ -13,4 +13,24 @@ export interface Settings { * This feature can be disabled if you do not want the global site tag to set first party cookies on your site domain. */ conversionLinker: boolean + /** + * Set to true to enable Google’s [Consent Mode](https://support.google.com/analytics/answer/9976101?hl=en). Set to false by default. + */ + enableConsentMode?: boolean + /** + * 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 + /** + * The default value for ad 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. + */ + defaultAdsStorageConsentState?: string + /** + * 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 } diff --git a/packages/browser-destinations/destinations/google-campaign-manager/src/index.ts b/packages/browser-destinations/destinations/google-campaign-manager/src/index.ts index 482be16ddb..46ec587ec9 100644 --- a/packages/browser-destinations/destinations/google-campaign-manager/src/index.ts +++ b/packages/browser-destinations/destinations/google-campaign-manager/src/index.ts @@ -11,6 +11,8 @@ declare global { } } +type ConsentParamsArg = 'granted' | 'denied' | undefined + export const destination: BrowserDestinationDefinition = { name: 'Google Tag for Campaign Manager', slug: 'actions-google-campaign-manager', @@ -40,6 +42,92 @@ export const destination: BrowserDestinationDefinition = { type: 'boolean', required: true, default: true + }, + enableConsentMode: { + description: `Set to true to enable Google’s [Consent Mode](https://support.google.com/analytics/answer/9976101?hl=en). Set to false by default.`, + label: 'Enable Consent Mode', + type: 'boolean', + default: false + }, + 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, + depends_on: { + conditions: [ + { + fieldKey: 'enableConsentMode', + operator: 'is', + value: true + } + ] + } + }, + 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, + depends_on: { + conditions: [ + { + fieldKey: 'enableConsentMode', + operator: 'is', + value: true + } + ] + } + }, + defaultAdsStorageConsentState: { + description: + 'The default value for ad 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.', + label: 'Default Ads Storage Consent State', + type: 'string', + choices: [ + { label: 'Granted', value: 'granted' }, + { label: 'Denied', value: 'denied' } + ], + default: undefined, + depends_on: { + conditions: [ + { + fieldKey: 'enableConsentMode', + operator: 'is', + value: true + } + ] + } + }, + defaultAnalyticsStorageConsentState: { + description: + '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.', + label: 'Default Analytics Storage Consent State', + type: 'string', + choices: [ + { label: 'Granted', value: 'granted' }, + { label: 'Denied', value: 'denied' } + ], + default: undefined, + depends_on: { + conditions: [ + { + fieldKey: 'enableConsentMode', + operator: 'is', + value: true + } + ] + } } }, @@ -49,12 +137,35 @@ export const destination: BrowserDestinationDefinition = { // eslint-disable-next-line prefer-rest-params window.dataLayer.push(arguments) } - window.gtag('set', 'allow_ad_personalization_signals', settings.allowAdPersonalizationSignals) window.gtag('js', new Date()) window.gtag('config', settings.advertiserId, { conversion_linker: settings.conversionLinker }) + if (settings.enableConsentMode) { + const consent: { + ad_storage?: ConsentParamsArg + analytics_storage?: ConsentParamsArg + ad_user_data?: ConsentParamsArg + ad_personalization?: ConsentParamsArg + allow_ad_personalization_signals?: Boolean + } = {} + + if (settings.defaultAnalyticsStorageConsentState) { + consent.analytics_storage = settings.defaultAnalyticsStorageConsentState as ConsentParamsArg + } + if (settings.defaultAdsStorageConsentState) { + consent.ad_storage = settings.defaultAdsStorageConsentState as ConsentParamsArg + } + if (settings.adUserDataConsentState) { + consent.ad_user_data = settings.adUserDataConsentState as ConsentParamsArg + } + if (settings.adPersonalizationConsentState) { + consent.ad_personalization = settings.adPersonalizationConsentState as ConsentParamsArg + } + + window.gtag('consent', 'default', consent) + } const script = `https://www.googletagmanager.com/gtag/js?id=${settings.advertiserId}` await deps.loadScript(script) return window.gtag diff --git a/packages/browser-destinations/destinations/heap/package.json b/packages/browser-destinations/destinations/heap/package.json index 50822569bc..e630f461e3 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.21.0", + "version": "1.51.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.90.0", - "@segment/browser-destination-runtime": "^1.20.0" + "@segment/actions-core": "^3.121.0", + "@segment/browser-destination-runtime": "^1.50.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..d7ead7d208 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,54 @@ describe('#trackEvent', () => { type: 'track', name: 'hello!', properties: { - banana: '📞', - apple: [ + products: [ { - carrot: 12, - broccoli: [ - { - onion: 'crisp', - tomato: 'fruit' - } - ] + name: 'Test Product 1', + 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', + 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","color":"red","qty":2,"custom_vars":{"position":0,"something_else":"test","another_one":["one","two","three"]}},{"name":"Test Product 2","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) @@ -132,24 +110,20 @@ describe('#trackEvent', () => { for (let i = 1; i <= 3; i++) { expect(heapTrackSpy).toHaveBeenNthCalledWith(i, 'hello! testArray1 item', { - val: i, + val: i.toString(), segment_library: HEAP_SEGMENT_BROWSER_LIBRARY_NAME }) } for (let i = 4; i <= 5; i++) { expect(heapTrackSpy).toHaveBeenNthCalledWith(i, 'hello! testArray2 item', { - val: i, + val: i.toString(), segment_library: HEAP_SEGMENT_BROWSER_LIBRARY_NAME }) } 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 +141,86 @@ 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', + 'custom_vars.bodyText': 'Testing text', + 'custom_vars.ctaText': 'Click me', + 'custom_vars.position': '0', + 'custom_vars.testNestedValues.count': '5', + 'custom_vars.testNestedValues.color': 'green' + }) + }) + + it('should flatten properties on parent when browserArrayLimit is set', async () => { + await eventWithUnrolling.track?.( + new Context({ + type: 'track', + name: 'hello!', + properties: { + boolean_test: false, + string_test: 'react', + number_test: 0, + custom_vars: { + property: 1 + } + } + }) + ) + expect(heapTrackSpy).toHaveBeenCalledWith('hello!', { + segment_library: HEAP_SEGMENT_BROWSER_LIBRARY_NAME, + boolean_test: 'false', + string_test: 'react', + number_test: '0', + 'custom_vars.property': '1' + }) + }) + it('should send segment_library property if no other properties were provided', async () => { await event.track?.( new Context({ @@ -238,4 +286,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..ce9cbce6f0 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) @@ -99,13 +101,13 @@ const heapTrackArrays = ( return eventProperties } + delete eventProperties[key] + eventProperties = { ...eventProperties, ...flat({ [key]: value }) } + if (!Array.isArray(value)) { continue } - delete eventProperties[key] - eventProperties = { ...eventProperties, ...flat({ [key]: value }) } - const arrayLength = value.length let arrayPropertyValues // truncate in case there are multiple array properties diff --git a/packages/browser-destinations/destinations/heap/src/utils.ts b/packages/browser-destinations/destinations/heap/src/utils.ts index f19a1c039b..52e89bd178 100644 --- a/packages/browser-destinations/destinations/heap/src/utils.ts +++ b/packages/browser-destinations/destinations/heap/src/utils.ts @@ -10,7 +10,7 @@ export type Properties = { } type FlattenProperties = object & { - [k: string]: string + [k: string]: string | null } export function flat(data?: Properties, prefix = ''): FlattenProperties | undefined { @@ -19,7 +19,7 @@ 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 { @@ -37,14 +37,15 @@ export const flattenProperties = (arrayPropertyValue: any) => { if (typeof value == 'object' && value !== null) { arrayProperties = { ...arrayProperties, ...flat({ [key]: value as Properties }) } } else { - arrayProperties = Object.assign(arrayProperties, { [key]: value }) + const stringifiedValue = stringify(value) + arrayProperties = Object.assign(arrayProperties, { [key]: stringifiedValue }) } } return arrayProperties } -function stringify(value: unknown): string { - if (typeof value === 'string') { +function stringify(value: unknown): string | null { + if (typeof value === 'string' || value === null) { return value } if (typeof value === 'number' || typeof value === 'boolean') { diff --git a/packages/browser-destinations/destinations/hubble-web/README.md b/packages/browser-destinations/destinations/hubble-web/README.md index fad1cb7abb..b4884b277e 100644 --- a/packages/browser-destinations/destinations/hubble-web/README.md +++ b/packages/browser-destinations/destinations/hubble-web/README.md @@ -6,7 +6,7 @@ The Hubble (actions) browser action destination for use with @segment/analytics- 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/hubble-web/package.json b/packages/browser-destinations/destinations/hubble-web/package.json index 2c481773c9..f4d28ce3e0 100644 --- a/packages/browser-destinations/destinations/hubble-web/package.json +++ b/packages/browser-destinations/destinations/hubble-web/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-hubble-web", - "version": "1.7.0", + "version": "1.37.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.90.0", - "@segment/browser-destination-runtime": "^1.20.0" + "@segment/actions-core": "^3.121.0", + "@segment/browser-destination-runtime": "^1.50.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/hubspot-web/package.json b/packages/browser-destinations/destinations/hubspot-web/package.json index a349b6e478..067f17edcc 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.21.0", + "version": "1.51.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.90.0", - "@segment/browser-destination-runtime": "^1.20.0" + "@segment/actions-core": "^3.121.0", + "@segment/browser-destination-runtime": "^1.50.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 6a3d184535..111dc55059 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.21.0", + "version": "1.54.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,9 +15,9 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.90.0", - "@segment/actions-shared": "^1.71.0", - "@segment/browser-destination-runtime": "^1.20.0" + "@segment/actions-core": "^3.121.0", + "@segment/actions-shared": "^1.102.0", + "@segment/browser-destination-runtime": "^1.50.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/intercom/src/__tests__/index.test.ts b/packages/browser-destinations/destinations/intercom/src/__tests__/index.test.ts index 769339b6ac..a43b08bf86 100644 --- a/packages/browser-destinations/destinations/intercom/src/__tests__/index.test.ts +++ b/packages/browser-destinations/destinations/intercom/src/__tests__/index.test.ts @@ -39,6 +39,10 @@ describe('Intercom (actions)', () => { await event.load(Context.system(), {} as Analytics) expect(destination.initialize).toHaveBeenCalled() + expect(window.intercomSettings).toBeDefined() + expect(window.intercomSettings.app_id).toEqual('topSecretKey') + expect(window.intercomSettings.installation_type).toEqual('s') + const scripts = window.document.querySelectorAll('script') expect(scripts).toMatchInlineSnapshot(` NodeList [ diff --git a/packages/browser-destinations/destinations/intercom/src/identifyUser/index.ts b/packages/browser-destinations/destinations/intercom/src/identifyUser/index.ts index 574dcb7e78..4384859222 100644 --- a/packages/browser-destinations/destinations/intercom/src/identifyUser/index.ts +++ b/packages/browser-destinations/destinations/intercom/src/identifyUser/index.ts @@ -97,9 +97,9 @@ const action: BrowserActionDefinition = { required: false, default: { '@if': { - exists: { '@path': '$.context.Intercom.user_hash' }, - then: { '@path': '$.context.Intercom.user_hash' }, - else: { '@path': '$.context.Intercom.userHash' } + exists: { '@path': '$.integrations.Intercom.user_hash' }, + then: { '@path': '$.integrations.Intercom.user_hash' }, + else: { '@path': '$.integrations.Intercom.userHash' } } } }, diff --git a/packages/browser-destinations/destinations/intercom/src/index.ts b/packages/browser-destinations/destinations/intercom/src/index.ts index 6e00218699..598f003dda 100644 --- a/packages/browser-destinations/destinations/intercom/src/index.ts +++ b/packages/browser-destinations/destinations/intercom/src/index.ts @@ -1,7 +1,7 @@ import type { Settings } from './generated-types' import type { BrowserDestinationDefinition } from '@segment/browser-destination-runtime/types' import { browserDestination } from '@segment/browser-destination-runtime/shim' -import { initialBoot, initScript } from './init-script' +import { initialBoot, initScript, initSettings } from './init-script' import { Intercom } from './api' import trackEvent from './trackEvent' @@ -12,6 +12,10 @@ import { defaultValues } from '@segment/actions-core' declare global { interface Window { Intercom: Intercom + intercomSettings: { + app_id: string + installation_type?: string + } } } @@ -90,6 +94,7 @@ export const destination: BrowserDestinationDefinition = { initialize: async ({ settings }, deps) => { //initialize Intercom initScript({ appId: settings.appId }) + initSettings({ appId: settings.appId }) const preloadedIntercom = window.Intercom initialBoot(settings.appId, { api_base: settings.apiBase }) diff --git a/packages/browser-destinations/destinations/intercom/src/init-script.ts b/packages/browser-destinations/destinations/intercom/src/init-script.ts index a9b7ec35ac..b6b624276b 100644 --- a/packages/browser-destinations/destinations/intercom/src/init-script.ts +++ b/packages/browser-destinations/destinations/intercom/src/init-script.ts @@ -44,6 +44,19 @@ export function initialBoot(appId: string, options = {}) { window.Intercom && window.Intercom('boot', { app_id: appId, + installation_type: 's', ...options }) } + +export function initSettings({ appId }) { + if (window.intercomSettings) { + window.intercomSettings.app_id = appId + window.intercomSettings.installation_type = 's' + } else { + window.intercomSettings = { + app_id: appId, + installation_type: 's' + } + } +} diff --git a/packages/browser-destinations/destinations/iterate/package.json b/packages/browser-destinations/destinations/iterate/package.json index 1f6a0da30f..d3a14e9f79 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.21.0", + "version": "1.51.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.90.0", - "@segment/browser-destination-runtime": "^1.20.0" + "@segment/actions-core": "^3.121.0", + "@segment/browser-destination-runtime": "^1.50.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 index 356ae0725b..be894a1ed3 100644 --- a/packages/browser-destinations/destinations/jimo/README.md +++ b/packages/browser-destinations/destinations/jimo/README.md @@ -6,7 +6,7 @@ The Jimo 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/jimo/package.json b/packages/browser-destinations/destinations/jimo/package.json index 35a896e2df..907a52339a 100644 --- a/packages/browser-destinations/destinations/jimo/package.json +++ b/packages/browser-destinations/destinations/jimo/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-jimo", - "version": "1.7.0", + "version": "1.39.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,7 +15,7 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/browser-destination-runtime": "^1.20.0" + "@segment/browser-destination-runtime": "^1.50.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/jimo/src/index.ts b/packages/browser-destinations/destinations/jimo/src/index.ts index fd23a11714..fd06c0c8d5 100644 --- a/packages/browser-destinations/destinations/jimo/src/index.ts +++ b/packages/browser-destinations/destinations/jimo/src/index.ts @@ -3,6 +3,7 @@ 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' @@ -38,6 +39,13 @@ export const destination: BrowserDestinationDefinition = { 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) => { @@ -45,12 +53,13 @@ export const destination: BrowserDestinationDefinition = { await deps.loadScript(`${ENDPOINT_UNDERCITY}`) - await deps.resolveWhen(() => Array.isArray(window.jimo), 100) + await deps.resolveWhen(() => Array.isArray(window.jimo) === false, 100) return window.jimo as JimoSDK }, actions: { - sendUserData + sendUserData, + sendTrackEvent } } 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/koala/package.json b/packages/browser-destinations/destinations/koala/package.json index 4ee1c3300b..6102ab757d 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.21.0", + "version": "1.52.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.90.0", - "@segment/browser-destination-runtime": "^1.20.0" + "@segment/actions-core": "^3.121.0", + "@segment/browser-destination-runtime": "^1.50.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/koala/src/identifyVisitor/__tests__/index.test.ts b/packages/browser-destinations/destinations/koala/src/identifyVisitor/__tests__/index.test.ts index 87b79b23c9..213b90176b 100644 --- a/packages/browser-destinations/destinations/koala/src/identifyVisitor/__tests__/index.test.ts +++ b/packages/browser-destinations/destinations/koala/src/identifyVisitor/__tests__/index.test.ts @@ -64,6 +64,9 @@ describe('Koala.identifyVisitor', () => { expect(window.ko.identify).toHaveBeenCalledWith( expect.objectContaining({ name: 'Matt' + }), + expect.objectContaining({ + source: 'segment-browser' }) ) }) diff --git a/packages/browser-destinations/destinations/koala/src/identifyVisitor/index.ts b/packages/browser-destinations/destinations/koala/src/identifyVisitor/index.ts index 390b15cbd7..e334b82075 100644 --- a/packages/browser-destinations/destinations/koala/src/identifyVisitor/index.ts +++ b/packages/browser-destinations/destinations/koala/src/identifyVisitor/index.ts @@ -20,7 +20,7 @@ const action: BrowserActionDefinition = { }, perform: (koala, { payload }) => { if (payload?.traits) { - return koala.identify(payload.traits) + return koala.identify(payload.traits, { source: 'segment-browser' }) } } } diff --git a/packages/browser-destinations/destinations/koala/src/types.ts b/packages/browser-destinations/destinations/koala/src/types.ts index ef1affa74b..f13839c938 100644 --- a/packages/browser-destinations/destinations/koala/src/types.ts +++ b/packages/browser-destinations/destinations/koala/src/types.ts @@ -1,7 +1,7 @@ export interface Koala { ready: (fn?: () => Promise | unknown) => Promise track: (event: string, data?: { [key: string]: unknown }) => Promise - identify: (traits: Record) => Promise + identify: (traits: Record, options?: { source?: string }) => Promise } export interface KoalaSDK { diff --git a/packages/browser-destinations/destinations/logrocket/package.json b/packages/browser-destinations/destinations/logrocket/package.json index e45d964d73..6b5de92a03 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.21.0", + "version": "1.51.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.90.0", - "@segment/browser-destination-runtime": "^1.20.0", + "@segment/actions-core": "^3.121.0", + "@segment/browser-destination-runtime": "^1.50.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 dc7e9caaa9..3fbbddbd67 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.9.0", + "version": "1.40.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,7 +15,7 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/browser-destination-runtime": "^1.20.0" + "@segment/browser-destination-runtime": "^1.50.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 8fd9020ac7..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 @@ -13,4 +13,12 @@ export interface Settings { * If you are using Pendo's CNAME feature, this will update your Pendo install snippet with your content host. */ cnameContentHost?: string + /** + * 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. + */ + disableUserTraitsOnLoad?: boolean + /** + * 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. + */ + 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 ad8a393b06..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' } } } @@ -70,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 ee9e7736ae..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 @@ -6,7 +6,7 @@ export interface Payload { */ visitorId: string /** - * Pendo Account ID + * 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 /** 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 9af7ca9910..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 @@ -2,6 +2,7 @@ import type { BrowserActionDefinition } from '@segment/browser-destination-runti import type { Settings } from '../generated-types' import type { Payload } from './generated-types' import type { PendoSDK, PendoOptions } from '../types' +import { removeNestedObject, AnyObject, getSubstringDifference } from '../utils' const action: BrowserActionDefinition = { title: 'Send Group Event', @@ -21,11 +22,12 @@ const action: BrowserActionDefinition = { }, accountId: { label: 'Account ID', - description: 'Pendo Account ID', + 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' }, - readOnly: true + readOnly: false }, accountData: { label: 'Account Metadata', @@ -33,7 +35,7 @@ const action: BrowserActionDefinition = { type: 'object', required: false, default: { '@path': '$.traits' }, - readOnly: true + readOnly: false }, parentAccountData: { label: 'Parent Account Metadata', @@ -52,22 +54,40 @@ const action: BrowserActionDefinition = { required: false } }, - perform: (pendo, event) => { - const payload: PendoOptions = { + perform: (pendo, { mapping, payload }) => { + // remove parentAccountData field data from the accountData if the paths overlap + + type pathMapping = { + '@path': string + } + + 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 + } + + const pendoPayload: PendoOptions = { visitor: { - id: event.payload.visitorId + id: payload.visitorId }, account: { - ...event.payload.accountData, - id: event.payload.accountId + ...accountData, + id: payload.accountId } } - if (event.payload.parentAccountData) { - payload.parentAccount = event.payload.parentAccountData + if (payload.parentAccountData) { + pendoPayload.parentAccount = payload.parentAccountData } - pendo.identify(payload) + pendo.identify(pendoPayload) } } 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 ab7506a506..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 @@ -26,7 +26,7 @@ const action: BrowserActionDefinition = { default: { '@path': '$.traits' }, - readOnly: true + readOnly: false } }, perform: (pendo, event) => { 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 c92a02f57b..79e902ebbb 100644 --- a/packages/browser-destinations/destinations/pendo-web-actions/src/index.ts +++ b/packages/browser-destinations/destinations/pendo-web-actions/src/index.ts @@ -4,6 +4,7 @@ import { browserDestination } from '@segment/browser-destination-runtime/shim' import { loadPendo } from './loadScript' 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' @@ -48,6 +49,22 @@ export const destination: BrowserDestinationDefinition = { "If you are using Pendo's CNAME feature, this will update your Pendo install snippet with your content host.", type: 'string', 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 } }, @@ -77,10 +94,10 @@ export const destination: BrowserDestinationDefinition = { const options: PendoOptions = { visitor: { - ...analytics.user().traits(), + ...(!settings.disableUserTraitsOnLoad ? analytics.user().traits() : {}), id: visitorId }, - ...(accountId + ...(accountId && !settings.disableGroupIdAndTraitsOnLoad ? { account: { ...analytics.group().traits(), @@ -98,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/track/index.ts b/packages/browser-destinations/destinations/pendo-web-actions/src/track/index.ts index 3c4ac47bcf..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 @@ -17,7 +17,7 @@ const action: BrowserActionDefinition = { default: { '@path': '$.event' }, - readOnly: true + readOnly: false }, metadata: { label: 'Metadata', @@ -26,7 +26,7 @@ const action: BrowserActionDefinition = { default: { '@path': '$.properties' }, - readOnly: true + readOnly: false } }, perform: (pendo, { payload }) => { 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 7a8f8a5086..29bd8cc75a 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.21.0", + "version": "1.51.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.90.0", - "@segment/browser-destination-runtime": "^1.20.0" + "@segment/actions-core": "^3.121.0", + "@segment/browser-destination-runtime": "^1.50.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 index b780826a2c..b89dd05b88 100644 --- a/packages/browser-destinations/destinations/replaybird/README.md +++ b/packages/browser-destinations/destinations/replaybird/README.md @@ -6,7 +6,7 @@ The Replaybird 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/replaybird/package.json b/packages/browser-destinations/destinations/replaybird/package.json index 074957e6fc..0b3ffbbb1e 100644 --- a/packages/browser-destinations/destinations/replaybird/package.json +++ b/packages/browser-destinations/destinations/replaybird/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-replaybird", - "version": "1.2.0", + "version": "1.32.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,7 +15,7 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/browser-destination-runtime": "^1.20.0" + "@segment/browser-destination-runtime": "^1.50.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/ripe/package.json b/packages/browser-destinations/destinations/ripe/package.json index 3325ae5a1e..83852f8066 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.21.0", + "version": "1.51.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.90.0", - "@segment/browser-destination-runtime": "^1.20.0" + "@segment/actions-core": "^3.121.0", + "@segment/browser-destination-runtime": "^1.50.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/ripe/src/group/__tests__/index.test.ts b/packages/browser-destinations/destinations/ripe/src/group/__tests__/index.test.ts index 35e8d0d66a..9bd452e944 100644 --- a/packages/browser-destinations/destinations/ripe/src/group/__tests__/index.test.ts +++ b/packages/browser-destinations/destinations/ripe/src/group/__tests__/index.test.ts @@ -31,6 +31,9 @@ const subscriptions: Subscription[] = [ }, traits: { '@path': '$.traits' + }, + context: { + '@path': '$.context' } } } @@ -61,6 +64,9 @@ describe('Ripe.group', () => { groupId: 'groupId1', traits: { is_new_group: true + }, + context: { + ip: '1.2.3.4' } }) ) @@ -75,6 +81,9 @@ describe('Ripe.group', () => { groupId: 'groupId1', traits: { is_new_group: true + }, + context: { + ip: '1.2.3.4' } } }) @@ -85,7 +94,10 @@ describe('Ripe.group', () => { anonymousId: 'anonId1', userId: undefined, groupId: 'groupId1', - traits: expect.objectContaining({ is_new_group: true }) + traits: expect.objectContaining({ is_new_group: true }), + context: { + ip: '1.2.3.4' + } }) }) }) diff --git a/packages/browser-destinations/destinations/ripe/src/group/generated-types.ts b/packages/browser-destinations/destinations/ripe/src/group/generated-types.ts index fddde614a6..09a3766387 100644 --- a/packages/browser-destinations/destinations/ripe/src/group/generated-types.ts +++ b/packages/browser-destinations/destinations/ripe/src/group/generated-types.ts @@ -23,4 +23,10 @@ export interface Payload { * The Segment messageId */ messageId?: string + /** + * Device context + */ + context?: { + [k: string]: unknown + } } diff --git a/packages/browser-destinations/destinations/ripe/src/group/index.ts b/packages/browser-destinations/destinations/ripe/src/group/index.ts index c4fc0bc38b..599d8c3e7d 100644 --- a/packages/browser-destinations/destinations/ripe/src/group/index.ts +++ b/packages/browser-destinations/destinations/ripe/src/group/index.ts @@ -45,6 +45,13 @@ const action: BrowserActionDefinition = { description: 'The Segment messageId', label: 'MessageId', default: { '@path': '$.messageId' } + }, + context: { + type: 'object', + label: 'Context', + description: 'Device context', + required: false, + default: { '@path': '$.context' } } }, perform: async (ripe, { payload }) => { @@ -53,7 +60,8 @@ const action: BrowserActionDefinition = { anonymousId: payload.anonymousId, userId: payload.userId, groupId: payload.groupId, - traits: payload.traits + traits: payload.traits, + context: payload.context }) } } diff --git a/packages/browser-destinations/destinations/ripe/src/identify/__tests__/index.test.ts b/packages/browser-destinations/destinations/ripe/src/identify/__tests__/index.test.ts index e646526266..662e544f84 100644 --- a/packages/browser-destinations/destinations/ripe/src/identify/__tests__/index.test.ts +++ b/packages/browser-destinations/destinations/ripe/src/identify/__tests__/index.test.ts @@ -31,6 +31,9 @@ const subscriptions: Subscription[] = [ }, traits: { '@path': '$.traits' + }, + context: { + '@path': '$.context' } } } @@ -61,6 +64,9 @@ describe('Ripe.identify', () => { userId: 'userId', traits: { name: 'Simon' + }, + context: { + ip: '1.2.3.4' } }) ) @@ -75,6 +81,9 @@ describe('Ripe.identify', () => { groupId: undefined, traits: { name: 'Simon' + }, + context: { + ip: '1.2.3.4' } } }) @@ -85,7 +94,10 @@ describe('Ripe.identify', () => { userId: expect.stringMatching('userId'), anonymousId: 'anonymousId', groupId: undefined, - traits: expect.objectContaining({ name: 'Simon' }) + traits: expect.objectContaining({ name: 'Simon' }), + context: { + ip: '1.2.3.4' + } }) }) }) diff --git a/packages/browser-destinations/destinations/ripe/src/identify/generated-types.ts b/packages/browser-destinations/destinations/ripe/src/identify/generated-types.ts index aafc8a5b93..b5c71dac14 100644 --- a/packages/browser-destinations/destinations/ripe/src/identify/generated-types.ts +++ b/packages/browser-destinations/destinations/ripe/src/identify/generated-types.ts @@ -23,4 +23,10 @@ export interface Payload { * The Segment messageId */ messageId?: string + /** + * Device context + */ + context?: { + [k: string]: unknown + } } diff --git a/packages/browser-destinations/destinations/ripe/src/identify/index.ts b/packages/browser-destinations/destinations/ripe/src/identify/index.ts index a4da201d54..380633e058 100644 --- a/packages/browser-destinations/destinations/ripe/src/identify/index.ts +++ b/packages/browser-destinations/destinations/ripe/src/identify/index.ts @@ -45,6 +45,13 @@ const action: BrowserActionDefinition = { description: 'The Segment messageId', label: 'MessageId', default: { '@path': '$.messageId' } + }, + context: { + type: 'object', + label: 'Context', + description: 'Device context', + required: false, + default: { '@path': '$.context' } } }, perform: async (ripe, { payload }) => { @@ -53,7 +60,8 @@ const action: BrowserActionDefinition = { anonymousId: payload.anonymousId, userId: payload.userId, groupId: payload.groupId, - traits: payload.traits + traits: payload.traits, + context: payload.context }) } } diff --git a/packages/browser-destinations/destinations/ripe/src/page/__tests__/index.test.ts b/packages/browser-destinations/destinations/ripe/src/page/__tests__/index.test.ts index 35570448f0..25730a6945 100644 --- a/packages/browser-destinations/destinations/ripe/src/page/__tests__/index.test.ts +++ b/packages/browser-destinations/destinations/ripe/src/page/__tests__/index.test.ts @@ -37,6 +37,9 @@ const subscriptions: Subscription[] = [ }, properties: { '@path': '$.properties' + }, + context: { + '@path': '$.context' } } } @@ -68,6 +71,9 @@ describe('Ripe.page', () => { name: 'page2', properties: { previous: 'page1' + }, + context: { + ip: '1.2.3.4' } }) ) @@ -84,6 +90,9 @@ describe('Ripe.page', () => { name: 'page2', properties: { previous: 'page1' + }, + context: { + ip: '1.2.3.4' } } }) @@ -96,7 +105,10 @@ describe('Ripe.page', () => { anonymousId: 'anonymousId', category: 'main', name: 'page2', - properties: expect.objectContaining({ previous: 'page1' }) + properties: expect.objectContaining({ previous: 'page1' }), + context: { + ip: '1.2.3.4' + } }) }) }) diff --git a/packages/browser-destinations/destinations/ripe/src/page/generated-types.ts b/packages/browser-destinations/destinations/ripe/src/page/generated-types.ts index 88306f8cb7..6b8d340b8b 100644 --- a/packages/browser-destinations/destinations/ripe/src/page/generated-types.ts +++ b/packages/browser-destinations/destinations/ripe/src/page/generated-types.ts @@ -31,4 +31,10 @@ export interface Payload { * The Segment messageId */ messageId?: string + /** + * Device context + */ + context?: { + [k: string]: unknown + } } diff --git a/packages/browser-destinations/destinations/ripe/src/page/index.ts b/packages/browser-destinations/destinations/ripe/src/page/index.ts index b18e4c7f5c..46f626e1b1 100644 --- a/packages/browser-destinations/destinations/ripe/src/page/index.ts +++ b/packages/browser-destinations/destinations/ripe/src/page/index.ts @@ -71,6 +71,13 @@ const action: BrowserActionDefinition = { description: 'The Segment messageId', label: 'MessageId', default: { '@path': '$.messageId' } + }, + context: { + type: 'object', + label: 'Context', + description: 'Device context', + required: false, + default: { '@path': '$.context' } } }, perform: async (ripe, { payload }) => { @@ -81,7 +88,8 @@ const action: BrowserActionDefinition = { groupId: payload.groupId, category: payload.category, name: payload.name, - properties: payload.properties + properties: payload.properties, + context: payload.context }) } } diff --git a/packages/browser-destinations/destinations/ripe/src/track/__tests__/index.test.ts b/packages/browser-destinations/destinations/ripe/src/track/__tests__/index.test.ts index ed03c70f73..5c86e406c2 100644 --- a/packages/browser-destinations/destinations/ripe/src/track/__tests__/index.test.ts +++ b/packages/browser-destinations/destinations/ripe/src/track/__tests__/index.test.ts @@ -34,6 +34,9 @@ const subscriptions: Subscription[] = [ }, properties: { '@path': '$.properties' + }, + context: { + '@path': '$.context' } } } @@ -64,6 +67,9 @@ describe('Ripe.track', () => { event: 'Form Submitted', properties: { is_new_lead: true + }, + context: { + ip: '1.2.3.4' } }) ) @@ -79,6 +85,9 @@ describe('Ripe.track', () => { event: 'Form Submitted', properties: { is_new_lead: true + }, + context: { + ip: '1.2.3.4' } } }) @@ -90,7 +99,10 @@ describe('Ripe.track', () => { userId: undefined, groupId: undefined, event: 'Form Submitted', - properties: expect.objectContaining({ is_new_lead: true }) + properties: expect.objectContaining({ is_new_lead: true }), + context: { + ip: '1.2.3.4' + } }) }) }) diff --git a/packages/browser-destinations/destinations/ripe/src/track/generated-types.ts b/packages/browser-destinations/destinations/ripe/src/track/generated-types.ts index 8e79d8bb6f..ed4daab8fe 100644 --- a/packages/browser-destinations/destinations/ripe/src/track/generated-types.ts +++ b/packages/browser-destinations/destinations/ripe/src/track/generated-types.ts @@ -27,4 +27,10 @@ export interface Payload { * The Segment messageId */ messageId?: string + /** + * Device context + */ + context?: { + [k: string]: unknown + } } diff --git a/packages/browser-destinations/destinations/ripe/src/track/index.ts b/packages/browser-destinations/destinations/ripe/src/track/index.ts index cee99bb853..ab4c6906c1 100644 --- a/packages/browser-destinations/destinations/ripe/src/track/index.ts +++ b/packages/browser-destinations/destinations/ripe/src/track/index.ts @@ -52,6 +52,13 @@ const action: BrowserActionDefinition = { description: 'The Segment messageId', label: 'MessageId', default: { '@path': '$.messageId' } + }, + context: { + type: 'object', + label: 'Context', + description: 'Device context', + required: false, + default: { '@path': '$.context' } } }, perform: async (ripe, { payload }) => { @@ -62,7 +69,8 @@ const action: BrowserActionDefinition = { userId: payload.userId, groupId: payload.groupId, event: payload.event, - properties: payload.properties + properties: payload.properties, + context: payload.context }) } } diff --git a/packages/browser-destinations/destinations/ripe/src/types.ts b/packages/browser-destinations/destinations/ripe/src/types.ts index 5638beecb5..a09120f234 100644 --- a/packages/browser-destinations/destinations/ripe/src/types.ts +++ b/packages/browser-destinations/destinations/ripe/src/types.ts @@ -11,6 +11,7 @@ export interface RipeSDK { userId?: string | null groupId: string | null traits?: Record + context?: Record }) => Promise identify: ({ messageId, @@ -24,6 +25,7 @@ export interface RipeSDK { userId?: string | null groupId?: string | null traits?: Record + context?: Record }) => Promise init: (apiKey: string) => Promise page: ({ @@ -42,6 +44,7 @@ export interface RipeSDK { category?: string name?: string properties?: Record + context?: Record }) => Promise track: ({ messageId, @@ -57,5 +60,6 @@ export interface RipeSDK { groupId?: string | null event: string properties?: Record + context?: Record }) => Promise } 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 1e9874f307..d3af4181ab 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.10.0", + "version": "1.40.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.90.0", - "@segment/browser-destination-runtime": "^1.20.0" + "@segment/actions-core": "^3.121.0", + "@segment/browser-destination-runtime": "^1.50.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 05d4673f75..33dea429b2 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.21.0", + "version": "1.52.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.90.0", - "@segment/browser-destination-runtime": "^1.20.0" + "@segment/actions-core": "^3.121.0", + "@segment/browser-destination-runtime": "^1.50.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/screeb/src/__tests__/index.test.ts b/packages/browser-destinations/destinations/screeb/src/__tests__/index.test.ts index 3e7403822d..1bc72e7bc3 100644 --- a/packages/browser-destinations/destinations/screeb/src/__tests__/index.test.ts +++ b/packages/browser-destinations/destinations/screeb/src/__tests__/index.test.ts @@ -36,6 +36,6 @@ describe('Screeb initialization', () => { await event.load(Context.system(), {} as Analytics) expect(destination.initialize).toHaveBeenCalled() - expect(window.$screeb.q).toStrictEqual([['init', 'fake-website-id']]) + expect(window.$screeb.q).toStrictEqual([['init', 'fake-website-id', { "identity": {"id": null}} ]]) }) }) diff --git a/packages/browser-destinations/destinations/screeb/src/alias/__tests__/index.test.ts b/packages/browser-destinations/destinations/screeb/src/alias/__tests__/index.test.ts index e578ee41fc..b986f5c531 100644 --- a/packages/browser-destinations/destinations/screeb/src/alias/__tests__/index.test.ts +++ b/packages/browser-destinations/destinations/screeb/src/alias/__tests__/index.test.ts @@ -56,7 +56,7 @@ describe('alias', () => { ) expect(window.$screeb.q).toStrictEqual([ - ['init', 'fake-website-id'], + ['init', 'fake-website-id', { "identity": {"id": null}}], ['identity', 'user-id'] ]) }) @@ -86,7 +86,7 @@ describe('alias', () => { ) expect(window.$screeb.q).toStrictEqual([ - ['init', 'fake-website-id'], + ['init', 'fake-website-id', {"identity": {"id": null}}], ['identity', 'anonymous-id'] ]) }) diff --git a/packages/browser-destinations/destinations/screeb/src/group/__tests__/index.test.ts b/packages/browser-destinations/destinations/screeb/src/group/__tests__/index.test.ts index 9509f05e18..5774da6a81 100644 --- a/packages/browser-destinations/destinations/screeb/src/group/__tests__/index.test.ts +++ b/packages/browser-destinations/destinations/screeb/src/group/__tests__/index.test.ts @@ -63,7 +63,7 @@ describe('group', () => { ) expect(window.$screeb.q).toStrictEqual([ - ['init', 'fake-website-id'], + ['init', 'fake-website-id', { "identity": {"id": null}}], ['identity.group.assign', undefined, 'group-name', { plan: 'free' }] ]) }) @@ -102,7 +102,7 @@ describe('group', () => { ) expect(window.$screeb.q).toStrictEqual([ - ['init', 'fake-website-id'], + ['init', 'fake-website-id', { "identity": {"id": null}}], ['identity.group.assign', 'cohort', 'group-name', { plan: 'free', group_type: 'cohort' }] ]) }) diff --git a/packages/browser-destinations/destinations/screeb/src/identify/__tests__/index.test.ts b/packages/browser-destinations/destinations/screeb/src/identify/__tests__/index.test.ts index ae9776520f..21fc91ce2d 100644 --- a/packages/browser-destinations/destinations/screeb/src/identify/__tests__/index.test.ts +++ b/packages/browser-destinations/destinations/screeb/src/identify/__tests__/index.test.ts @@ -69,7 +69,7 @@ describe('identify', () => { ) expect(window.$screeb.q).toStrictEqual([ - ['init', 'fake-website-id'], + ['init', 'fake-website-id', { "identity": {"id": null}}], ['identity', 'user-id', { firstname: 'Frida', lastname: 'Khalo', email: 'frida.khalo@screeb.app' }] ]) }) @@ -109,7 +109,7 @@ describe('identify', () => { ) expect(window.$screeb.q).toStrictEqual([ - ['init', 'fake-website-id'], + ['init', 'fake-website-id', { "identity": {"id": null}}], ['identity', 'anonymous-id', { firstname: 'Frida', lastname: 'Khalo', email: 'frida.khalo@screeb.app' }] ]) }) diff --git a/packages/browser-destinations/destinations/screeb/src/index.ts b/packages/browser-destinations/destinations/screeb/src/index.ts index d65e4347ac..c7e47fe1dd 100644 --- a/packages/browser-destinations/destinations/screeb/src/index.ts +++ b/packages/browser-destinations/destinations/screeb/src/index.ts @@ -7,6 +7,7 @@ import identify from './identify' import track from './track' import group from './group' import alias from './alias' +import { ID } from '@segment/analytics-next' declare global { interface Window { @@ -59,7 +60,7 @@ export const destination: BrowserDestinationDefinition = { } ], - initialize: async ({ settings }, deps) => { + initialize: async ({ settings, analytics }, deps) => { const preloadFunction = function (...args: unknown[]) { if (window.$screeb.q) { window.$screeb.q.push(args) @@ -71,7 +72,13 @@ export const destination: BrowserDestinationDefinition = { await deps.loadScript('https://t.screeb.app/tag.js') await deps.resolveWhen(() => window.$screeb !== preloadFunction, 500) - window.$screeb('init', settings.websiteId) + + let visitorId: ID = null + if (analytics && typeof analytics.user === 'function' && analytics.user().id) { + visitorId = analytics.user().id() + } + + window.$screeb('init', settings.websiteId, { identity: { id: visitorId } }) return window.$screeb }, diff --git a/packages/browser-destinations/destinations/screeb/src/track/__tests__/index.test.ts b/packages/browser-destinations/destinations/screeb/src/track/__tests__/index.test.ts index 5bfa38157c..7f9898ae70 100644 --- a/packages/browser-destinations/destinations/screeb/src/track/__tests__/index.test.ts +++ b/packages/browser-destinations/destinations/screeb/src/track/__tests__/index.test.ts @@ -62,7 +62,7 @@ describe('track', () => { ) expect(window.$screeb.q).toStrictEqual([ - ['init', 'fake-website-id'], + ['init', 'fake-website-id', { "identity": {"id": null}}], ['event.track', 'event-name', { prop1: 1, prop2: 'pickle sandwish' }] ]) }) diff --git a/packages/browser-destinations/destinations/segment-utilities-web/package.json b/packages/browser-destinations/destinations/segment-utilities-web/package.json index d23327099a..8faa1a8c2c 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.21.0", + "version": "1.51.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,7 +15,7 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/browser-destination-runtime": "^1.20.0" + "@segment/browser-destination-runtime": "^1.50.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 index e52ffaa37f..251004a21e 100644 --- a/packages/browser-destinations/destinations/snap-plugins/README.md +++ b/packages/browser-destinations/destinations/snap-plugins/README.md @@ -6,7 +6,7 @@ The Snap Browser Plugins browser action destination for use with @segment/analyt 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/snap-plugins/package.json b/packages/browser-destinations/destinations/snap-plugins/package.json index 85502c497c..c165e10a76 100644 --- a/packages/browser-destinations/destinations/snap-plugins/package.json +++ b/packages/browser-destinations/destinations/snap-plugins/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-snap-plugins", - "version": "1.2.0", + "version": "1.32.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,7 +15,7 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/browser-destination-runtime": "^1.20.0" + "@segment/browser-destination-runtime": "^1.50.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/sprig-web/package.json b/packages/browser-destinations/destinations/sprig-web/package.json index 3447a47da6..6cf3d8a61c 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.21.0", + "version": "1.51.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.90.0", - "@segment/browser-destination-runtime": "^1.20.0" + "@segment/actions-core": "^3.121.0", + "@segment/browser-destination-runtime": "^1.50.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 96fef383b9..911544c3eb 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.21.0", + "version": "1.53.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.90.0", - "@segment/browser-destination-runtime": "^1.20.0" + "@segment/actions-core": "^3.121.0", + "@segment/browser-destination-runtime": "^1.50.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/stackadapt/src/index.ts b/packages/browser-destinations/destinations/stackadapt/src/index.ts index f4f6929ec9..18c00c1d01 100644 --- a/packages/browser-destinations/destinations/stackadapt/src/index.ts +++ b/packages/browser-destinations/destinations/stackadapt/src/index.ts @@ -14,7 +14,7 @@ declare global { } export const destination: BrowserDestinationDefinition = { - name: 'StackAdapt (Actions)', + name: 'StackAdapt Pixel', slug: 'actions-stackadapt', mode: 'device', presets: [ 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..0122f6da04 --- /dev/null +++ b/packages/browser-destinations/destinations/survicate/package.json @@ -0,0 +1,23 @@ +{ + "name": "@segment/analytics-browser-actions-survicate", + "version": "1.27.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.50.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 e6c25fe35b..5e5898fabc 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.18.0", + "version": "1.51.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.90.0", - "@segment/browser-destination-runtime": "^1.20.0" + "@segment/actions-core": "^3.121.0", + "@segment/browser-destination-runtime": "^1.50.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/tiktok-pixel/src/common_fields.ts b/packages/browser-destinations/destinations/tiktok-pixel/src/common_fields.ts new file mode 100644 index 0000000000..e4111ed6ef --- /dev/null +++ b/packages/browser-destinations/destinations/tiktok-pixel/src/common_fields.ts @@ -0,0 +1,242 @@ +import { InputField } from '@segment/actions-core' + +export const commonFields: Record = { + event: { + label: 'Event Name', + type: 'string', + required: true, + description: + 'Conversion event name. Please refer to the "Supported Web Events" section on in TikTok’s [Pixel SDK documentation](https://business-api.tiktok.com/portal/docs?id=1739585696931842) for accepted event names.' + }, + event_id: { + label: 'Event ID', + type: 'string', + description: 'Any hashed ID that can identify a unique user/session.', + default: { + '@path': '$.messageId' + } + }, + phone_number: { + label: 'Phone Number', + description: + 'A single phone number in E.164 standard format. TikTok Pixel will hash this value before sending to TikTok. e.g. +14150000000. Segment will hash this value before sending to TikTok.', + type: 'string', + multiple: true, + default: { + '@if': { + exists: { '@path': '$.properties.phone' }, + then: { '@path': '$.properties.phone' }, + else: { '@path': '$.context.traits.phone' } + } + } + }, + email: { + label: 'Email', + description: 'A single email address. TikTok Pixel will be hash this value before sending to TikTok.', + type: 'string', + multiple: true, + default: { + '@if': { + exists: { '@path': '$.properties.email' }, + then: { '@path': '$.properties.email' }, + else: { '@path': '$.context.traits.email' } + } + } + }, + first_name: { + label: 'First Name', + description: + 'The first name of the customer. The name should be in lowercase without any punctuation. Special characters are allowed.', + type: 'string', + default: { + '@if': { + exists: { '@path': '$.properties.first_name' }, + then: { '@path': '$.properties.first_name' }, + else: { '@path': '$.context.traits.first_name' } + } + } + }, + last_name: { + label: 'Last Name', + description: + 'The last name of the customer. The name should be in lowercase without any punctuation. Special characters are allowed.', + type: 'string', + default: { + '@if': { + exists: { '@path': '$.properties.last_name' }, + then: { '@path': '$.properties.last_name' }, + else: { '@path': '$.context.traits.last_name' } + } + } + }, + address: { + label: 'Address', + type: 'object', + description: 'The address of the customer.', + additionalProperties: false, + properties: { + city: { + label: 'City', + type: 'string', + description: "The customer's city." + }, + country: { + label: 'Country', + type: 'string', + description: "The customer's country." + }, + zip_code: { + label: 'Zip Code', + type: 'string', + description: "The customer's Zip Code." + }, + state: { + label: 'State', + type: 'string', + description: "The customer's State." + } + }, + default: { + city: { + '@if': { + exists: { '@path': '$.properties.address.city' }, + then: { '@path': '$.properties.address.city' }, + else: { '@path': '$.context.traits.address.city' } + } + }, + country: { + '@if': { + exists: { '@path': '$.properties.address.country' }, + then: { '@path': '$.properties.address.country' }, + else: { '@path': '$.context.traits.address.country' } + } + }, + zip_code: { + '@if': { + exists: { '@path': '$.properties.address.postal_code' }, + then: { '@path': '$.properties.address.postal_code' }, + else: { '@path': '$.context.traits.address.postal_code' } + } + }, + state: { + '@if': { + exists: { '@path': '$.properties.address.state' }, + then: { '@path': '$.properties.address.state' }, + else: { '@path': '$.context.traits.address.state' } + } + } + } + }, + order_id: { + label: 'Order ID', + type: 'string', + description: 'Order ID of the transaction.', + default: { + '@path': '$.properties.order_id' + } + }, + shop_id: { + label: 'Shop ID', + type: 'string', + description: 'Shop ID of the transaction.', + default: { + '@path': '$.properties.shop_id' + } + }, + external_id: { + label: 'External ID', + description: + 'Uniquely identifies the user who triggered the conversion event. TikTok Pixel will hash this value before sending to TikTok.', + type: 'string', + multiple: true, + default: { + '@if': { + exists: { '@path': '$.userId' }, + then: { '@path': '$.userId' }, + else: { '@path': '$.anonymousId' } + } + } + }, + contents: { + label: 'Contents', + type: 'object', + multiple: true, + description: 'Related item details for the event.', + properties: { + price: { + label: 'Price', + description: 'Price of the item.', + type: 'number' + }, + quantity: { + label: 'Quantity', + description: 'Number of items.', + type: 'number' + }, + content_category: { + label: 'Content Category', + description: 'Category of the product item.', + type: 'string' + }, + content_id: { + label: 'Content ID', + description: 'ID of the product item.', + type: 'string' + }, + content_name: { + label: 'Content Name', + description: 'Name of the product item.', + type: 'string' + }, + brand: { + label: 'Brand', + description: 'Brand name of the product item.', + type: 'string' + } + } + }, + content_type: { + label: 'Content Type', + description: + 'Type of the product item. When the `content_id` in the `Contents` field is specified as a `sku_id`, set this field to `product`. When the `content_id` in the `Contents` field is specified as an `item_group_id`, set this field to `product_group`.', + type: 'string', + choices: [ + { label: 'product', value: 'product' }, + { label: 'product_group', value: 'product_group' } + ], + default: 'product' + }, + currency: { + label: 'Currency', + type: 'string', + description: 'Currency for the value specified as ISO 4217 code.', + default: { + '@path': '$.properties.currency' + } + }, + value: { + label: 'Value', + type: 'number', + description: 'Value of the order or items sold.', + default: { + '@if': { + exists: { '@path': '$.properties.value' }, + then: { '@path': '$.properties.value' }, + else: { '@path': '$.properties.revenue' } + } + } + }, + description: { + label: 'Description', + type: 'string', + description: 'A string description of the web event.' + }, + query: { + label: 'Query', + type: 'string', + description: 'The text string that was searched for.', + default: { + '@path': '$.properties.query' + } + } +} diff --git a/packages/browser-destinations/destinations/tiktok-pixel/src/reportWebEvent/formatter.ts b/packages/browser-destinations/destinations/tiktok-pixel/src/formatter.ts similarity index 56% rename from packages/browser-destinations/destinations/tiktok-pixel/src/reportWebEvent/formatter.ts rename to packages/browser-destinations/destinations/tiktok-pixel/src/formatter.ts index e7f9a54e96..9af5263556 100644 --- a/packages/browser-destinations/destinations/tiktok-pixel/src/reportWebEvent/formatter.ts +++ b/packages/browser-destinations/destinations/tiktok-pixel/src/formatter.ts @@ -16,3 +16,22 @@ export function formatPhone(phone: string | undefined): string | undefined { formattedPhone = formattedPhone.substring(0, 15) return formattedPhone } + +export function handleArrayInput(mightBeArray: string[] | string | undefined): string { + if (typeof mightBeArray === 'string') return mightBeArray + if (typeof mightBeArray === 'undefined') return '' + if (Array.isArray(mightBeArray)) { + return mightBeArray.length > 0 ? mightBeArray[0] : '' + } + return '' +} + +export function formatString(str: string | undefined | null): string { + if (!str) return '' + return str.replace(/\s/g, '').toLowerCase() +} + +export function formatAddress(address: string | undefined | null): string { + if (!address) return '' + return address.replace(/[^A-Za-z0-9]/g, '').toLowerCase() +} 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 608ad2bb52..beaedeebe4 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,11 @@ export interface Settings { */ pixelCode: string /** - * 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. + * In order to help facilitate advertiser's compliance with the right to opt-out of sale and sharing of personal data under certain U.S. state privacy laws, TikTok offers a Limited Data Use ("LDU") feature. For more information, please refer to TikTok's [documentation page](https://business-api.tiktok.com/portal/docs?id=1770092377990145). + */ + ldu?: boolean + /** + * Deprecated. Please do not provide any value. */ useExistingPixel?: boolean } diff --git a/packages/browser-destinations/destinations/tiktok-pixel/src/identify/__tests__/index.test.ts b/packages/browser-destinations/destinations/tiktok-pixel/src/identify/__tests__/index.test.ts new file mode 100644 index 0000000000..6de3ae945a --- /dev/null +++ b/packages/browser-destinations/destinations/tiktok-pixel/src/identify/__tests__/index.test.ts @@ -0,0 +1,110 @@ +import { Analytics, Context } from '@segment/analytics-next' +import { Subscription } from '@segment/browser-destination-runtime' +import TikTokDestination, { destination } from '../../index' +import { TikTokPixel } from '../../types' + +describe('TikTokPixel.reportWebEvent', () => { + const settings = { + pixelCode: '1234', + useExistingPixel: false + } + + let mockTtp: TikTokPixel + let reportWebEvent: any + beforeEach(async () => { + jest.restoreAllMocks() + jest.spyOn(destination, 'initialize').mockImplementation(() => { + mockTtp = { + page: jest.fn(), + identify: jest.fn(), + track: jest.fn(), + instance: jest.fn(() => mockTtp) + } + return Promise.resolve(mockTtp) + }) + }) + + test('fires identify with PII', async () => { + const subscriptions: Subscription[] = [ + { + partnerAction: 'identify', + name: 'Identify', + enabled: true, + subscribe: 'type = "identify"', + mapping: { + event_id: { + '@path': '$.messageId' + }, + anonymousId: { + '@path': '$.anonymousId' + }, + external_id: { + '@path': '$.userId' + }, + phone_number: { + '@path': '$.traits.phone' + }, + email: { + '@path': '$.traits.email' + }, + last_name: { + '@path': '$.traits.last_name' + }, + first_name: { + '@path': '$.traits.first_name' + }, + address: { + city: { + '@path': '$.traits.address.city' + }, + state: { + '@path': '$.traits.address.state' + }, + country: { + '@path': '$.traits.address.country' + } + } + } + } + ] + + const context = new Context({ + messageId: 'ajs-71f386523ee5dfa90c7d0fda28b6b5c6', + type: 'identify', + anonymousId: 'anonymousId', + userId: 'userId', + traits: { + last_name: 'lastName', + first_name: 'firstName', + email: 'aaa@aaa.com', + phone: '+12345678900', + address: { + city: 'city', + state: 'state', + country: 'country' + } + } + }) + + const [identifyEvent] = await TikTokDestination({ + ...settings, + subscriptions + }) + reportWebEvent = identifyEvent + + await reportWebEvent.load(Context.system(), {} as Analytics) + await reportWebEvent.identify?.(context) + + expect(mockTtp.identify).toHaveBeenCalledWith({ + city: 'city', + country: 'country', + email: 'aaa@aaa.com', + phone_number: '+12345678900', + external_id: 'userId', + first_name: 'firstname', + last_name: 'lastname', + state: 'state', + zip_code: '' + }) + }) +}) diff --git a/packages/browser-destinations/destinations/tiktok-pixel/src/identify/generated-types.ts b/packages/browser-destinations/destinations/tiktok-pixel/src/identify/generated-types.ts new file mode 100644 index 0000000000..ac49bf0542 --- /dev/null +++ b/packages/browser-destinations/destinations/tiktok-pixel/src/identify/generated-types.ts @@ -0,0 +1,110 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * Conversion event name. Please refer to the "Supported Web Events" section on in TikTok’s [Pixel SDK documentation](https://business-api.tiktok.com/portal/docs?id=1739585696931842) for accepted event names. + */ + event: string + /** + * Any hashed ID that can identify a unique user/session. + */ + event_id?: string + /** + * A single phone number in E.164 standard format. TikTok Pixel will hash this value before sending to TikTok. e.g. +14150000000. Segment will hash this value before sending to TikTok. + */ + phone_number?: string[] + /** + * A single email address. TikTok Pixel will be hash this value before sending to TikTok. + */ + email?: string[] + /** + * The first name of the customer. The name should be in lowercase without any punctuation. Special characters are allowed. + */ + first_name?: string + /** + * The last name of the customer. The name should be in lowercase without any punctuation. Special characters are allowed. + */ + last_name?: string + /** + * The address of the customer. + */ + address?: { + /** + * The customer's city. + */ + city?: string + /** + * The customer's country. + */ + country?: string + /** + * The customer's Zip Code. + */ + zip_code?: string + /** + * The customer's State. + */ + state?: string + } + /** + * Order ID of the transaction. + */ + order_id?: string + /** + * Shop ID of the transaction. + */ + shop_id?: string + /** + * Uniquely identifies the user who triggered the conversion event. TikTok Pixel will hash this value before sending to TikTok. + */ + external_id?: string[] + /** + * Related item details for the event. + */ + contents?: { + /** + * Price of the item. + */ + price?: number + /** + * Number of items. + */ + quantity?: number + /** + * Category of the product item. + */ + content_category?: string + /** + * ID of the product item. + */ + content_id?: string + /** + * Name of the product item. + */ + content_name?: string + /** + * Brand name of the product item. + */ + brand?: string + }[] + /** + * Type of the product item. When the `content_id` in the `Contents` field is specified as a `sku_id`, set this field to `product`. When the `content_id` in the `Contents` field is specified as an `item_group_id`, set this field to `product_group`. + */ + content_type?: string + /** + * Currency for the value specified as ISO 4217 code. + */ + currency?: string + /** + * Value of the order or items sold. + */ + value?: number + /** + * A string description of the web event. + */ + description?: string + /** + * The text string that was searched for. + */ + query?: string +} diff --git a/packages/browser-destinations/destinations/tiktok-pixel/src/identify/index.ts b/packages/browser-destinations/destinations/tiktok-pixel/src/identify/index.ts new file mode 100644 index 0000000000..e73bae1363 --- /dev/null +++ b/packages/browser-destinations/destinations/tiktok-pixel/src/identify/index.ts @@ -0,0 +1,60 @@ +import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' +import { formatPhone, handleArrayInput, formatString, formatAddress } from '../formatter' +import { TikTokPixel } from '../types' +import { commonFields } from '../common_fields' + +// Change from unknown to the partner SDK types +const action: BrowserActionDefinition = { + title: 'Identify', + description: + 'Use a Segment identify() call to sent PII data to TikTok Pixel. Note that the PII information will be sent with the next track() call.', + defaultSubscription: 'type = "identify"', + platform: 'web', + fields: { + ...commonFields, + phone_number: { + ...commonFields.phone_number, + default: { '@path': '$.traits.phone' } + }, + email: { + ...commonFields.email, + default: { '@path': '$.traits.email' } + }, + first_name: { + ...commonFields.first_name, + default: { '@path': '$.traits.first_name' } + }, + last_name: { + ...commonFields.last_name, + default: { '@path': '$.traits.last_name' } + }, + address: { + ...commonFields.address, + default: { + city: { '@path': '$.traits.address.city' }, + country: { '@path': '$.traits.address.country' }, + zip_code: { '@path': '$.traits.address.postal_code' }, + state: { '@path': '$.traits.address.state' } + } + } + }, + perform: (ttq, { payload }) => { + if (payload.email || payload.phone_number || payload.external_id) { + ttq.identify({ + email: handleArrayInput(payload.email), + phone_number: formatPhone(handleArrayInput(payload.phone_number)), + external_id: handleArrayInput(payload.external_id), + first_name: formatString(payload.first_name), + last_name: formatString(payload.last_name), + city: formatAddress(payload.address?.city), + state: formatAddress(payload.address?.state), + country: formatAddress(payload.address?.country), + zip_code: formatString(payload.address?.zip_code) + }) + } + } +} + +export default action diff --git a/packages/browser-destinations/destinations/tiktok-pixel/src/index.ts b/packages/browser-destinations/destinations/tiktok-pixel/src/index.ts index 6c5c13a28b..0fb6f1f102 100644 --- a/packages/browser-destinations/destinations/tiktok-pixel/src/index.ts +++ b/packages/browser-destinations/destinations/tiktok-pixel/src/index.ts @@ -6,6 +6,8 @@ import { defaultValues } from '@segment/actions-core' import { TikTokPixel } from './types' import { initScript } from './init-script' +import identify from './identify' + declare global { interface Window { ttq: TikTokPixel @@ -19,11 +21,17 @@ const productProperties = { quantity: { '@path': '$.quantity' }, - content_type: { + content_category: { '@path': '$.category' }, content_id: { '@path': '$.product_id' + }, + content_name: { + '@path': '$.name' + }, + brand: { + '@path': '$.brand' } } @@ -57,9 +65,49 @@ export const destination: BrowserDestinationDefinition = slug: 'actions-tiktok-pixel', mode: 'device', presets: [ + { + name: 'Complete Payment', + subscribe: 'event = "Order Completed"', + partnerAction: 'reportWebEvent', + mapping: { + ...multiProductContents, + event: 'CompletePayment' + }, + type: 'automatic' + }, + { + name: 'Contact', + subscribe: 'event = "Callback Started"', + partnerAction: 'reportWebEvent', + mapping: { + ...defaultValues(reportWebEvent.fields), + event: 'Contact' + }, + type: 'automatic' + }, + { + name: 'Subscribe', + subscribe: 'event = "Subscription Created"', + partnerAction: 'reportWebEvent', + mapping: { + ...defaultValues(reportWebEvent.fields), + event: 'Subscribe' + }, + type: 'automatic' + }, + { + name: 'Submit Form', + subscribe: 'event = "Form Submitted"', + partnerAction: 'reportWebEvent', + mapping: { + ...defaultValues(reportWebEvent.fields), + event: 'SubmitForm' + }, + type: 'automatic' + }, { name: 'View Content', - subscribe: 'type="page"', + subscribe: 'event = "Product Viewed"', partnerAction: 'reportWebEvent', mapping: { ...singleProductContents, @@ -67,6 +115,16 @@ export const destination: BrowserDestinationDefinition = }, type: 'automatic' }, + { + name: 'Click Button', + subscribe: 'event = "Product Clicked"', + partnerAction: 'reportWebEvent', + mapping: { + ...singleProductContents, + event: 'ClickButton' + }, + type: 'automatic' + }, { name: 'Search', subscribe: 'event = "Products Searched"', @@ -119,13 +177,33 @@ export const destination: BrowserDestinationDefinition = }, { name: 'Place an Order', - subscribe: 'event = "Order Completed"', + subscribe: 'event = "Order Placed"', partnerAction: 'reportWebEvent', mapping: { ...multiProductContents, event: 'PlaceAnOrder' }, type: 'automatic' + }, + { + name: 'Download', + subscribe: 'event = "Download Link Clicked"', + partnerAction: 'reportWebEvent', + mapping: { + ...defaultValues(reportWebEvent.fields), + event: 'Download' + }, + type: 'automatic' + }, + { + name: 'Complete Registration', + subscribe: 'event = "Signed Up"', + partnerAction: 'reportWebEvent', + mapping: { + ...defaultValues(reportWebEvent.fields), + event: 'CompleteRegistration' + }, + type: 'automatic' } ], settings: { @@ -136,22 +214,31 @@ export const destination: BrowserDestinationDefinition = "Your TikTok Pixel ID. Please see TikTok's [Pixel documentation](https://ads.tiktok.com/marketing_api/docs?id=1739583652957185) for information on how to find this value.", required: true }, - useExistingPixel: { - label: 'Use Existing Pixel', + ldu: { + label: 'Limited Data Use', type: 'boolean', 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.' + 'In order to help facilitate advertiser\'s compliance with the right to opt-out of sale and sharing of personal data under certain U.S. state privacy laws, TikTok offers a Limited Data Use ("LDU") feature. For more information, please refer to TikTok\'s [documentation page](https://business-api.tiktok.com/portal/docs?id=1770092377990145).' + }, + useExistingPixel: { + // TODO: HOW TO DELETE (reusing will not include Segment Partner name) + label: '[Deprecated] Use Existing Pixel', + type: 'boolean', + default: false, + required: false, + description: 'Deprecated. Please do not provide any value.' } }, initialize: async ({ settings }, deps) => { if (!settings.useExistingPixel) { - initScript(settings.pixelCode) + initScript(settings) } await deps.resolveWhen(() => window.ttq != null, 100) return window.ttq }, actions: { - reportWebEvent + reportWebEvent, + identify } } diff --git a/packages/browser-destinations/destinations/tiktok-pixel/src/init-script.ts b/packages/browser-destinations/destinations/tiktok-pixel/src/init-script.ts index 8e3794db99..01995ea72f 100644 --- a/packages/browser-destinations/destinations/tiktok-pixel/src/init-script.ts +++ b/packages/browser-destinations/destinations/tiktok-pixel/src/init-script.ts @@ -1,6 +1,6 @@ /* eslint-disable */ // @ts-nocheck -export function initScript(pixelCode) { +export function initScript(settings) { !(function (w, d, t) { w.TiktokAnalyticsObject = t var ttq = (w[t] = w[t] || []) @@ -37,14 +37,17 @@ export function initScript(pixelCode) { (ttq._t = ttq._t || {}), (ttq._t[e] = +new Date()), (ttq._o = ttq._o || {}), - (ttq._o[e] = n || {}) + (ttq._o[e] = n || {}), + (ttq._partner = ttq._partner || 'Segment') var o = document.createElement('script') ;(o.type = 'text/javascript'), (o.async = !0), (o.src = i + '?sdkid=' + e + '&lib=' + t) var a = document.getElementsByTagName('script')[0] a.parentNode.insertBefore(o, a) }) - ttq.load(pixelCode) - ttq.page() + ttq.load(settings.pixelCode, { + limited_data_use: settings.ldu ? settings.ldu : false + }) + ttq.instance(settings.pixelCode).page() })(window, document, 'ttq') } 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 a7acd528c6..87df815623 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 @@ -17,7 +17,8 @@ describe('TikTokPixel.reportWebEvent', () => { mockTtp = { page: jest.fn(), identify: jest.fn(), - track: jest.fn() + track: jest.fn(), + instance: jest.fn(() => mockTtp) } return Promise.resolve(mockTtp) }) @@ -46,6 +47,23 @@ describe('TikTokPixel.reportWebEvent', () => { email: { '@path': '$.properties.email' }, + last_name: { + '@path': '$.context.traits.last_name' + }, + first_name: { + '@path': '$.context.traits.first_name' + }, + address: { + city: { + '@path': '$.context.traits.address.city' + }, + state: { + '@path': '$.context.traits.address.state' + }, + country: { + '@path': '$.context.traits.address.country' + } + }, groupId: { '@path': '$.groupId' }, @@ -91,6 +109,17 @@ describe('TikTokPixel.reportWebEvent', () => { anonymousId: 'anonymousId', userId: 'userId', event: 'Order Completed', + context: { + traits: { + last_name: 'lastName', + first_name: 'firstName', + address: { + city: 'city', + state: 'state', + country: 'country' + } + } + }, properties: { products: [ { @@ -109,8 +138,8 @@ describe('TikTokPixel.reportWebEvent', () => { query: 'test-query', value: 10, currency: 'USD', - phone: '+12345678900', - email: 'aaa@aaa.com', + phone: ['+12345678900'], + email: ['aaa@aaa.com'], description: 'test-description' } }) @@ -125,9 +154,15 @@ describe('TikTokPixel.reportWebEvent', () => { await reportWebEvent.track?.(context) expect(mockTtp.identify).toHaveBeenCalledWith({ + city: 'city', + country: 'country', email: 'aaa@aaa.com', phone_number: '+12345678900', - external_id: 'userId' + external_id: 'userId', + first_name: 'firstname', + last_name: 'lastname', + state: 'state', + zip_code: '' }) expect(mockTtp.track).toHaveBeenCalledWith( 'PlaceAnOrder', @@ -168,6 +203,23 @@ describe('TikTokPixel.reportWebEvent', () => { email: { '@path': '$.properties.email' }, + last_name: { + '@path': '$.context.traits.last_name' + }, + first_name: { + '@path': '$.context.traits.first_name' + }, + address: { + city: { + '@path': '$.context.traits.address.city' + }, + state: { + '@path': '$.context.traits.address.state' + }, + country: { + '@path': '$.context.traits.address.country' + } + }, groupId: { '@path': '$.groupId' }, @@ -213,6 +265,17 @@ describe('TikTokPixel.reportWebEvent', () => { anonymousId: 'anonymousId', userId: 'userId', event: 'Product Added', + context: { + traits: { + last_name: 'lastName', + first_name: 'firstName', + address: { + city: 'city', + state: 'state', + country: 'country' + } + } + }, properties: { product_id: '123', category: 'product', @@ -221,8 +284,8 @@ describe('TikTokPixel.reportWebEvent', () => { query: 'test-query', value: 10, currency: 'USD', - phone: '+12345678900', - email: 'aaa@aaa.com', + phone: ['+12345678900'], + email: ['aaa@aaa.com'], description: 'test-description' } }) @@ -237,9 +300,15 @@ describe('TikTokPixel.reportWebEvent', () => { await reportWebEvent.track?.(context) expect(mockTtp.identify).toHaveBeenCalledWith({ + city: 'city', + country: 'country', email: 'aaa@aaa.com', phone_number: '+12345678900', - external_id: 'userId' + external_id: 'userId', + first_name: 'firstname', + last_name: 'lastname', + state: 'state', + zip_code: '' }) expect(mockTtp.track).toHaveBeenCalledWith( 'AddToCart', @@ -277,6 +346,23 @@ describe('TikTokPixel.reportWebEvent', () => { email: { '@path': '$.properties.email' }, + last_name: { + '@path': '$.context.traits.last_name' + }, + first_name: { + '@path': '$.context.traits.first_name' + }, + address: { + city: { + '@path': '$.context.traits.address.city' + }, + state: { + '@path': '$.context.traits.address.state' + }, + country: { + '@path': '$.context.traits.address.country' + } + }, groupId: { '@path': '$.groupId' }, @@ -321,6 +407,17 @@ describe('TikTokPixel.reportWebEvent', () => { type: 'page', anonymousId: 'anonymousId', userId: 'userId', + context: { + traits: { + last_name: 'lastName', + first_name: 'firstName', + address: { + city: 'city', + state: 'state', + country: 'country' + } + } + }, properties: { product_id: '123', category: 'product', @@ -329,8 +426,8 @@ describe('TikTokPixel.reportWebEvent', () => { query: 'test-query', value: 10, currency: 'USD', - phone: '+12345678900', - email: 'aaa@aaa.com', + phone: ['+12345678900'], + email: ['aaa@aaa.com'], description: 'test-description' } }) @@ -345,9 +442,15 @@ describe('TikTokPixel.reportWebEvent', () => { await reportWebEvent.track?.(context) expect(mockTtp.identify).toHaveBeenCalledWith({ + city: 'city', + country: 'country', email: 'aaa@aaa.com', phone_number: '+12345678900', - external_id: 'userId' + external_id: 'userId', + first_name: 'firstname', + last_name: 'lastname', + state: 'state', + zip_code: '' }) expect(mockTtp.track).toHaveBeenCalledWith( 'ViewContent', @@ -361,4 +464,160 @@ describe('TikTokPixel.reportWebEvent', () => { { event_id: 'ajs-71f386523ee5dfa90c7d0fda28b6b5c6' } ) }) + + test('identifiers can be passed as strings only', async () => { + const subscriptions: Subscription[] = [ + { + partnerAction: 'reportWebEvent', + name: 'Place an Order', + enabled: true, + subscribe: 'event = "Order Completed"', + mapping: { + event_id: { + '@path': '$.messageId' + }, + anonymousId: { + '@path': '$.anonymousId' + }, + external_id: { + '@path': '$.userId' + }, + phone_number: { + '@path': '$.properties.phone' + }, + email: { + '@path': '$.properties.email' + }, + last_name: { + '@path': '$.context.traits.last_name' + }, + first_name: { + '@path': '$.context.traits.first_name' + }, + address: { + city: { + '@path': '$.context.traits.address.city' + }, + state: { + '@path': '$.context.traits.address.state' + }, + country: { + '@path': '$.context.traits.address.country' + } + }, + groupId: { + '@path': '$.groupId' + }, + event: 'PlaceAnOrder', + contents: { + '@arrayPath': [ + '$.properties.products', + { + price: { + '@path': '$.price' + }, + quantity: { + '@path': '$.quantity' + }, + content_type: { + '@path': '$.category' + }, + content_id: { + '@path': '$.product_id' + } + } + ] + }, + currency: { + '@path': '$.properties.currency' + }, + value: { + '@path': '$.properties.value' + }, + query: { + '@path': '$.properties.query' + }, + description: { + '@path': '$.properties.description' + } + } + } + ] + + const context = new Context({ + messageId: 'ajs-71f386523ee5dfa90c7d0fda28b6b5c6', + type: 'track', + anonymousId: 'anonymousId', + userId: 'userId', + event: 'Order Completed', + context: { + traits: { + last_name: 'lastName', + first_name: 'firstName', + address: { + city: 'city', + state: 'state', + country: 'country' + } + } + }, + properties: { + products: [ + { + product_id: '123', + category: 'product', + quantity: 1, + price: 1 + }, + { + product_id: '456', + category: 'product', + quantity: 2, + price: 2 + } + ], + query: 'test-query', + value: 10, + currency: 'USD', + phone: '+12345678900', + email: 'aaa@aaa.com', + description: 'test-description' + } + }) + + const [webEvent] = await TikTokDestination({ + ...settings, + subscriptions + }) + reportWebEvent = webEvent + + await reportWebEvent.load(Context.system(), {} as Analytics) + await reportWebEvent.track?.(context) + + expect(mockTtp.identify).toHaveBeenCalledWith({ + city: 'city', + country: 'country', + email: 'aaa@aaa.com', + phone_number: '+12345678900', + external_id: 'userId', + first_name: 'firstname', + last_name: 'lastname', + state: 'state', + zip_code: '' + }) + expect(mockTtp.track).toHaveBeenCalledWith( + 'PlaceAnOrder', + { + contents: [ + { content_id: '123', content_type: 'product', price: 1, quantity: 1 }, + { content_id: '456', content_type: 'product', price: 2, quantity: 2 } + ], + currency: 'USD', + description: 'test-description', + query: 'test-query', + value: 10 + }, + { event_id: 'ajs-71f386523ee5dfa90c7d0fda28b6b5c6' } + ) + }) }) diff --git a/packages/browser-destinations/destinations/tiktok-pixel/src/reportWebEvent/generated-types.ts b/packages/browser-destinations/destinations/tiktok-pixel/src/reportWebEvent/generated-types.ts index f0f1dbf9dd..ac49bf0542 100644 --- a/packages/browser-destinations/destinations/tiktok-pixel/src/reportWebEvent/generated-types.ts +++ b/packages/browser-destinations/destinations/tiktok-pixel/src/reportWebEvent/generated-types.ts @@ -2,7 +2,7 @@ export interface Payload { /** - * Conversion event name. Please refer to the "Supported Web Events" section on in TikTok’s [Pixel documentation](https://ads.tiktok.com/marketing_api/docs?id=1739585696931842) for accepted event names. + * Conversion event name. Please refer to the "Supported Web Events" section on in TikTok’s [Pixel SDK documentation](https://business-api.tiktok.com/portal/docs?id=1739585696931842) for accepted event names. */ event: string /** @@ -10,19 +10,56 @@ export interface Payload { */ event_id?: string /** - * Phone number of the user who triggered the conversion event, in E.164 standard format, e.g. +14150000000. Segment will hash this value before sending to TikTok. + * A single phone number in E.164 standard format. TikTok Pixel will hash this value before sending to TikTok. e.g. +14150000000. Segment will hash this value before sending to TikTok. */ - phone_number?: string + phone_number?: string[] /** - * Email address of the user who triggered the conversion event. Segment will hash this value before sending to TikTok. + * A single email address. TikTok Pixel will be hash this value before sending to TikTok. */ - email?: string + email?: string[] /** - * Uniquely identifies the user who triggered the conversion event. Segment will hash this value before sending to TikTok. + * The first name of the customer. The name should be in lowercase without any punctuation. Special characters are allowed. */ - external_id?: string + first_name?: string /** - * Related items in a web event. + * The last name of the customer. The name should be in lowercase without any punctuation. Special characters are allowed. + */ + last_name?: string + /** + * The address of the customer. + */ + address?: { + /** + * The customer's city. + */ + city?: string + /** + * The customer's country. + */ + country?: string + /** + * The customer's Zip Code. + */ + zip_code?: string + /** + * The customer's State. + */ + state?: string + } + /** + * Order ID of the transaction. + */ + order_id?: string + /** + * Shop ID of the transaction. + */ + shop_id?: string + /** + * Uniquely identifies the user who triggered the conversion event. TikTok Pixel will hash this value before sending to TikTok. + */ + external_id?: string[] + /** + * Related item details for the event. */ contents?: { /** @@ -34,14 +71,26 @@ export interface Payload { */ quantity?: number /** - * Type of the product item. + * Category of the product item. */ - content_type?: string + content_category?: string /** * ID of the product item. */ content_id?: string + /** + * Name of the product item. + */ + content_name?: string + /** + * Brand name of the product item. + */ + brand?: string }[] + /** + * Type of the product item. When the `content_id` in the `Contents` field is specified as a `sku_id`, set this field to `product`. When the `content_id` in the `Contents` field is specified as an `item_group_id`, set this field to `product_group`. + */ + content_type?: string /** * Currency for the value specified as ISO 4217 code. */ 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 a6a5b404f5..c5667254ce 100644 --- a/packages/browser-destinations/destinations/tiktok-pixel/src/reportWebEvent/index.ts +++ b/packages/browser-destinations/destinations/tiktok-pixel/src/reportWebEvent/index.ts @@ -1,8 +1,9 @@ import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { formatPhone } from './formatter' +import { formatPhone, handleArrayInput, formatString, formatAddress } from '../formatter' import { TikTokPixel } from '../types' +import { commonFields } from '../common_fields' const action: BrowserActionDefinition = { title: 'Report Web Event', @@ -11,147 +12,37 @@ const action: BrowserActionDefinition = { platform: 'web', defaultSubscription: 'type = "track"', fields: { - event: { - label: 'Event Name', - type: 'string', - required: true, - description: - 'Conversion event name. Please refer to the "Supported Web Events" section on in TikTok’s [Pixel documentation](https://ads.tiktok.com/marketing_api/docs?id=1739585696931842) for accepted event names.' - }, - event_id: { - label: 'Event ID', - type: 'string', - description: 'Any hashed ID that can identify a unique user/session.', - default: { - '@path': '$.messageId' - } - }, - // PII Fields - These fields must be hashed using SHA 256 and encoded as websafe-base64. - phone_number: { - label: 'Phone Number', - description: - 'Phone number of the user who triggered the conversion event, in E.164 standard format, e.g. +14150000000. Segment will hash this value before sending to TikTok.', - type: 'string', - default: { - '@if': { - exists: { '@path': '$.properties.phone' }, - then: { '@path': '$.properties.phone' }, - else: { '@path': '$.traits.phone' } - } - } - }, - email: { - label: 'Email', - description: - 'Email address of the user who triggered the conversion event. Segment will hash this value before sending to TikTok.', - type: 'string', - format: 'email', - default: { - '@if': { - exists: { '@path': '$.properties.email' }, - then: { '@path': '$.properties.email' }, - else: { '@path': '$.traits.email' } - } - } - }, - external_id: { - label: 'External ID', - description: - 'Uniquely identifies the user who triggered the conversion event. Segment will hash this value before sending to TikTok.', - type: 'string', - default: { - '@if': { - exists: { '@path': '$.userId' }, - then: { '@path': '$.userId' }, - else: { '@path': '$.anonymousId' } - } - } - }, - contents: { - label: 'Contents', - type: 'object', - multiple: true, - description: 'Related items in a web event.', - properties: { - price: { - label: 'Price', - description: 'Price of the item.', - type: 'number' - }, - quantity: { - label: 'Quantity', - description: 'Number of items.', - type: 'number' - }, - content_type: { - label: 'Content Type', - description: 'Type of the product item.', - type: 'string' - }, - content_id: { - label: 'Content ID', - description: 'ID of the product item.', - type: 'string' - } - } - }, - currency: { - label: 'Currency', - type: 'string', - description: 'Currency for the value specified as ISO 4217 code.', - default: { - '@path': '$.properties.currency' - } - }, - value: { - label: 'Value', - type: 'number', - description: 'Value of the order or items sold.', - default: { - '@if': { - exists: { '@path': '$.properties.value' }, - then: { '@path': '$.properties.value' }, - else: { '@path': '$.properties.revenue' } - } - } - }, - description: { - label: 'Description', - type: 'string', - description: 'A string description of the web event.', - default: { - '@path': '$.properties.description' - } - }, - query: { - label: 'Query', - type: 'string', - description: 'The text string that was searched for.', - default: { - '@path': '$.properties.query' - } - } + ...commonFields }, - perform: (ttq, { payload }) => { - if (payload.email || payload.phone_number) { + perform: (ttq, { payload, settings }) => { + if (payload.email || payload.phone_number || payload.external_id) { ttq.identify({ - email: payload.email, - phone_number: formatPhone(payload.phone_number), - external_id: payload.external_id + email: handleArrayInput(payload.email), + phone_number: formatPhone(handleArrayInput(payload.phone_number)), + external_id: handleArrayInput(payload.external_id), + first_name: formatString(payload.first_name), + last_name: formatString(payload.last_name), + city: formatAddress(payload.address?.city), + state: formatAddress(payload.address?.state), + country: formatAddress(payload.address?.country), + zip_code: formatString(payload.address?.zip_code) }) } - ttq.track( + ttq.instance(settings.pixelCode).track( payload.event, { contents: payload.contents ? payload.contents : [], - currency: payload.currency ? payload.currency : 'USD', // default to 'USD' - value: payload.value ? payload.value : 0, //default to 0 - description: payload.description, - query: payload.query + content_type: payload.content_type ? payload.content_type : undefined, + currency: payload.currency ? payload.currency : 'USD', + value: payload.value || payload.value === 0 ? payload.value : undefined, + query: payload.query ? payload.query : undefined, + description: payload.description ? payload.description : undefined, + order_id: payload.order_id ? payload.order_id : undefined, + shop_id: payload.shop_id ? payload.shop_id : undefined }, { - event_id: payload.event_id + event_id: payload.event_id ? payload.event_id : '' } ) } diff --git a/packages/browser-destinations/destinations/tiktok-pixel/src/types.ts b/packages/browser-destinations/destinations/tiktok-pixel/src/types.ts index c1ceacf601..93042f663d 100644 --- a/packages/browser-destinations/destinations/tiktok-pixel/src/types.ts +++ b/packages/browser-destinations/destinations/tiktok-pixel/src/types.ts @@ -1,35 +1,56 @@ export interface TikTokPixel { page: () => void + instance: (pixel_code: string) => TikTokPixel identify: ({ email, phone_number, - external_id + external_id, + first_name, + last_name, + city, + state, + country, + zip_code }: { email: string | undefined phone_number: string | undefined external_id: string | undefined + first_name: string | undefined + last_name: string | undefined + city: string | undefined + state: string | undefined + country: string | undefined + zip_code: string | undefined }) => void track: ( event: string, { contents, + content_type, currency, value, description, - query + query, + order_id, + shop_id }: { contents: | { - price?: number - quantity?: number - content_type?: string - content_id?: string + price?: number | undefined + quantity?: number | undefined + content_category?: string | undefined + content_id?: string | undefined + content_name?: string | undefined + brand?: string | undefined }[] | [] - currency: string - value: number + content_type: string | undefined + currency: string | undefined + value: number | undefined description: string | undefined query: string | undefined + order_id: string | undefined + shop_id: string | undefined }, { event_id diff --git a/packages/browser-destinations/destinations/upollo/package.json b/packages/browser-destinations/destinations/upollo/package.json index 37c3d6c9ae..35772b320d 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.21.0", + "version": "1.51.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.90.0", - "@segment/browser-destination-runtime": "^1.20.0" + "@segment/actions-core": "^3.121.0", + "@segment/browser-destination-runtime": "^1.50.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 2d658fa5ed..c085b3de5e 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.21.0", + "version": "1.51.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.90.0", - "@segment/browser-destination-runtime": "^1.20.0" + "@segment/actions-core": "^3.121.0", + "@segment/browser-destination-runtime": "^1.50.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 a008370fdc..46f9478668 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.22.0", + "version": "1.52.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.90.0", - "@segment/browser-destination-runtime": "^1.20.0" + "@segment/actions-core": "^3.121.0", + "@segment/browser-destination-runtime": "^1.50.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 c3559f21f6..baadb5f349 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.21.0", + "version": "1.52.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.90.0", - "@segment/browser-destination-runtime": "^1.20.0" + "@segment/actions-core": "^3.121.0", + "@segment/browser-destination-runtime": "^1.50.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/wisepops/src/index.ts b/packages/browser-destinations/destinations/wisepops/src/index.ts index 89fca442f8..977d72da72 100644 --- a/packages/browser-destinations/destinations/wisepops/src/index.ts +++ b/packages/browser-destinations/destinations/wisepops/src/index.ts @@ -49,13 +49,6 @@ export const destination: BrowserDestinationDefinition = { mapping: defaultValues(trackEvent.fields), type: 'automatic' }, - { - name: trackGoal.title, - subscribe: trackGoal.defaultSubscription!, - partnerAction: 'trackGoal', - mapping: defaultValues(trackGoal.fields), - type: 'automatic' - }, { name: trackPage.title, subscribe: trackPage.defaultSubscription!, diff --git a/packages/browser-destinations/destinations/wisepops/src/trackGoal/__tests__/index.test.ts b/packages/browser-destinations/destinations/wisepops/src/trackGoal/__tests__/index.test.ts index 06eeeda8b5..80fd63b865 100644 --- a/packages/browser-destinations/destinations/wisepops/src/trackGoal/__tests__/index.test.ts +++ b/packages/browser-destinations/destinations/wisepops/src/trackGoal/__tests__/index.test.ts @@ -19,7 +19,7 @@ describe('Wisepops.trackGoal', () => { subscribe: trackGoalObject.defaultSubscription!, mapping: { goalName: { - '@path': '$.event' + '@path': '$.properties.goalName' }, goalRevenue: { '@path': '$.properties.revenue' @@ -28,7 +28,7 @@ describe('Wisepops.trackGoal', () => { } ] - test('named goal with revenue', async () => { + test('old named goal with revenue', async () => { const [trackGoal] = await wisepopsDestination({ websiteId: '1234567890', subscriptions @@ -42,6 +42,7 @@ describe('Wisepops.trackGoal', () => { type: 'track', event: 'Order Completed', properties: { + goalName: 'Order Completed', revenue: 15 } }) @@ -49,4 +50,27 @@ describe('Wisepops.trackGoal', () => { expect(window.wisepops.q.push).toHaveBeenCalledWith(['goal', 'Order Completed', 15]) }) + + test('new goal with revenue', async () => { + const [trackGoal] = await wisepopsDestination({ + websiteId: '1234567890', + subscriptions + }) + expect(trackGoal).toBeDefined() + + await trackGoal.load(Context.system(), {} as Analytics) + jest.spyOn(window.wisepops.q as any, 'push') + + const context = new Context({ + type: 'track', + event: 'Order Completed', + properties: { + goalName: 'yhqnj9RTF3Fk6TnTmRW6vhxiugipbUKc', + revenue: 15 + } + }) + trackGoal.track?.(context) + + expect(window.wisepops.q.push).toHaveBeenCalledWith(['goal', 'yhqnj9RTF3Fk6TnTmRW6vhxiugipbUKc', {revenue: 15}]) + }) }) diff --git a/packages/browser-destinations/destinations/wisepops/src/trackGoal/generated-types.ts b/packages/browser-destinations/destinations/wisepops/src/trackGoal/generated-types.ts index 9846eec850..86267ed930 100644 --- a/packages/browser-destinations/destinations/wisepops/src/trackGoal/generated-types.ts +++ b/packages/browser-destinations/destinations/wisepops/src/trackGoal/generated-types.ts @@ -2,7 +2,7 @@ export interface Payload { /** - * The name of the goal to send to Wisepops. + * This is a 32-character identifier, visible when you create the JS goal in Wisepops. */ goalName?: string /** diff --git a/packages/browser-destinations/destinations/wisepops/src/trackGoal/index.ts b/packages/browser-destinations/destinations/wisepops/src/trackGoal/index.ts index ed2a998956..f1e201405c 100644 --- a/packages/browser-destinations/destinations/wisepops/src/trackGoal/index.ts +++ b/packages/browser-destinations/destinations/wisepops/src/trackGoal/index.ts @@ -3,7 +3,6 @@ import type { Settings } from '../generated-types' import type { Payload } from './generated-types' import type { Wisepops } from '../types' -// Change from unknown to the partner SDK types const action: BrowserActionDefinition = { title: 'Track Goal', description: '[Track goals and revenue](https://support.wisepops.com/article/mx3z8na6yb-set-up-goal-tracking) to know which campaigns are generating the most value.', @@ -11,13 +10,10 @@ const action: BrowserActionDefinition = { platform: 'web', fields: { goalName: { - description: 'The name of the goal to send to Wisepops.', - label: 'Goal Name', + description: 'This is a 32-character identifier, visible when you create the JS goal in Wisepops.', + label: 'Goal Identifier', type: 'string', required: false, - default: { - '@path': '$.event' - } }, goalRevenue: { description: 'The revenue associated with the goal.', @@ -30,7 +26,14 @@ const action: BrowserActionDefinition = { }, }, perform: (wisepops, event) => { - wisepops('goal', event.payload.goalName, event.payload.goalRevenue); + let revenue = null; + if (['string', 'number'].includes(typeof event.payload.goalRevenue) && !Number.isNaN(Number(event.payload.goalRevenue))) { + revenue = Number(event.payload.goalRevenue); + } + if (typeof event.payload.goalName === 'string' && /^[a-zA-Z0-9]{32}$/.test(event.payload.goalName)) { + revenue = {revenue}; + } + wisepops('goal', event.payload.goalName, revenue); } } diff --git a/packages/browser-destinations/package.json b/packages/browser-destinations/package.json index c235dc2188..dc0e10d318 100644 --- a/packages/browser-destinations/package.json +++ b/packages/browser-destinations/package.json @@ -1,7 +1,7 @@ { "name": "@segment/browser-destinations", "private": true, - "version": "0.0.0", + "version": "0.1.0", "description": "Action based browser destinations", "author": "Netto Farah", "license": "MIT", @@ -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,13 +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", "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", @@ -77,5 +80,11 @@ "/test/setup-after-env.ts" ], "forceExit": true - } + }, + "size-limit": [ + { + "path": "dist/web/*/*.js", + "limit": "169 KB" + } + ] } diff --git a/packages/cli-internal/package.json b/packages/cli-internal/package.json index 64d730633a..316a8728ba 100644 --- a/packages/cli-internal/package.json +++ b/packages/cli-internal/package.json @@ -1,7 +1,7 @@ { "name": "@segment/actions-cli-internal", "description": "CLI to interact with Segment integrations", - "version": "3.143.1", + "version": "3.146.0", "license": "MIT", "repository": { "type": "git", @@ -49,13 +49,13 @@ "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", - "@segment/action-destinations": "^3.190.0", - "@segment/actions-core": "^3.76.0", - "@segment/destinations-manifest": "^1.6.0", + "@segment/action-destinations": "^3.288.0", + "@segment/actions-core": "^3.121.0", + "@segment/destinations-manifest": "^1.71.0", "@types/node": "^18.11.15", "chalk": "^4.1.1", "chokidar": "^3.5.1", @@ -64,7 +64,7 @@ "execa": "^5.1.1", "fs-extra": "^10.0.0", "globby": "^11.0.3", - "jscodeshift": "^0.13.0", + "jscodeshift": "^0.14.0", "jscodeshift-add-imports": "^1.0.10", "jsdom": "^18.0.0", "json-diff": "^0.5.4", 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 816184d4ad..f5f1140cc4 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@segment/actions-cli", "description": "CLI to interact with Segment integrations", - "version": "3.143.1", + "version": "3.146.0", "license": "MIT", "repository": { "type": "git", @@ -52,13 +52,13 @@ "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", - "@segment/action-destinations": "^3.190.0", - "@segment/actions-core": "^3.76.0", - "@segment/destinations-manifest": "^1.6.0", + "@segment/action-destinations": "^3.288.0", + "@segment/actions-core": "^3.121.0", + "@segment/destinations-manifest": "^1.71.0", "@types/node": "^18.11.15", "chalk": "^4.1.1", "chokidar": "^3.5.1", @@ -67,7 +67,7 @@ "execa": "^5.1.1", "fs-extra": "^10.0.0", "globby": "^11.0.3", - "jscodeshift": "^0.13.0", + "jscodeshift": "^0.14.0", "jscodeshift-add-imports": "^1.0.10", "jsdom": "^18.0.0", "json-diff": "^0.5.4", @@ -82,7 +82,7 @@ "tslib": "^2.3.1" }, "optionalDependencies": { - "@segment/actions-cli-internal": "^3.143.1" + "@segment/actions-cli-internal": "^3.146.0" }, "oclif": { "commands": "./dist/commands", 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/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/lib/server.ts b/packages/cli/src/lib/server.ts index ff75d3a220..8a68fa4aa9 100644 --- a/packages/cli/src/lib/server.ts +++ b/packages/cli/src/lib/server.ts @@ -296,7 +296,8 @@ 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)) { @@ -362,7 +363,8 @@ function setupRoutes(def: DestinationDefinition | null): void { page: req.body.page || 1, auth: req.body.auth || {}, audienceSettings: req.body.audienceSettings || {}, - hookInputs: req.body.hookInputs || {} + hookInputs: req.body.hookInputs || {}, + hookOutputs: req.body.hookOutputs || {} } const action = destination.actions[actionSlug] diff --git a/packages/cli/src/lib/summarize-http.ts b/packages/cli/src/lib/summarize-http.ts index d21b667360..8bd9cde61b 100644 --- a/packages/cli/src/lib/summarize-http.ts +++ b/packages/cli/src/lib/summarize-http.ts @@ -7,7 +7,7 @@ export interface Exchange { export interface RequestToDestination { url: string - headers: Headers + headers: { [key: string]: string } // JSON.strigify() does not work for request headers method: string body: unknown } @@ -36,10 +36,42 @@ async function summarizeRequest(response: Response): Promise { + if (sensitiveHeaders.includes(key.toLowerCase())) { + headersObject[key] = '' + } else { + headersObject[key] = value + } + }) + return { url: request.url, method: request.method, - headers: request.headers, + headers: headersObject, body: data ?? '' } } 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 ab23d06912..ed7f24b3e7 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.90.0", + "version": "3.121.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.31.0", + "@segment/action-emitters": "^1.3.6", + "@segment/ajv-human-errors": "^2.13.0", + "@segment/destination-subscriptions": "^3.34.0", "@types/node": "^18.11.15", "abort-controller": "^3.0.0", "aggregate-error": "^3.1.0", 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__/destination-kit.test.ts b/packages/core/src/__tests__/destination-kit.test.ts index 55beb96c39..6135405a4a 100644 --- a/packages/core/src/__tests__/destination-kit.test.ts +++ b/packages/core/src/__tests__/destination-kit.test.ts @@ -111,6 +111,170 @@ const destinationWithOptions: DestinationDefinition = { } } +const destinationWithSyncMode: DestinationDefinition = { + name: 'Actions Google Analytics 4', + mode: 'cloud', + actions: { + customEvent: { + title: 'Send a Custom Event', + description: 'Send events to a custom event in API', + defaultSubscription: 'type = "track"', + fields: {}, + syncMode: { + default: 'add', + description: 'Select the sync mode for the subscription', + label: 'Sync Mode', + choices: [ + { + label: 'Insert', + value: 'add' + }, + { + label: 'Delete', + value: 'delete' + } + ] + }, + perform: (_request, { syncMode }) => { + return ['this is a test', syncMode] + }, + performBatch: (_request, { syncMode }) => { + return ['this is a test', syncMode] + } + } + } +} + +const destinationWithIdentifier: DestinationDefinition = { + name: 'Actions Google Analytics 4', + mode: 'cloud', + actions: { + customEvent: { + title: 'Send a Custom Event', + description: 'Send events to a custom event in API', + defaultSubscription: 'type = "track"', + fields: { + userId: { + label: 'User ID', + description: 'The user ID', + type: 'string', + required: true, + category: 'identifier' + } + }, + perform: (_request, { matchingKey }) => { + return ['this is a test', matchingKey] + }, + performBatch: (_request, { matchingKey }) => { + return ['this is a test', matchingKey] + } + } + } +} + +const destinationWithDynamicFields: DestinationDefinition = { + name: 'Actions Dynamic Fields', + mode: 'cloud', + actions: { + customEvent: { + title: 'Send a Custom Event', + description: 'Send events to a custom event in API', + defaultSubscription: 'type = "track"', + fields: { + testDynamicField: { + label: 'Dynamic Field', + description: 'A dynamic field', + type: 'string', + required: true, + dynamic: true + }, + testUnstructuredObject: { + label: 'Unstructured Object', + description: 'An unstructured object', + type: 'object', + dynamic: true + }, + testStructuredObject: { + label: 'Structured Object', + description: 'A structured object', + type: 'object', + properties: { + testDynamicSubfield: { + label: 'Test Field', + description: 'A test field', + type: 'string', + required: true, + dynamic: true + } + } + }, + testObjectArrays: { + label: 'Structured Array of Object', + description: 'A structured array of object', + type: 'object', + multiple: true, + properties: { + testDynamicSubfield: { + label: 'Test Field', + description: 'A test field', + type: 'string', + required: true, + dynamic: true + } + } + } + }, + dynamicFields: { + testDynamicField: async () => { + return { + choices: [{ label: 'test', value: 'test' }], + nextPage: '' + } + }, + testUnstructuredObject: { + __keys__: async () => { + return { + choices: [{ label: 'Im a key', value: '🔑' }], + nextPage: '' + } + }, + __values__: async (_, input) => { + const { dynamicFieldContext } = input + + return { + choices: [{ label: `Im a value for ${dynamicFieldContext?.selectedKey}`, value: '2️⃣' }], + nextPage: '' + } + } + }, + testStructuredObject: { + testDynamicSubfield: async () => { + return { + choices: [{ label: 'Im a subfield', value: 'nah' }], + nextPage: '' + } + } + }, + testObjectArrays: { + testDynamicSubfield: async (_, input) => { + const { dynamicFieldContext } = input + + return { + choices: [ + { label: `Im a subfield for element ${dynamicFieldContext?.selectedArrayIndex}`, value: 'nah' } + ], + nextPage: '' + } + } + } + }, + perform: (_request, { syncMode }) => { + return ['this is a test', syncMode] + } + } + } +} + describe('destination kit', () => { describe('event validations', () => { test('should return `invalid subscription` when sending an empty subscribe', async () => { @@ -275,6 +439,106 @@ describe('destination kit', () => { { output: 'Action Executed', data: ['this is a test', {}] } ]) }) + + test('should inject the syncMode value in the perform handler', async () => { + const destinationTest = new Destination(destinationWithSyncMode) + const testEvent: SegmentEvent = { type: 'track' } + const testSettings = { + apiSecret: 'test_key', + subscription: { + subscribe: 'type = "track"', + partnerAction: 'customEvent', + mapping: { + __segment_internal_sync_mode: 'add' + } + } + } + + const res = await destinationTest.onEvent(testEvent, testSettings) + + expect(res).toEqual([ + { output: 'Mappings resolved' }, + { + output: 'Action Executed', + data: ['this is a test', 'add'] + } + ]) + }) + + test('should inject the syncMode value in the performBatch handler', async () => { + const destinationTest = new Destination(destinationWithSyncMode) + const testEvent: SegmentEvent = { type: 'track' } + const testSettings = { + apiSecret: 'test_key', + subscription: { + subscribe: 'type = "track"', + partnerAction: 'customEvent', + mapping: { + __segment_internal_sync_mode: 'add' + } + } + } + + const res = await destinationTest.onBatch([testEvent], testSettings) + + expect(res).toEqual([ + { + output: 'Action Executed', + data: ['this is a test', 'add'] + } + ]) + }) + + test('should inject the matchingKey value in the perform handler', async () => { + const destinationTest = new Destination(destinationWithIdentifier) + const testEvent: SegmentEvent = { type: 'track' } + const testSettings = { + apiSecret: 'test_key', + subscription: { + subscribe: 'type = "track"', + partnerAction: 'customEvent', + mapping: { + __segment_internal_matching_key: 'userId', + userId: 'this-is-a-user-id' + } + } + } + + const res = await destinationTest.onEvent(testEvent, testSettings) + + expect(res).toEqual([ + { output: 'Mappings resolved' }, + { output: 'Payload validated' }, + { + output: 'Action Executed', + data: ['this is a test', 'userId'] + } + ]) + }) + + test('should inject the matchingKey value in the performBatch handler', async () => { + const destinationTest = new Destination(destinationWithIdentifier) + const testEvent: SegmentEvent = { type: 'track' } + const testSettings = { + subscription: { + subscribe: 'type = "track"', + partnerAction: 'customEvent', + mapping: { + __segment_internal_matching_key: 'userId', + userId: 'this-is-a-user-id' + } + } + } + + const res = await destinationTest.onBatch([testEvent], testSettings) + + expect(res).toEqual([ + { + output: 'Action Executed', + data: ['this is a test', 'userId'] + } + ]) + }) }) describe('refresh token', () => { @@ -593,4 +857,100 @@ describe('destination kit', () => { ]) }) }) + + describe('dynamicFields', () => { + test('should return empty array if action is not part of definition', async () => { + const destinationTest = new Destination(destinationWithDynamicFields) + const res = await destinationTest.executeDynamicField('ghostAction', 'testDynamicField', { + settings: {}, + payload: {} + }) + expect(res).toEqual([]) + }) + + test('should return 404 if handler for dynamic field does not exist', async () => { + const destinationTest = new Destination(destinationWithDynamicFields) + const res = await destinationTest.executeDynamicField('customEvent', 'randomField', { + settings: {}, + payload: {} + }) + expect(res).toEqual({ + choices: [], + error: { code: '404', message: 'No dynamic field named randomField found.' }, + nextPage: '' + }) + }) + + test('should return a response with choices for string fields', async () => { + const destinationTest = new Destination(destinationWithDynamicFields) + const res = await destinationTest.executeDynamicField('customEvent', 'testDynamicField', { + settings: {}, + payload: {} + }) + expect(res).toEqual({ choices: [{ label: 'test', value: 'test' }], nextPage: '' }) + }) + + test('fetches keys for unstructured objects', async () => { + const destinationTest = new Destination(destinationWithDynamicFields) + const res = await destinationTest.executeDynamicField('customEvent', 'testUnstructuredObject.__keys__', { + settings: {}, + payload: {} + }) + expect(res).toEqual({ choices: [{ label: 'Im a key', value: '🔑' }], nextPage: '' }) + }) + + test('fetches values for unstructured objects', async () => { + const destinationTest = new Destination(destinationWithDynamicFields) + ;('testUnstructuredObject.__values__') + let res = await destinationTest.executeDynamicField('customEvent', 'testUnstructuredObject.keyOne', { + settings: {}, + payload: {} + }) + expect(res).toEqual({ choices: [{ label: 'Im a value for keyOne', value: '2️⃣' }], nextPage: '' }) + + res = await destinationTest.executeDynamicField('customEvent', 'testUnstructuredObject.keyTwo', { + settings: {}, + payload: {} + }) + + expect(res).toEqual({ choices: [{ label: 'Im a value for keyTwo', value: '2️⃣' }], nextPage: '' }) + }) + + test('fetches values for structured object subfields', async () => { + const destinationTest = new Destination(destinationWithDynamicFields) + const res = await destinationTest.executeDynamicField('customEvent', 'testStructuredObject.testDynamicSubfield', { + settings: {}, + payload: {} + }) + expect(res).toEqual({ choices: [{ label: 'Im a subfield', value: 'nah' }], nextPage: '' }) + }) + + test('fetches values for structured array of object', async () => { + const destinationTest = new Destination(destinationWithDynamicFields) + let res = await destinationTest.executeDynamicField('customEvent', 'testObjectArrays.[0].testDynamicSubfield', { + settings: {}, + payload: {} + }) + expect(res).toEqual({ choices: [{ label: 'Im a subfield for element 0', value: 'nah' }], nextPage: '' }) + + res = await destinationTest.executeDynamicField('customEvent', 'testObjectArrays.[113].testDynamicSubfield', { + settings: {}, + payload: {} + }) + expect(res).toEqual({ choices: [{ label: 'Im a subfield for element 113', value: 'nah' }], nextPage: '' }) + }) + + test('returns 404 for invalid subfields', async () => { + const destinationTest = new Destination(destinationWithDynamicFields) + const res = await destinationTest.executeDynamicField('customEvent', 'testStructuredObject.ghostSubfield', { + settings: {}, + payload: {} + }) + expect(res).toEqual({ + choices: [], + error: { code: '404', message: 'No dynamic field named testStructuredObject.ghostSubfield found.' }, + nextPage: '' + }) + }) + }) }) diff --git a/packages/core/src/__tests__/remove-empty-values.test.ts b/packages/core/src/__tests__/remove-empty-values.test.ts index 7ff213d8bf..1b1a5d5d49 100644 --- a/packages/core/src/__tests__/remove-empty-values.test.ts +++ b/packages/core/src/__tests__/remove-empty-values.test.ts @@ -185,4 +185,60 @@ describe(removeEmptyValues.name, () => { } }) }) + + it('null values with different schema types', () => { + const input = { + product: { + product_id: null, // string that doesn't allow null + name: null, // string that allows null + address: null, // object that doesn't allow null + traits: null, // object that allow null + age: null, // number that doesn't allow null + accountsCount: null, // integer that allows null + isPremium: null, // boolean that doesn't allow null + hasSubscription: null, // boolean that allows null + location: null, // no explicit type specified + nested: { + foo: null, + bar: '' + } + } + } + + const schema: JSONSchema4 = { + type: 'object', + properties: { + product: { + type: 'object', + properties: { + product_id: { type: 'string' }, + name: { type: ['null', 'string'] }, + address: { type: 'object' }, + traits: { type: ['null', 'object'] }, + age: { type: 'number' }, + accountsCount: { type: ['null', 'integer'] }, + isPremium: { type: 'boolean' }, + hasSubscription: { type: ['null', 'boolean'] }, + nested: { + type: 'object' + } + } + } + } + } + + expect(removeEmptyValues(input, schema, true)).toEqual({ + product: { + name: null, + traits: null, + location: null, + accountsCount: null, + hasSubscription: null, + nested: { + foo: null, + bar: '' + } + } + }) + }) }) diff --git a/packages/core/src/destination-kit/action.ts b/packages/core/src/destination-kit/action.ts index 43233e9925..9a56e9a492 100644 --- a/packages/core/src/destination-kit/action.ts +++ b/packages/core/src/destination-kit/action.ts @@ -5,7 +5,17 @@ import { InputData, Features, transform, transformBatch } from '../mapping-kit' import { fieldsToJsonSchema } from './fields-to-jsonschema' import { Response } from '../fetch' import type { ModifiedResponse } from '../types' -import type { DynamicFieldResponse, InputField, RequestExtension, ExecuteInput, Result } from './types' +import type { + DynamicFieldResponse, + InputField, + RequestExtension, + ExecuteInput, + Result, + SyncMode, + SyncModeDefinition, + DynamicFieldContext +} from './types' +import { syncModeTypes } from './types' import { NormalizedOptions } from '../request-client' import type { JSONSchema4 } from 'json-schema' import { validateSchema } from '../schema-validation' @@ -13,6 +23,7 @@ import { AuthTokens } from './parse-settings' import { IntegrationError } from '../errors' import { removeEmptyValues } from '../remove-empty-values' import { Logger, StatsContext, TransactionContext, StateContext, DataFeedCache } from './index' +import { get } from '../get' type MaybePromise = T | Promise type RequestClient = ReturnType @@ -49,11 +60,11 @@ export interface BaseActionDefinition { fields: Record } -type HookValueTypes = string | boolean | number +type HookValueTypes = string | boolean | number | Array type GenericActionHookValues = Record type GenericActionHookBundle = { - [K in ActionHookType]: { + [K in ActionHookType]?: { inputs?: GenericActionHookValues outputs?: GenericActionHookValues } @@ -71,7 +82,16 @@ export interface ActionDefinition< * This is likely going to change as we productionalize the data model and definition object */ dynamicFields?: { - [K in keyof Payload]?: RequestFn + [K in keyof Payload]?: Payload[K] extends object + ? { + [ObjectProperty in keyof Payload[K] | '__keys__' | '__values__']?: RequestFn< + Settings, + Payload[K], + DynamicFieldResponse, + AudienceSettings + > + } + : RequestFn } /** The operation to perform when this action is triggered */ @@ -85,17 +105,20 @@ export interface ActionDefinition< * in the mapping for later use in the action. */ hooks?: { - [K in ActionHookType]: ActionHookDefinition< + [K in ActionHookType]?: ActionHookDefinition< Settings, Payload, AudienceSettings, - GeneratedActionHookBundle[K]['outputs'], - GeneratedActionHookBundle[K]['inputs'] + NonNullable['outputs'], + NonNullable['inputs'] > } + + /** The sync mode setting definition. This enables subscription sync mode selection when subscribing to this action. */ + syncMode?: SyncModeDefinition } -export const hookTypeStrings = ['onMappingSave'] as const +export const hookTypeStrings = ['onMappingSave', 'retlOnMappingSave'] 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. @@ -149,6 +172,11 @@ export interface ExecuteDynamicFieldInput { @@ -167,6 +195,17 @@ interface ExecuteBundle { + return syncModeTypes.find((validValue) => value === validValue) !== undefined +} + +const INTERNAL_HIDDEN_FIELDS = ['__segment_internal_sync_mode', '__segment_internal_matching_key'] +const removeInternalHiddenFields = (mapping: JSONObject): JSONObject => { + return Object.keys(mapping).reduce((acc, key) => { + return INTERNAL_HIDDEN_FIELDS.includes(key) ? acc : { ...acc, [key]: mapping[key] } + }, {}) +} + /** * Action is the beginning step for all partner actions. Entrypoints always start with the * MapAndValidateInput step. @@ -203,7 +242,7 @@ export class Action