From 191529bb326cae7106a26c49ec155f64c6914e76 Mon Sep 17 00:00:00 2001 From: Noah Cooper Date: Tue, 7 Feb 2023 17:57:35 -0500 Subject: [PATCH 01/10] main (#5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Publish - @segment/actions-cli-internal@3.123.2 - @segment/actions-cli@3.123.2 - @segment/action-destinations@3.127.2 * Subscribe to more events (Redo without required type) (#986) * Add subscriptions to track event * Generate types and new snapshot * Add test * Improve labels and descriptions * Generated types * Change description * Generate types * Remove required * Generate types * Update Ripe web destination (#968) * Update Ripe web destination - Add new endpoint setting for testing purposes - Remove alias call - Update misleading anonymousId descriptions * Update erroneous default paths * Set anonymousId in identify call * Heap 34916 - add session_id + update segment library for tracking purposes (#787) * Fix events payload * Use the single event not the bulk * Fix tests * Fix should not override * remove console log and update SEGMENT_LIB var * update constant value * update browser tests as well * Adding Group support for customerio -Rename identifier field names (#973) * Initial commit for objects * Added Test cases * Adding Tests validation for the payload * committing generate type file * Adding group support from identify * Fixing conflicts * Adding traits to attributes property for createUpdateObject action * renaming id and type_id to object_id and object_type_id Co-authored-by: kishoredevarasettyn * SalesWings (Actions) Destination (#945) * Generated integration from scaffold * Fix action name * Implement SalesWings destination actions * Send user agent, rearrange fields * Bugfixes * Remove debug logging * First tests * Auth tests & track event tests * Page event tests * Identify event tests * Screen event tests * Event batch test * More event batch tests * Change API key description * Commit generated types * Minor cleanup * Fix square brackets in field description UI * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Hardcoding timestamps for snapshot tests * Extract email from properties of Track event * Add action description * Add default subscription to action * Add destination present * Merge URL fields * Dedicated actions per event type * Cleanup * Update field descriptions * Update geenrated types Co-authored-by: Yevgeny Terov <73266004+yevsw@users.noreply.github.com> Co-authored-by: Marín Alcaraz Co-authored-by: Yevgeny Terov * Changing default subscription to group for group call (#995) * Initial commit for objects * Added Test cases * Adding Tests validation for the payload * committing generate type file * Adding group support from identify * Fixing conflicts * Adding traits to attributes property for createUpdateObject action * setting default subscription as group for createUpdateObject and addressing other review comments * correcting merge overrides Co-authored-by: kishoredevarasettyn * Add anonymous id as a user property (#981) * Update setting description in Google Ads Conversions (#983) * HGI-237 | Updated Description for Braze Cohorts Fields (#992) * udpated description for braze cohorts * updated description for braze cohorts * updated description in mapping fields * updated description * updated event_properties to hidden * made event_properties unhidden Co-authored-by: Gaurav Kochar * Increase CI timeout to 15 minutes and 10 minutes respectively (#985) * Increase CI timeout to 15 minutes * Bump browser tests to 10 minutes Co-authored-by: Nolan Chan * Pipedrive actions PE-20 (#996) * fix for pipedrive pe-20 issue * removing default visible_to * Register Saleswing Action (#999) Co-authored-by: Nolan Chan * Publish - @segment/browser-destinations@3.72.0 - @segment/actions-cli-internal@3.124.0 - @segment/actions-cli@3.124.0 - @segment/action-destinations@3.128.0 * ACT-362 Brackets Support (#993) * add support for brackets inside js keys in get method * add double quotes * explanatory text + new link * safari support * remove invalid bracket test since it is now supported * use class based regex to avoid parseError * actually convert the regex correctly * cleanup * split tests by functionality * Refactor .get test (#1000) * Heap Fix for empty event name (#1004) * fix for pe-52 * fixing breaking tests * Publish - @segment/actions-shared@1.32.0 - @segment/browser-destinations@3.73.0 - @segment/actions-cli-internal@3.125.0 - @segment/actions-cli@3.125.0 - @segment/actions-core@3.50.0 - @segment/action-destinations@3.129.0 * fix scaffolding for oauth (#1008) * [CHANNELS-329] Add WhatsApp support for Twilio Engage (#987) * feat: added whatsapp support * fix: added missing dependencies * refactor: minor cleanup * fix: moved dependency to package level * fix: uri encoding for get traits * fix: using same auth scheme for sms & whatsapp whatsApp thankfully allows using apiKeySid & apiSecret instead of accountSid & authToken * fix: reverted changed package version * feat: allow bypassing contentVariables reconciliation * Publish - @segment/actions-cli-internal@3.126.0 - @segment/actions-cli@3.126.0 - @segment/action-destinations@3.130.0 * Add browser destination tests with saucelabs (#994) * Node 18 Upgrade (#991) * packages * ci + nvm * lock for 18 * fix webpack hashing issue * node types to 18 * Update node version for browser-tests and snyk * Fix tests * Try to fix browser tests * Fix pipedrive unit tests * Fix domain in snapshot tests * Fix yarn subscriptions * Update README --------- Co-authored-by: Dan Lasky * Use node18 for browser tests destinations (#1014) * Contribution pe 53 (#1007) * updating contributing guidelines * adding extra instructions for post deployment changes * spelling corrections * spelling corrections * Update CONTRIBUTING.md Co-authored-by: SyedWasiHaider * Update CONTRIBUTING.md Co-authored-by: SyedWasiHaider --------- Co-authored-by: SyedWasiHaider * Qualtrics upsert transaction (#963) * adding upsert contact transacion destination * fixing snapshots * Updating perform function for upsertTransaction * Adding dynamic fields for directoryId. Updating field descriptions. Update upsertTransaction defaultSubscription * updating types * Update qualtrics destination name and descriptons on actions --------- Co-authored-by: Carl Lee * fixing a couple of issues with new Ironclad destination (#1002) * fixing a couple of issues with new Ironclad destination * adding updated generated types * fixing broken test * [salesforce] - Verify the `instanceUrl` is a valid Salesforce domain (#997) * Regex and WIP unit tests * Unit tests working * Updates regex and unit tests * Updates other unit tests * Saving package.json * Adds a couple more unit tests * Removes package.json from commits * Removes package.json from commits * Imports request client using absolute path instead of relative path * Enforce https * Publish - @segment/actions-shared@1.33.0 - @segment/browser-destinations-integration-tests@0.1.0 - @segment/browser-destinations@3.74.0 - @segment/actions-cli-internal@3.127.0 - @segment/actions-cli@3.127.0 - @segment/actions-core@3.51.0 - @segment/action-destinations@3.131.0 - @segment/destination-subscriptions@3.15.0 * Fix CommandBar browser destination initialization when CommandBar has already been loaded through other means (#1009) Co-authored-by: Thomas Kainrad * remove flow that attempts to create a JIRA ticket (#1021) * Twilio Studio as a Segment Action Destination (#1023) * Twilio Studio as a Segment Action Destination * Replaced phone number with userid in the cache key * Addressed review comments * DOTORG-839: Blackbaud Raiser's Edge NXT Destination (#998) * DOTORG-839: Create or Update Individual Constituent Action (#1) * DOTORG-839 Added OAuth2 settings for Blackbaud (#2) * Move bbApiSubscriptionKey to settings * Only aggregate integrationErrors * Update Online Presence label * Update directory structure * Add types * Abstract API calls * Add dateStringToFuzzyDate * Add types * Don't retry 401s * Don't catch errors on constituent search or creation * Concatenate integrationErrors * Add throwHttpErrors * Set default for lookup_id to userId * Pass constituentId to updateConstituent * Remove try/catch * Use camelCase traits * Add filterObjectListByMatchFields * Check if primary property is defined * DOTORG-839 Added authentication test (#3) * Don't match on country * Use datetime type * Strip non-numeric characters from phone when matching * Don't match on undefined boolean fields * Update generated-types.ts * Fix linting errors * Move fixtures out of tests directory * Update constituentData * Update default lookup_id mapping * Update testAuthentication * Remove UNEXPECTED_RECORD_COUNT error * Update tests --------- Co-authored-by: twilio-hwong <91703194+twilio-hwong@users.noreply.github.com> --------- Co-authored-by: Nick Aguilar Co-authored-by: Stella Chung Co-authored-by: Simon Co-authored-by: A Murphy Co-authored-by: kishoredevarasettyn <97026912+kishoredevarasettyn@users.noreply.github.com> Co-authored-by: kishoredevarasettyn Co-authored-by: Denis Egorushkin <98813888+denis-egorushkin-sw@users.noreply.github.com> Co-authored-by: Yevgeny Terov <73266004+yevsw@users.noreply.github.com> Co-authored-by: Marín Alcaraz Co-authored-by: Yevgeny Terov Co-authored-by: maryamsharif <99763167+maryamsharif@users.noreply.github.com> Co-authored-by: Innovative-GauravKochar <117165746+Innovative-GauravKochar@users.noreply.github.com> Co-authored-by: Gaurav Kochar Co-authored-by: Nolan Chan Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> Co-authored-by: Nolan Chan Co-authored-by: Dan Co-authored-by: Seth Silesky <5115498+silesky@users.noreply.github.com> Co-authored-by: rhall-twilio <103517471+rhall-twilio@users.noreply.github.com> Co-authored-by: alfrimpong <119889384+alfrimpong@users.noreply.github.com> Co-authored-by: SyedWasiHaider Co-authored-by: Dan Lasky Co-authored-by: drakauskas <119876674+drakauskas@users.noreply.github.com> Co-authored-by: Carl Lee Co-authored-by: Wasi Haider Co-authored-by: Thomas Kainrad <7394822+tkainrad@users.noreply.github.com> Co-authored-by: Thomas Kainrad Co-authored-by: aradhakrishnan-twilio <116877054+aradhakrishnan-twilio@users.noreply.github.com> Co-authored-by: twilio-hwong <91703194+twilio-hwong@users.noreply.github.com> --- .eslintrc.js | 6 + .github/workflows/ci.yml | 63 +- .github/workflows/ext.yml | 2 +- .github/workflows/pr2jira.yml | 32 - .gitignore | 5 +- .nvmrc | 2 +- CONTRIBUTING.md | 46 +- README.md | 4 +- docs/testing.md | 5 + package.json | 9 +- packages/actions-shared/package.json | 6 +- packages/actions-shared/tsconfig.json | 2 +- .../.gitignore | 1 + .../README.md | 33 + .../package.json | 26 + .../src/pageobjects/page.ts | 11 + .../src/server/config.ts | 5 + .../src/server/destination-server.ts | 40 + .../src/server/server.ts | 30 + .../src/server/start-destination-server.ts | 4 + .../src/server/utils.ts | 21 + .../src/tests/browser-destinations.test.ts | 25 + .../tsconfig.json | 16 + .../wdio.conf.local.ts | 98 + .../wdio.conf.sauce.ts | 41 + packages/browser-destinations/package.json | 12 +- .../src/destinations/commandbar/index.ts | 10 +- .../src/destinations/heap/constants.ts | 2 +- .../heap/trackEvent/__tests__/index.test.ts | 8 +- .../heap/trackEvent/generated-types.ts | 4 + .../src/destinations/heap/trackEvent/index.ts | 18 +- .../ripe/alias/__tests__/index.test.ts | 100 - .../ripe/alias/generated-types.ts | 16 - .../src/destinations/ripe/alias/index.ts | 46 - .../src/destinations/ripe/generated-types.ts | 4 + .../ripe/group/generated-types.ts | 2 +- .../src/destinations/ripe/group/index.ts | 2 +- .../ripe/identify/__tests__/index.test.ts | 6 + .../ripe/identify/generated-types.ts | 2 +- .../src/destinations/ripe/identify/index.ts | 7 +- .../src/destinations/ripe/index.ts | 19 +- .../src/destinations/ripe/init-script.ts | 2 +- .../destinations/ripe/page/generated-types.ts | 2 +- .../src/destinations/ripe/page/index.ts | 20 +- .../ripe/track/generated-types.ts | 2 +- .../src/destinations/ripe/track/index.ts | 4 +- .../src/destinations/ripe/types.ts | 3 +- .../test/setup-after-env.ts | 10 + packages/browser-destinations/tsconfig.json | 3 +- packages/cli-internal/package.json | 11 +- packages/cli/package.json | 13 +- .../destinations/oauth2-auth/index.ts | 2 +- packages/core/package.json | 8 +- .../src/__tests__/destination-kit.test.ts | 2 +- packages/core/src/__tests__/get.iso.test.ts | 56 +- packages/core/src/get.ts | 25 +- .../mapping-kit/__tests__/index.iso.test.ts | 14 - packages/core/tsconfig.json | 2 +- packages/core/tsconfig.karma.json | 2 +- packages/destination-actions/package.json | 11 +- .../__snapshots__/snapshot.test.ts.snap | 21 + .../__tests__/index.test.ts | 21 + .../__tests__/snapshot.test.ts | 81 + .../blackbaud-raisers-edge-nxt/api/index.ts | 156 + .../constants/index.ts | 2 + .../__snapshots__/snapshot.test.ts.snap | 21 + .../__tests__/index.test.ts | 601 ++++ .../__tests__/snapshot.test.ts | 79 + .../fixtures.ts | 169 ++ .../generated-types.ts | 67 + .../index.ts | 751 +++++ .../generated-types.ts | 8 + .../blackbaud-raisers-edge-nxt/index.ts | 61 + .../blackbaud-raisers-edge-nxt/types/index.ts | 78 + .../blackbaud-raisers-edge-nxt/utils/index.ts | 48 + .../syncAudiences/generated-types.ts | 6 +- .../braze-cohorts/syncAudiences/index.ts | 8 +- .../__tests__/createUpdateObject.test.ts | 24 +- .../__tests__/createUpdatePerson.test.ts | 2 +- .../createUpdateObject/generated-types.ts | 2 +- .../customerio/createUpdateObject/index.ts | 11 +- .../customerio/createUpdatePerson/index.ts | 4 +- .../__tests__/send-whatsapp.test.ts | 379 +++ .../engage-messaging-twilio/index.ts | 4 +- .../engage-messaging-twilio/sendSms/index.ts | 147 +- .../sendSms/sms-sender.ts | 80 + .../sendWhatsApp/generated-types.ts | 67 + .../sendWhatsApp/index.ts | 127 + .../sendWhatsApp/whatsapp-sender.ts | 89 + .../utils/message-sender.ts | 141 + .../generated-types.ts | 2 +- .../google-enhanced-conversions/index.ts | 2 +- .../__snapshots__/snapshot.test.ts.snap | 10 +- .../src/destinations/heap/constants.ts | 2 +- .../src/destinations/heap/index.ts | 2 +- .../__snapshots__/snapshot.test.ts.snap | 10 +- .../heap/trackEvent/__tests__/index.test.ts | 30 +- .../heap/trackEvent/generated-types.ts | 16 +- .../src/destinations/heap/trackEvent/index.ts | 79 +- .../src/destinations/index.ts | 1 + .../RecordAction/__tests__/index.test.ts | 2 +- .../ironclad/RecordAction/generated-types.ts | 4 +- .../ironclad/RecordAction/index.ts | 7 +- .../destinations/ironclad/generated-types.ts | 4 +- .../src/destinations/ironclad/index.ts | 3 - .../__tests__/snapshot.test.ts | 3 + .../createUpdateActivity/generated-types.ts | 4 +- .../pipedrive/createUpdateActivity/index.ts | 64 +- .../__tests__/snapshot.test.ts | 5 +- .../pipedrive/createUpdateDeal/index.ts | 49 +- .../createUpdateLead/__tests__/index.test.ts | 14 +- .../__tests__/snapshot.test.ts | 6 +- .../createUpdateLead/generated-types.ts | 20 +- .../pipedrive/createUpdateLead/index.ts | 88 +- .../__tests__/snapshot.test.ts | 5 +- .../createUpdateNote/generated-types.ts | 6 +- .../pipedrive/createUpdateNote/index.ts | 38 +- .../__tests__/index.test.ts | 10 +- .../__tests__/snapshot.test.ts | 7 +- .../createUpdateOrganization/index.ts | 13 +- .../__tests__/index.test.ts | 10 +- .../__tests__/snapshot.test.ts | 11 +- .../pipedrive/createUpdatePerson/index.ts | 20 +- .../src/destinations/pipedrive/index.ts | 24 +- .../pipedrive/pipedriveApi/leads.ts | 7 +- .../pipedrive/pipedriveApi/notes.ts | 2 +- .../pipedriveApi/pipedrive-client.ts | 9 +- .../__snapshots__/snapshot.test.ts.snap | 23 + .../qualtrics/__tests__/index.test.ts | 6 +- .../qualtrics/__tests__/snapshot.test.ts | 13 +- .../qualtrics/addContactToXmd/index.ts | 13 +- .../destinations/qualtrics/dynamicFields.ts | 41 + .../src/destinations/qualtrics/index.ts | 10 +- .../qualtrics/qualtricsApiClient.ts | 114 +- .../qualtrics/triggerXflowWorkflow/index.ts | 4 +- .../__snapshots__/snapshot.test.ts.snap | 24 + .../__tests__/index.test.ts | 239 ++ .../__tests__/snapshot.test.ts | 85 + .../generated-types.ts | 60 + .../upsertContactTransaction/index.ts | 212 ++ .../src/destinations/qualtrics/utils.ts | 49 + .../salesforce/__tests__/account.test.ts | 2 +- .../salesforce/__tests__/cases.test.ts | 2 +- .../salesforce/__tests__/contact.test.ts | 2 +- .../salesforce/__tests__/customObject.test.ts | 2 +- .../salesforce/__tests__/lead.test.ts | 2 +- .../salesforce/__tests__/opportunity.test.ts | 2 +- .../__tests__/sf-operations.test.ts | 2 +- .../salesforce/__tests__/sf-utils.test.ts | 46 + .../destinations/salesforce/sf-operations.ts | 6 +- .../src/destinations/salesforce/sf-utils.ts | 23 + .../saleswings/__tests__/index.test.ts | 21 + .../src/destinations/saleswings/api.ts | 46 + .../src/destinations/saleswings/common.ts | 38 + .../src/destinations/saleswings/converter.ts | 30 + .../src/destinations/saleswings/fields.ts | 109 + .../saleswings/generated-types.ts | 8 + .../src/destinations/saleswings/index.ts | 68 + .../__snapshots__/snapshot.test.ts.snap | 57 + .../__tests__/index.test.ts | 135 + .../__tests__/snapshot.test.ts | 83 + .../submitIdentifyEvent/generated-types.ts | 46 + .../saleswings/submitIdentifyEvent/index.ts | 43 + .../__snapshots__/snapshot.test.ts.snap | 39 + .../submitPageEvent/__tests__/index.test.ts | 138 + .../__tests__/snapshot.test.ts | 77 + .../submitPageEvent/generated-types.ts | 28 + .../saleswings/submitPageEvent/index.ts | 38 + .../__snapshots__/snapshot.test.ts.snap | 62 + .../submitScreenEvent/__tests__/index.test.ts | 135 + .../__tests__/snapshot.test.ts | 79 + .../submitScreenEvent/generated-types.ts | 46 + .../saleswings/submitScreenEvent/index.ts | 45 + .../__snapshots__/snapshot.test.ts.snap | 62 + .../submitTrackEvent/__tests__/index.test.ts | 268 ++ .../__tests__/snapshot.test.ts | 77 + .../submitTrackEvent/generated-types.ts | 46 + .../saleswings/submitTrackEvent/index.ts | 45 + .../src/destinations/saleswings/testing.ts | 49 + .../__tests__/triggerStudioFlow.test.ts | 204 ++ .../twilio-studio/generated-types.ts | 20 + .../src/destinations/twilio-studio/index.ts | 59 + .../triggerStudioFlow/generated-types.ts | 28 + .../twilio-studio/triggerStudioFlow/index.ts | 121 + .../src/destinations/twilio-studio/utils.ts | 81 + packages/destination-actions/tsconfig.json | 2 +- .../destination-subscriptions/package.json | 2 +- .../src/__tests__/fql.test.ts | 2 +- .../destination-subscriptions/tsconfig.json | 6 +- scripts/clean.sh | 8 + tsconfig.json | 2 +- yarn.lock | 2534 ++++++++++++++++- 192 files changed, 9969 insertions(+), 827 deletions(-) delete mode 100644 .github/workflows/pr2jira.yml create mode 100644 packages/browser-destinations-integration-tests/.gitignore create mode 100644 packages/browser-destinations-integration-tests/README.md create mode 100644 packages/browser-destinations-integration-tests/package.json create mode 100644 packages/browser-destinations-integration-tests/src/pageobjects/page.ts create mode 100644 packages/browser-destinations-integration-tests/src/server/config.ts create mode 100644 packages/browser-destinations-integration-tests/src/server/destination-server.ts create mode 100644 packages/browser-destinations-integration-tests/src/server/server.ts create mode 100644 packages/browser-destinations-integration-tests/src/server/start-destination-server.ts create mode 100644 packages/browser-destinations-integration-tests/src/server/utils.ts create mode 100644 packages/browser-destinations-integration-tests/src/tests/browser-destinations.test.ts create mode 100644 packages/browser-destinations-integration-tests/tsconfig.json create mode 100644 packages/browser-destinations-integration-tests/wdio.conf.local.ts create mode 100644 packages/browser-destinations-integration-tests/wdio.conf.sauce.ts delete mode 100644 packages/browser-destinations/src/destinations/ripe/alias/__tests__/index.test.ts delete mode 100644 packages/browser-destinations/src/destinations/ripe/alias/generated-types.ts delete mode 100644 packages/browser-destinations/src/destinations/ripe/alias/index.ts create mode 100644 packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/__tests__/__snapshots__/snapshot.test.ts.snap create mode 100644 packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/__tests__/snapshot.test.ts create mode 100644 packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/api/index.ts create mode 100644 packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/constants/index.ts create mode 100644 packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/__tests__/__snapshots__/snapshot.test.ts.snap create mode 100644 packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/__tests__/snapshot.test.ts create mode 100644 packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/fixtures.ts create mode 100644 packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/index.ts create mode 100644 packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/index.ts create mode 100644 packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/types/index.ts create mode 100644 packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/utils/index.ts create mode 100644 packages/destination-actions/src/destinations/engage-messaging-twilio/__tests__/send-whatsapp.test.ts create mode 100644 packages/destination-actions/src/destinations/engage-messaging-twilio/sendSms/sms-sender.ts create mode 100644 packages/destination-actions/src/destinations/engage-messaging-twilio/sendWhatsApp/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/engage-messaging-twilio/sendWhatsApp/index.ts create mode 100644 packages/destination-actions/src/destinations/engage-messaging-twilio/sendWhatsApp/whatsapp-sender.ts create mode 100644 packages/destination-actions/src/destinations/engage-messaging-twilio/utils/message-sender.ts create mode 100644 packages/destination-actions/src/destinations/qualtrics/dynamicFields.ts create mode 100644 packages/destination-actions/src/destinations/qualtrics/upsertContactTransaction/__tests__/__snapshots__/snapshot.test.ts.snap create mode 100644 packages/destination-actions/src/destinations/qualtrics/upsertContactTransaction/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/qualtrics/upsertContactTransaction/__tests__/snapshot.test.ts create mode 100644 packages/destination-actions/src/destinations/qualtrics/upsertContactTransaction/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/qualtrics/upsertContactTransaction/index.ts create mode 100644 packages/destination-actions/src/destinations/qualtrics/utils.ts create mode 100644 packages/destination-actions/src/destinations/saleswings/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/saleswings/api.ts create mode 100644 packages/destination-actions/src/destinations/saleswings/common.ts create mode 100644 packages/destination-actions/src/destinations/saleswings/converter.ts create mode 100644 packages/destination-actions/src/destinations/saleswings/fields.ts create mode 100644 packages/destination-actions/src/destinations/saleswings/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/saleswings/index.ts create mode 100644 packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/__tests__/__snapshots__/snapshot.test.ts.snap create mode 100644 packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/__tests__/snapshot.test.ts create mode 100644 packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/index.ts create mode 100644 packages/destination-actions/src/destinations/saleswings/submitPageEvent/__tests__/__snapshots__/snapshot.test.ts.snap create mode 100644 packages/destination-actions/src/destinations/saleswings/submitPageEvent/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/saleswings/submitPageEvent/__tests__/snapshot.test.ts create mode 100644 packages/destination-actions/src/destinations/saleswings/submitPageEvent/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/saleswings/submitPageEvent/index.ts create mode 100644 packages/destination-actions/src/destinations/saleswings/submitScreenEvent/__tests__/__snapshots__/snapshot.test.ts.snap create mode 100644 packages/destination-actions/src/destinations/saleswings/submitScreenEvent/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/saleswings/submitScreenEvent/__tests__/snapshot.test.ts create mode 100644 packages/destination-actions/src/destinations/saleswings/submitScreenEvent/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/saleswings/submitScreenEvent/index.ts create mode 100644 packages/destination-actions/src/destinations/saleswings/submitTrackEvent/__tests__/__snapshots__/snapshot.test.ts.snap create mode 100644 packages/destination-actions/src/destinations/saleswings/submitTrackEvent/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/saleswings/submitTrackEvent/__tests__/snapshot.test.ts create mode 100644 packages/destination-actions/src/destinations/saleswings/submitTrackEvent/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/saleswings/submitTrackEvent/index.ts create mode 100644 packages/destination-actions/src/destinations/saleswings/testing.ts create mode 100644 packages/destination-actions/src/destinations/twilio-studio/__tests__/triggerStudioFlow.test.ts create mode 100644 packages/destination-actions/src/destinations/twilio-studio/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/twilio-studio/index.ts create mode 100644 packages/destination-actions/src/destinations/twilio-studio/triggerStudioFlow/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/twilio-studio/triggerStudioFlow/index.ts create mode 100644 packages/destination-actions/src/destinations/twilio-studio/utils.ts create mode 100644 scripts/clean.sh diff --git a/.eslintrc.js b/.eslintrc.js index dc846c5bd5..511bede27f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -76,6 +76,12 @@ module.exports = { rules: { '@typescript-eslint/no-var-requires': 'off' } + }, + { + files: ['packages/browser-destinations-integration-tests/**/*.ts'], + rules: { + '@typescript-eslint/no-unsafe-call': 'off' + } } ] } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05fdd7134f..4d03a8f6a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,11 +10,11 @@ jobs: test-and-build: runs-on: ubuntu-20.04 - timeout-minutes: 10 + timeout-minutes: 15 strategy: matrix: - node-version: [14.x] + node-version: [18.x] steps: - uses: actions/checkout@v2 @@ -65,14 +65,63 @@ jobs: yarn subscriptions size fi - browser-tests: + browser-tests-destination: + env: + SAUCE_USERNAME: ${{secrets.SAUCE_USERNAME}} + SAUCE_ACCESS_KEY: ${{secrets.SAUCE_ACCESS_KEY}} + runs-on: ubuntu-20.04 - timeout-minutes: 5 + timeout-minutes: 20 + + strategy: + matrix: + node-version: [18.x] + + steps: + - uses: actions/checkout@master + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + registry-url: 'https://registry.npmjs.org' + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - uses: actions/cache@v2 + id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Install Dependencies + run: yarn install --frozen-lockfile + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Build + run: NODE_ENV=production yarn lerna run build --scope=@segment/browser-destinations --include-dependencies --stream + + - name: Run Saucelabs Tests + working-directory: packages/browser-destinations-integration-tests + shell: bash + run: | + yarn start-destination-server & + yarn test:sauce + + browser-tests-core: + runs-on: ubuntu-20.04 + + timeout-minutes: 10 strategy: matrix: - node-version: [14.x] + node-version: [18.x] steps: - uses: actions/checkout@v2 @@ -107,7 +156,7 @@ jobs: run: npx playwright install-deps - name: Build - run: NODE_ENV=production yarn build + run: NODE_ENV=production yarn lerna run build --scope=@segment/actions-core --include-dependencies --stream - name: Browser Test run: yarn test-browser @@ -119,7 +168,7 @@ jobs: strategy: matrix: - node-version: [14.x] + node-version: [18.x] steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/ext.yml b/.github/workflows/ext.yml index 7d259ee5fc..1bd6b35484 100644 --- a/.github/workflows/ext.yml +++ b/.github/workflows/ext.yml @@ -14,7 +14,7 @@ jobs: strategy: matrix: - node-version: [14.x] + node-version: [18.x] steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/pr2jira.yml b/.github/workflows/pr2jira.yml deleted file mode 100644 index 1f1a5126d2..0000000000 --- a/.github/workflows/pr2jira.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Track External PRs in Jira - -on: - pull_request_target: - types: [opened] - -jobs: - create-jira: - runs-on: ubuntu-20.04 - name: Create a ticket in Jira to track the pull request - if: github.event.pull_request.head.repo.fork - steps: - - name: Login - uses: atlassian/gajira-login@master - env: - JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} - JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} - JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} - - name: Create - id: create - uses: atlassian/gajira-create@master - with: - project: BE - issuetype: Task - summary: | - External PR: ${{ github.event.pull_request.title }} - description: | - See: ${{ github.event.pull_request.html_url }} - fields: '{"customfield_10002": "BE-132" }' - - - name: Log created issue - run: echo "Issue ${{ steps.create.outputs.issue }} was created" diff --git a/.gitignore b/.gitignore index 72d63c61fd..35b316e330 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,7 @@ package-lock.json .env # JetBrains byproduct .idea -coverage \ No newline at end of file +coverage + +# playwright +playwright-report diff --git a/.nvmrc b/.nvmrc index 898c8715b1..72c7744b30 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -14.17 +18.12.1 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bee24cd963..40b75bb09c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,6 +14,7 @@ Before continuing, please make sure to read our [Code of Conduct](./CODE_OF_COND 6. [Provide catalog metadata](#provide-integration-metadata-for-the-catalog) 7. [Release to Private Beta](#release-to-private-beta-for-customer-testing) 8. [Release to Public](#release-to-public-in-the-segment-catalog) +9. [Submitting subsequent changes](#submitting-changes-after-your-integration-is-already-live) ## Become a Segment Partner @@ -62,27 +63,28 @@ Before continuing, please make sure to read our [Code of Conduct](./CODE_OF_COND 2. Your PR is merged! - Congratulations! Once your PR is merged by a Segment developer, they will deploy your changes and notify you when it’s publicly available. If the destination is in private beta, our folks at Segment will provide a link to access your destination. Once the destination is ready for general availability and has been approved, the destination will be visible from the catalog itself. - - *Note*: we currently do weekly deploys on Wednesdays for all non-emergency changes. Changes should be approved and merged by Tuesday EOD to make the Wednesday release. Thank you! + - _Note_: we currently do weekly deploys on Wednesdays for all non-emergency changes. Changes should be approved and merged by Tuesday EOD to make the Wednesday release. Thank you! ## Write documentation Documentation ensures users of your destination can enable and configure the destination, and understand how it interacts with your platform. -1. Write your integration’s documentation. Segment provides two templates: [doc-template-new.md](./docs/doc-template-new.md) for new destinations, and [doc-template-update.md](./docs/doc-template-update.md) for updates to existing destinations. +1. Write your integration’s documentation. Segment provides two templates: [doc-template-new.md](./docs/doc-template-new.md) for new destinations, and [doc-template-update.md](./docs/doc-template-update.md) for updates to existing destinations. These templates contain content that automatically pulls in information. Do not edit this content. - - The table at the top is the yaml front matter, and it is not rendered in the final documentation. - - The snippet `{% include content/plan-grid.md name="actions" %}` indicates which Segment account tiers have access to Destination Actions; all account tiers have access. - - The snippet `{% include content/ajs-upgrade.md %}` is a note to encourage customers to upgrade to Analytics.js 2.0. - - The snippet `{% include components/actions-fields.html %}` will automatically populate information about your destination’s Settings, Mappings, Actions, and Action fields, using Segment's Public API. This information will be populated as soon as your destination reaches the Public Beta phase. This means you don't need to include any of this information in your documentation. + +- The table at the top is the yaml front matter, and it is not rendered in the final documentation. +- The snippet `{% include content/plan-grid.md name="actions" %}` indicates which Segment account tiers have access to Destination Actions; all account tiers have access. +- The snippet `{% include content/ajs-upgrade.md %}` is a note to encourage customers to upgrade to Analytics.js 2.0. +- The snippet `{% include components/actions-fields.html %}` will automatically populate information about your destination’s Settings, Mappings, Actions, and Action fields, using Segment's Public API. This information will be populated as soon as your destination reaches the Public Beta phase. This means you don't need to include any of this information in your documentation. These templates contain sections that you should edit to explain the following: - - The purpose of the destination - - Benefits / features of the destination - - Steps to add and configure the destination within Segment (replace the destination name with your destination) - - Breaking changes compared to a classic version of the destination (if applicable) - - Migration steps (if applicable) +- The purpose of the destination +- Benefits / features of the destination +- Steps to add and configure the destination within Segment (replace the destination name with your destination) +- Breaking changes compared to a classic version of the destination (if applicable) +- Migration steps (if applicable) To help you write your documentation, see examples of documentation for other destinations: [Slack (Actions) Destination](https://segment.com/docs/connections/destinations/catalog/actions-slack/), [TikTok Conversions Destination](https://segment.com/docs/connections/destinations/catalog/tiktok-conversions/). @@ -122,3 +124,25 @@ Please find the below info for _Name of integration_ Catalog entry. 2. Write a blog post for your company’s blog, write a [recipe](https://segment.com/recipes/) to help customers solve a specific problem using your Integration, and/or work with our Marketing team to be featured in the Segment blog. 3. Maintain your integration. Fix bugs, update it if your APIs change, add functionality as requested by customers. + +## Submitting changes after your Integration is already live + +After your Integration is live and in use by customers you will still be able to make changes to your Integration code. However, an extra level of governance and oversight is required in order to avoid causing issues for customers who are already using your Integration. + +Please observe the dos and dont's listed below: + +**Please do not:** + +1. Do not add a **required** field to an Integration which is already in use by customers. Doing so will prevent existing instances of your Integration from working, and will lead to an incident. + +2. Do not change your perform() or batchPerform() functions so that they now depend on a value from a field in order for the outbound API call to be successful. Doing so may cause pre-existing Integrations to fail. + +3. Do not raise a PR containing changes for more than 1 Integration. For example if you need to make changes to a Cloud Mode and Device Mode Integration you should raise separate PRs. + +4. Do not change the name or slug of your Integration after the Integration has been deployed. If you do want to change the name of your Integration please email partner-support@segment.com. + +5. Do not delete an Action from an Integration. This capability is not yet supported by our Framework. If you do want to do this please email partner-support@segment.com. + +**Please do the following:** + +1. If adding a new field or amending the configuration of an existing field, please attach a video to the PR of you testing the change using the [Action Tester](./docs/testing.md). Try to use realistic Segment API payloads as inputs, and if possible show how the payloads are reflected in your Destination platform. diff --git a/README.md b/README.md index 111c3582dd..61f65a90c3 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ This is a monorepo with multiple packages leveraging [`lerna`](https://github.co You'll need to have some tools installed locally to build and test action destinations. - Yarn 1.x -- Node 14.17 (latest LTS, we recommand using [`nvm`](https://github.com/nvm-sh/nvm) for managing Node versions) +- Node 18.12 (latest LTS, we recommand using [`nvm`](https://github.com/nvm-sh/nvm) for managing Node versions) If you are a Segment employee you can directly `git clone` the repository locally. Otherwise you'll want to fork this repository for your organization to submit Pull Requests against the main Segment repository. Once you've got a fork, you can `git clone` that locally. @@ -62,7 +62,7 @@ cd action-destinations npm login yarn login -# Requires node 14.17, optionally: nvm use 14.17 +# Requires node 18.12.1, optionally: nvm use 18.12.1 yarn --ignore-optional yarn bootstrap yarn build diff --git a/docs/testing.md b/docs/testing.md index 6a76ad96a9..b4709ab647 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -10,6 +10,7 @@ - [Examples](#examples) - [Snapshot Testing](#snapshot-testing) - [Code Coverage](#code-coverage) +- [Post Deployment Change Testing](#post-deployment-change-testing) ## Actions Tester @@ -195,3 +196,7 @@ yarn jest --testPathPattern='./packages/destination-actions/src/destinations/ { + await browser.url(`${browser.options.baseUrl}?destination=${destination}`) + + await browser.waitUntil(() => browser.execute(() => document.readyState === 'complete'), { + timeout: 10000 + }) + } +} + +export default new Page() diff --git a/packages/browser-destinations-integration-tests/src/server/config.ts b/packages/browser-destinations-integration-tests/src/server/config.ts new file mode 100644 index 0000000000..7f2738d49b --- /dev/null +++ b/packages/browser-destinations-integration-tests/src/server/config.ts @@ -0,0 +1,5 @@ +export const config = { + get destinationTestServerPort(): number { + return 5555 + } +} diff --git a/packages/browser-destinations-integration-tests/src/server/destination-server.ts b/packages/browser-destinations-integration-tests/src/server/destination-server.ts new file mode 100644 index 0000000000..e3e3eab642 --- /dev/null +++ b/packages/browser-destinations-integration-tests/src/server/destination-server.ts @@ -0,0 +1,40 @@ +import { startServer } from './server' +import { listDestinations, DESTINATIONS_DIST_WEB } from './utils' +import express from 'express' +import path from 'path' + +const htmlWithScript = (src: string): string => ` + + + + + + Script found: "${src}" + + +` + +export const startDestinationServer = (...args: Parameters): ReturnType => { + const destinations = listDestinations() + return startServer(...args).then(([app, server]) => { + app.use('/js', express.static(path.join(DESTINATIONS_DIST_WEB))) + app.get('/', (req, res) => { + console.log('req!', req.url) + const { destination } = req.query + if (!destination) { + return res.status(400).send('No destination param passed.') + } + const foundDestination = destinations.find((d) => d.dirPath === destination) + if (!foundDestination) { + return res.status(404).send('Cannot find destination.') + } + res.send(htmlWithScript(path.join('js', foundDestination.dirPath, foundDestination.fileName))) + }) + app.get('/destinations', (_, res) => { + const dirNames = destinations.map((d) => d.dirPath) + res.json(dirNames) + }) + + return [app, server] + }) +} diff --git a/packages/browser-destinations-integration-tests/src/server/server.ts b/packages/browser-destinations-integration-tests/src/server/server.ts new file mode 100644 index 0000000000..6921096c1c --- /dev/null +++ b/packages/browser-destinations-integration-tests/src/server/server.ts @@ -0,0 +1,30 @@ +import express from 'express' +import { Server } from 'http' + +const onExit = (server: Server) => { + return (): void => { + console.log('closing server...') + server.close(() => { + console.log('closed gracefully!') + process.exit() + }) + setTimeout(() => { + console.log('Force closing!') + process.exit(1) + }, 400) + } +} +export const startServer = (port: string | number): Promise<[express.Application, Server]> => { + if (!port) { + throw new Error('please pass a PORT') + } + return new Promise((resolve) => { + const app = express() + const server = app.listen(port, () => { + console.log(`Listening on http://localhost:${port} in ${app.get('env')}`) + resolve([app, server]) + }) + process.on('SIGINT', onExit(server)) + process.on('SIGTERM', onExit(server)) + }) +} diff --git a/packages/browser-destinations-integration-tests/src/server/start-destination-server.ts b/packages/browser-destinations-integration-tests/src/server/start-destination-server.ts new file mode 100644 index 0000000000..c2a1eb7767 --- /dev/null +++ b/packages/browser-destinations-integration-tests/src/server/start-destination-server.ts @@ -0,0 +1,4 @@ +import { config } from './config' +import { startDestinationServer } from './destination-server' + +void startDestinationServer(config.destinationTestServerPort).catch(console.error) diff --git a/packages/browser-destinations-integration-tests/src/server/utils.ts b/packages/browser-destinations-integration-tests/src/server/utils.ts new file mode 100644 index 0000000000..b969b81b71 --- /dev/null +++ b/packages/browser-destinations-integration-tests/src/server/utils.ts @@ -0,0 +1,21 @@ +import { readdirSync } from 'fs' +import path from 'path' + +const ls = (dirPath: string) => + readdirSync(dirPath, { withFileTypes: true }) + .filter((dirent) => dirent.isDirectory()) + .map((dirent) => dirent.name) + +export const DESTINATIONS_DIST_WEB = path.join(__dirname, '../../../browser-destinations', 'dist', 'web') + +export const listDestinations = () => + ls(DESTINATIONS_DIST_WEB).map((dirPath) => { + const destinationDirPath = path.join(DESTINATIONS_DIST_WEB, dirPath) + const fileName = readdirSync(destinationDirPath).find((el) => el.endsWith('js')) + if (!fileName) throw new Error('Invariant: no .js file found.') + return { + dirPath: dirPath, + fileName, + filePath: path.join(destinationDirPath, fileName) + } + }) diff --git a/packages/browser-destinations-integration-tests/src/tests/browser-destinations.test.ts b/packages/browser-destinations-integration-tests/src/tests/browser-destinations.test.ts new file mode 100644 index 0000000000..c6c7288bb5 --- /dev/null +++ b/packages/browser-destinations-integration-tests/src/tests/browser-destinations.test.ts @@ -0,0 +1,25 @@ +import page from '../pageobjects/page' +import { expect } from 'expect' +import { listDestinations } from '../server/utils' + +const allDestinations = listDestinations().map((el) => el.dirPath) + +describe('Bundles are capable of being parsed and loaded without errors', () => { + for (const destination of allDestinations) { + it(destination, async () => { + await page.loadDestination(destination) + + // written as a string so not transpiled -- using old JS to allow testing in old browsers. + // the "return" is important for this to work on saucelabs. + const code = `return (function() { + for (var key in window) { + if (key.indexOf('Destination') !== -1 && key.indexOf('webpack') === -1) { + return typeof window[key] + } + } + })()` + const destinationGlobalType = await browser.execute(code) + expect(destinationGlobalType).toBe('function') + }) + } +}) diff --git a/packages/browser-destinations-integration-tests/tsconfig.json b/packages/browser-destinations-integration-tests/tsconfig.json new file mode 100644 index 0000000000..1c8796a66c --- /dev/null +++ b/packages/browser-destinations-integration-tests/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "moduleResolution": "node", + "module": "CommonJS", + "target": "es6", + "esModuleInterop": true, + "types": ["node", "webdriverio/async", "@wdio/mocha-framework", "expect-webdriverio", "@wdio/sauce-service"] + }, + "ts-node": { + "transpileOnly": true, + "files": true, + "compilerOptions": { + "module": "commonjs" // get rid of "Cannot use import statement outside a module" for local scripts + } + } +} diff --git a/packages/browser-destinations-integration-tests/wdio.conf.local.ts b/packages/browser-destinations-integration-tests/wdio.conf.local.ts new file mode 100644 index 0000000000..80b16db396 --- /dev/null +++ b/packages/browser-destinations-integration-tests/wdio.conf.local.ts @@ -0,0 +1,98 @@ +import type { Options } from '@wdio/types' + +export const config: Options.Testrunner = { + baseUrl: 'http://localhost:5555', + // WebdriverIO allows it to run your tests in arbitrary locations (e.g. locally or + // on a remote machine). + runner: 'local', + // ================== + // Specify Test Files + // ================== + // Define which test specs should run. The pattern is relative to the directory + // from which `wdio` was called. Notice that, if you are calling `wdio` from an + // NPM script (see https://docs.npmjs.com/cli/run-script) then the current working + // directory is where your package.json resides, so `wdio` will be called from there. + // + specs: ['./src/**/*.test.ts'], + // Patterns to exclude. + exclude: [ + // 'path/to/excluded/files' + ], + maxInstances: 10, + capabilities: [ + { + maxInstances: 5, + browserName: 'chrome', + acceptInsecureCerts: true + } + ], + // Define all options that are relevant for the WebdriverIO instance here + // + // Level of logging verbosity: trace | debug | info | warn | error | silent + logLevel: 'debug', + // + // Set specific log levels per logger + // loggers: + // - webdriver, webdriverio + // - @wdio/applitools-service, @wdio/browserstack-service, @wdio/devtools-service, @wdio/sauce-service + // - @wdio/mocha-framework, @wdio/jasmine-framework + // - @wdio/local-runner + // - @wdio/sumologic-reporter + // - @wdio/cli, @wdio/config, @wdio/sync, @wdio/utils + // Level of logging verbosity: trace | debug | info | warn | error | silent + // logLevels: { + // webdriver: 'info', + // '@wdio/applitools-service': 'info' + // }, + // + // If you only want to run your tests until a specific amount of tests have failed use + bail: 1, // Fail fast + // + // Set a base URL in order to shorten url command calls. If your `url` parameter starts + // with `/`, the base url gets prepended, not including the path portion of your baseUrl. + // If your `url` parameter starts without a scheme or `/` (like `some/path`), the base url + // gets prepended directly. + // Default timeout for all waitFor* commands. + waitforTimeout: 10000, + // + // Default timeout in milliseconds for request + // if browser driver or grid doesn't send response + // A timeout of 3 min + connectionRetryTimeout: 3 * 60 * 1000, + // + // Default request retries count + connectionRetryCount: 3, + // + // Test runner services + // Services take over a specific job you don't want to take care of. They enhance + // your test setup with almost no effort. Unlike plugins, they don't add new + // commands. Instead, they hook themselves up into the test process. + + // Framework you want to run your specs with. + // The following are supported: Mocha, Jasmine, and Cucumber + // see also: https://webdriver.io/docs/frameworks + // + // Make sure you have the wdio adapter package for the specific framework installed + // before running any tests. + framework: 'mocha', + // + // The number of times to retry the entire specfile when it fails as a whole + // specFileRetries: 1, + // + // Delay in seconds between the spec file retry attempts + // specFileRetriesDelay: 0, + // + // Whether or not retried specfiles should be retried immediately or deferred to the end of the queue + // specFileRetriesDeferred: false, + // + // Test reporter for stdout. + // The only one supported by default is 'dot' + // see also: https://webdriver.io/docs/dot-reporter + reporters: ['spec'], + // Options to be passed to Mocha. + // See the full list at http://mochajs.org/ + mochaOpts: { + ui: 'bdd', + timeout: 60000 + } +} diff --git a/packages/browser-destinations-integration-tests/wdio.conf.sauce.ts b/packages/browser-destinations-integration-tests/wdio.conf.sauce.ts new file mode 100644 index 0000000000..1ec67e1d85 --- /dev/null +++ b/packages/browser-destinations-integration-tests/wdio.conf.sauce.ts @@ -0,0 +1,41 @@ +import type { Options } from '@wdio/types' +import { config as base } from './wdio.conf.local' + +export const config: Options.Testrunner = { + ...base, + user: process.env.SAUCE_USERNAME, + key: process.env.SAUCE_ACCESS_KEY, + region: 'us', + capabilities: [ + // @ts-ignore - actually has an iterator + ...base.capabilities, + { + browserName: 'firefox', + browserVersion: 'latest', + platformName: 'Windows 11', + 'sauce:options': {} + }, + + { + browserName: 'safari', + browserVersion: '16', + platformName: 'macOS 12', + 'sauce:options': {} + }, + { + browserName: 'safari', + platformName: 'ios', + 'appium:deviceName': 'iPhone Simulator', + 'appium:platformVersion': '13.4', + 'appium:automationName': 'XCUITest' + } + ], + services: [ + [ + 'sauce', + { + sauceConnect: true + } + ] + ] +} diff --git a/packages/browser-destinations/package.json b/packages/browser-destinations/package.json index 495b7c6cee..c05cfad30c 100644 --- a/packages/browser-destinations/package.json +++ b/packages/browser-destinations/package.json @@ -1,6 +1,6 @@ { "name": "@segment/browser-destinations", - "version": "3.71.0", + "version": "3.74.0", "description": "Action based browser destinations", "author": "Netto Farah", "license": "MIT", @@ -19,8 +19,8 @@ "build": "yarn clean && yarn build-ts && yarn build-cjs && yarn build-web", "build-ts": "yarn tsc -b tsconfig.build.json", "build-cjs": "yarn tsc -p ./tsconfig.build.json -m commonjs --outDir ./dist/cjs/", - "build-web": "NODE_ENV=production ASSET_ENV=production yarn webpack -c webpack.config.js", - "build-web-stage": "NODE_ENV=production ASSET_ENV=stage yarn webpack -c webpack.config.js", + "build-web": "NODE_ENV=production ASSET_ENV=production NODE_OPTIONS=--openssl-legacy-provider yarn webpack -c webpack.config.js", + "build-web-stage": "NODE_ENV=production ASSET_ENV=stage NODE_OPTIONS=--openssl-legacy-provider yarn webpack -c webpack.config.js", "deploy-prod": "yarn build-web && aws s3 sync ./dist/web/ s3://segment-ajs-next-destinations-production/next-integrations/actions --grants read=id=$npm_config_prod_cdn_oai,id=$npm_config_prod_custom_domain_oai", "deploy-stage": "yarn build-web-stage && aws-okta exec plat-write -- aws s3 sync ./dist/web/ s3://segment-ajs-next-destinations-stage/next-integrations/actions --grants read=id=$npm_config_stage_cdn_oai,id=$npm_config_stage_custom_domain_oai", "clean": "tsc -b tsconfig.build.json --clean", @@ -34,9 +34,9 @@ "@braze/web-sdk": "npm:@braze/web-sdk@^4.1.0", "@braze/web-sdk-v3": "npm:@braze/web-sdk@^3.5.1", "@fullstory/browser": "^1.4.9", - "@segment/actions-shared": "^1.31.0", + "@segment/actions-shared": "^1.33.0", "@segment/analytics-next": "^1.29.3", - "@segment/destination-subscriptions": "^3.14.0", + "@segment/destination-subscriptions": "^3.15.0", "dayjs": "^1.10.7", "logrocket": "^3.0.1", "tslib": "^2.3.1", @@ -48,7 +48,7 @@ "@babel/plugin-transform-modules-commonjs": "^7.13.8", "@babel/preset-env": "^7.13.10", "@babel/preset-typescript": "^7.13.0", - "@segment/actions-core": "^3.49.0", + "@segment/actions-core": "^3.51.0", "@types/amplitude-js": "^7.0.1", "@types/jest": "^27.0.0", "babel-jest": "^27.3.1", diff --git a/packages/browser-destinations/src/destinations/commandbar/index.ts b/packages/browser-destinations/src/destinations/commandbar/index.ts index 4ff5912225..a17e6814e9 100644 --- a/packages/browser-destinations/src/destinations/commandbar/index.ts +++ b/packages/browser-destinations/src/destinations/commandbar/index.ts @@ -52,13 +52,11 @@ export const destination: BrowserDestinationDefinition { - const preloadedCommandBar = window.CommandBar - - initScript(settings.orgId) + if (!window.CommandBar) { + initScript(settings.orgId) + } - await deps.resolveWhen(() => { - return window.CommandBar !== preloadedCommandBar - }, 100) + await deps.resolveWhen(() => Object.prototype.hasOwnProperty.call(window, 'CommandBar'), 100) return window.CommandBar }, diff --git a/packages/browser-destinations/src/destinations/heap/constants.ts b/packages/browser-destinations/src/destinations/heap/constants.ts index cbfd822ad5..efaffb18ad 100644 --- a/packages/browser-destinations/src/destinations/heap/constants.ts +++ b/packages/browser-destinations/src/destinations/heap/constants.ts @@ -1 +1 @@ -export const HEAP_SEGMENT_LIBRARY_NAME = 'destinations-actions' +export const HEAP_SEGMENT_BROWSER_LIBRARY_NAME = 'browser-destination' diff --git a/packages/browser-destinations/src/destinations/heap/trackEvent/__tests__/index.test.ts b/packages/browser-destinations/src/destinations/heap/trackEvent/__tests__/index.test.ts index cd2ac3f1f6..09019708ad 100644 --- a/packages/browser-destinations/src/destinations/heap/trackEvent/__tests__/index.test.ts +++ b/packages/browser-destinations/src/destinations/heap/trackEvent/__tests__/index.test.ts @@ -6,7 +6,7 @@ import { mockHeapJsHttpRequest, trackEventSubscription } from '../../test-utilities' -import { HEAP_SEGMENT_LIBRARY_NAME } from '../../constants' +import { HEAP_SEGMENT_BROWSER_LIBRARY_NAME } from '../../constants' describe('#trackEvent', () => { const createHeapDestinationAndSpy = async (): Promise<[Plugin, jest.SpyInstance]> => { @@ -34,7 +34,7 @@ describe('#trackEvent', () => { expect(heapTrackSpy).toHaveBeenCalledWith('hello!', { banana: '📞', - segment_library: HEAP_SEGMENT_LIBRARY_NAME + segment_library: HEAP_SEGMENT_BROWSER_LIBRARY_NAME }) }) @@ -48,7 +48,7 @@ describe('#trackEvent', () => { ) expect(heapTrackSpy).toHaveBeenCalledWith('hello!', { - segment_library: HEAP_SEGMENT_LIBRARY_NAME + segment_library: HEAP_SEGMENT_BROWSER_LIBRARY_NAME }) }) @@ -66,7 +66,7 @@ describe('#trackEvent', () => { ) expect(heapTrackSpy).toHaveBeenCalledWith('hello!', { - segment_library: segmentLibraryValue + segment_library: HEAP_SEGMENT_BROWSER_LIBRARY_NAME }) }) }) diff --git a/packages/browser-destinations/src/destinations/heap/trackEvent/generated-types.ts b/packages/browser-destinations/src/destinations/heap/trackEvent/generated-types.ts index fa59f538a4..ddc10b5615 100644 --- a/packages/browser-destinations/src/destinations/heap/trackEvent/generated-types.ts +++ b/packages/browser-destinations/src/destinations/heap/trackEvent/generated-types.ts @@ -11,4 +11,8 @@ export interface Payload { properties?: { [k: string]: unknown } + /** + * The segment anonymous identifier for the user + */ + anonymousId?: string } diff --git a/packages/browser-destinations/src/destinations/heap/trackEvent/index.ts b/packages/browser-destinations/src/destinations/heap/trackEvent/index.ts index 74c2df64e8..7ccb0b70fc 100644 --- a/packages/browser-destinations/src/destinations/heap/trackEvent/index.ts +++ b/packages/browser-destinations/src/destinations/heap/trackEvent/index.ts @@ -2,7 +2,7 @@ import type { BrowserActionDefinition } from '../../../lib/browser-destinations' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' import { HeapApi } from '../types' -import { HEAP_SEGMENT_LIBRARY_NAME } from '../constants' +import { HEAP_SEGMENT_BROWSER_LIBRARY_NAME } from '../constants' const action: BrowserActionDefinition = { title: 'Track Event', @@ -27,12 +27,24 @@ const action: BrowserActionDefinition = { default: { '@path': '$.properties' } + }, + anonymousId: { + type: 'string', + required: false, + description: 'The segment anonymous identifier for the user', + label: 'Anonymous ID', + default: { + '@path': '$.anonymousId' + } } }, perform: (heap, event) => { - const defaultEventProperties = { segment_library: HEAP_SEGMENT_LIBRARY_NAME } - const eventProperties = Object.assign(defaultEventProperties, event.payload.properties ?? {}) + const eventProperties = Object.assign({}, event.payload.properties ?? {}) + eventProperties.segment_library = HEAP_SEGMENT_BROWSER_LIBRARY_NAME heap.track(event.payload.name, eventProperties) + if (event.payload.anonymousId) { + heap.addUserProperties({ anonymous_id: event.payload.anonymousId }) + } } } diff --git a/packages/browser-destinations/src/destinations/ripe/alias/__tests__/index.test.ts b/packages/browser-destinations/src/destinations/ripe/alias/__tests__/index.test.ts deleted file mode 100644 index 5c72fcf040..0000000000 --- a/packages/browser-destinations/src/destinations/ripe/alias/__tests__/index.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import type { Subscription } from '../../../../lib/browser-destinations' -import { Analytics, Context } from '@segment/analytics-next' -import RipeDestination, { destination } from '../../index' -import { RipeSDK } from '../../types' - -import { loadScript } from '../../../../runtime/load-script' - -jest.mock('../../../../runtime/load-script') -beforeEach(async () => { - ;(loadScript as jest.Mock).mockResolvedValue(true) -}) - -const subscriptions: Subscription[] = [ - { - partnerAction: 'alias', - name: 'Alias user', - enabled: true, - subscribe: 'type = "alias"', - mapping: { - userId: { - '@path': '$.userId' - }, - anonymousId: { - '@path': '$.anonymousId' - } - } - } -] - -describe('Ripe.alias', () => { - test('it maps userId and passes it into RipeSDK.alias', async () => { - window.Ripe = { - init: jest.fn().mockResolvedValueOnce('123'), - alias: jest.fn().mockResolvedValueOnce(undefined), - setIds: jest.fn().mockResolvedValueOnce(undefined) - } as unknown as RipeSDK - - const [event] = await RipeDestination({ - subscriptions, - apiKey: '123' - }) - - const ajs = new Analytics({ writeKey: '123' }) - await event.load(Context.system(), ajs) - jest.spyOn(destination.actions.alias, 'perform') - - await event.alias?.( - new Context({ - type: 'alias', - userId: 'newId' - }) - ) - - expect(destination.actions.alias.perform).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - payload: { - userId: 'newId' - } - }) - ) - - expect(window.Ripe.alias).toHaveBeenCalledWith('newId') - }) - - test('it maps anonymousId if userId is not set and passes it into RipeSDK.alias', async () => { - window.Ripe = { - init: jest.fn().mockResolvedValueOnce('123'), - alias: jest.fn().mockResolvedValueOnce(undefined), - setIds: jest.fn().mockResolvedValueOnce(undefined) - } as unknown as RipeSDK - - const [event] = await RipeDestination({ - subscriptions, - apiKey: '123' - }) - - const ajs = new Analytics({ writeKey: '123' }) - await event.load(Context.system(), ajs) - jest.spyOn(destination.actions.alias, 'perform') - - await event.alias?.( - new Context({ - type: 'alias', - anonymousId: 'anonymousId' - }) - ) - - expect(destination.actions.alias.perform).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - payload: { - anonymousId: 'anonymousId' - } - }) - ) - - expect(window.Ripe.alias).toHaveBeenCalledWith('anonymousId') - }) -}) diff --git a/packages/browser-destinations/src/destinations/ripe/alias/generated-types.ts b/packages/browser-destinations/src/destinations/ripe/alias/generated-types.ts deleted file mode 100644 index 489a8e35cf..0000000000 --- a/packages/browser-destinations/src/destinations/ripe/alias/generated-types.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Generated file. DO NOT MODIFY IT BY HAND. - -export interface Payload { - /** - * The new user ID, if user ID is not set - */ - anonymousId: string - /** - * The ID associated with the user - */ - userId?: string - /** - * The ID associated groupId - */ - groupId?: string -} diff --git a/packages/browser-destinations/src/destinations/ripe/alias/index.ts b/packages/browser-destinations/src/destinations/ripe/alias/index.ts deleted file mode 100644 index 02c39aea4b..0000000000 --- a/packages/browser-destinations/src/destinations/ripe/alias/index.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { BrowserActionDefinition } from '../../../lib/browser-destinations' -import type { Settings } from '../generated-types' -import type { Payload } from './generated-types' -import { RipeSDK } from '../types' - -const action: BrowserActionDefinition = { - title: 'Alias', - description: 'Alias a user to new user ID in Ripe', - defaultSubscription: 'type = "alias"', - platform: 'web', - fields: { - anonymousId: { - type: 'string', - required: true, - description: 'The new user ID, if user ID is not set', - label: 'Anonymous ID', - default: { '@path': '$.anonymousId' } - }, - userId: { - type: 'string', - required: false, - description: 'The ID associated with the user', - label: 'User ID', - default: { '@path': '$.userId' } - }, - groupId: { - type: 'string', - required: false, - description: 'The ID associated groupId', - label: 'Group ID', - default: { '@path': '$.groupId' } - } - }, - perform: async (ripe, { payload }) => { - await ripe.setIds(payload.anonymousId, payload.userId, payload.groupId) - if (payload.userId) { - return ripe.alias(payload.userId) - } - - if (payload.anonymousId) { - return ripe.alias(payload.anonymousId) - } - } -} - -export default action diff --git a/packages/browser-destinations/src/destinations/ripe/generated-types.ts b/packages/browser-destinations/src/destinations/ripe/generated-types.ts index d23e286a4d..e29ccf1367 100644 --- a/packages/browser-destinations/src/destinations/ripe/generated-types.ts +++ b/packages/browser-destinations/src/destinations/ripe/generated-types.ts @@ -9,4 +9,8 @@ export interface Settings { * The Ripe API key found in the Ripe App */ apiKey: string + /** + * The Ripe API endpoint (do not change this unless you know what you're doing) + */ + endpoint?: string } diff --git a/packages/browser-destinations/src/destinations/ripe/group/generated-types.ts b/packages/browser-destinations/src/destinations/ripe/group/generated-types.ts index 1dccedcdba..2d62b12b15 100644 --- a/packages/browser-destinations/src/destinations/ripe/group/generated-types.ts +++ b/packages/browser-destinations/src/destinations/ripe/group/generated-types.ts @@ -2,7 +2,7 @@ export interface Payload { /** - * The new user ID, if user ID is not set + * The anonymous id */ anonymousId: string /** diff --git a/packages/browser-destinations/src/destinations/ripe/group/index.ts b/packages/browser-destinations/src/destinations/ripe/group/index.ts index 9f7ec0294c..3f814039e9 100644 --- a/packages/browser-destinations/src/destinations/ripe/group/index.ts +++ b/packages/browser-destinations/src/destinations/ripe/group/index.ts @@ -12,7 +12,7 @@ const action: BrowserActionDefinition = { anonymousId: { type: 'string', required: true, - description: 'The new user ID, if user ID is not set', + description: 'The anonymous id', label: 'Anonymous ID', default: { '@path': '$.anonymousId' } }, diff --git a/packages/browser-destinations/src/destinations/ripe/identify/__tests__/index.test.ts b/packages/browser-destinations/src/destinations/ripe/identify/__tests__/index.test.ts index 29e58e24f1..ba7225f353 100644 --- a/packages/browser-destinations/src/destinations/ripe/identify/__tests__/index.test.ts +++ b/packages/browser-destinations/src/destinations/ripe/identify/__tests__/index.test.ts @@ -17,6 +17,9 @@ const subscriptions: Subscription[] = [ enabled: true, subscribe: 'type = "identify"', mapping: { + anonymousId: { + '@path': '$.anonymousId' + }, userId: { '@path': '$.userId' }, @@ -47,6 +50,7 @@ describe('Ripe.identify', () => { await event.identify?.( new Context({ type: 'identify', + anonymousId: 'anonymousId', userId: 'userId', traits: { name: 'Simon' @@ -58,6 +62,7 @@ describe('Ripe.identify', () => { expect.anything(), expect.objectContaining({ payload: { + anonymousId: 'anonymousId', userId: 'userId', traits: { name: 'Simon' @@ -67,6 +72,7 @@ describe('Ripe.identify', () => { ) expect(window.Ripe.identify).toHaveBeenCalledWith( + expect.stringMatching('anonymousId'), expect.stringMatching('userId'), expect.objectContaining({ name: 'Simon' }) ) diff --git a/packages/browser-destinations/src/destinations/ripe/identify/generated-types.ts b/packages/browser-destinations/src/destinations/ripe/identify/generated-types.ts index 28b9488f88..b9ef09efdd 100644 --- a/packages/browser-destinations/src/destinations/ripe/identify/generated-types.ts +++ b/packages/browser-destinations/src/destinations/ripe/identify/generated-types.ts @@ -2,7 +2,7 @@ export interface Payload { /** - * The new user ID, if user ID is not set + * The anonymous id */ anonymousId: string /** diff --git a/packages/browser-destinations/src/destinations/ripe/identify/index.ts b/packages/browser-destinations/src/destinations/ripe/identify/index.ts index 75d7666692..408f023566 100644 --- a/packages/browser-destinations/src/destinations/ripe/identify/index.ts +++ b/packages/browser-destinations/src/destinations/ripe/identify/index.ts @@ -12,7 +12,7 @@ const action: BrowserActionDefinition = { anonymousId: { type: 'string', required: true, - description: 'The new user ID, if user ID is not set', + description: 'The anonymous id', label: 'Anonymous ID', default: { '@path': '$.anonymousId' } }, @@ -28,7 +28,7 @@ const action: BrowserActionDefinition = { required: false, description: 'The ID associated groupId', label: 'Group ID', - default: { '@path': '$.groupId' } + default: { '@path': '$.context.groupId' } }, traits: { type: 'object', @@ -39,8 +39,9 @@ const action: BrowserActionDefinition = { } }, perform: async (ripe, { payload }) => { + console.log(JSON.stringify(payload, null, 2)) await ripe.setIds(payload.anonymousId, payload.userId, payload.groupId) - return ripe.identify(payload.userId, payload.traits) + return ripe.identify(payload.anonymousId, payload.userId, payload.traits) } } diff --git a/packages/browser-destinations/src/destinations/ripe/index.ts b/packages/browser-destinations/src/destinations/ripe/index.ts index 921054c4da..a6d1a48c67 100644 --- a/packages/browser-destinations/src/destinations/ripe/index.ts +++ b/packages/browser-destinations/src/destinations/ripe/index.ts @@ -12,8 +12,6 @@ import { initScript } from './init-script' import page from './page' -import alias from './alias' - const defaultVersion = 'latest' declare global { @@ -46,6 +44,13 @@ export const destination: BrowserDestinationDefinition = { label: 'API Key', type: 'string', required: true + }, + endpoint: { + label: 'API Endpoint', + description: `The Ripe API endpoint (do not change this unless you know what you're doing)`, + type: 'string', + format: 'uri', + default: 'https://storage.getripe.com' } }, @@ -54,9 +59,10 @@ export const destination: BrowserDestinationDefinition = { const { sdkVersion, apiKey } = settings const version = sdkVersion ?? defaultVersion + const endpoint = settings.endpoint || 'https://storage.getripe.com' await deps - .loadScript(`https://storage.googleapis.com/sdk.getripe.com/sdk/${version}/sdk.umd.js`) + .loadScript(`${endpoint}/sdk/${version}/sdk.umd.js`) .catch((err) => console.error('Unable to load Ripe SDK script', err)) await deps.resolveWhen(() => Object.prototype.hasOwnProperty.call(window, 'Ripe'), 100) @@ -66,7 +72,6 @@ export const destination: BrowserDestinationDefinition = { }, actions: { - alias, group, identify, page, @@ -74,12 +79,6 @@ export const destination: BrowserDestinationDefinition = { }, presets: [ - { - name: 'Alias user', - subscribe: 'type = "alias"', - partnerAction: 'alias', - mapping: defaultValues(alias.fields) - }, { name: 'Group user', subscribe: 'type = "group"', diff --git a/packages/browser-destinations/src/destinations/ripe/init-script.ts b/packages/browser-destinations/src/destinations/ripe/init-script.ts index 6da5a42e46..9d075d896a 100644 --- a/packages/browser-destinations/src/destinations/ripe/init-script.ts +++ b/packages/browser-destinations/src/destinations/ripe/init-script.ts @@ -6,7 +6,7 @@ export function initScript() { } window.Ripe = [] - ;['alias', 'group', 'identify', 'init', 'page', 'setIds', 'track'].forEach(function (method) { + ;['group', 'identify', 'init', 'page', 'setIds', 'track'].forEach(function (method) { window.Ripe[method] = function () { let args = Array.from(arguments) args.unshift(method) diff --git a/packages/browser-destinations/src/destinations/ripe/page/generated-types.ts b/packages/browser-destinations/src/destinations/ripe/page/generated-types.ts index cec9e72c62..5fbda49ba9 100644 --- a/packages/browser-destinations/src/destinations/ripe/page/generated-types.ts +++ b/packages/browser-destinations/src/destinations/ripe/page/generated-types.ts @@ -2,7 +2,7 @@ export interface Payload { /** - * The new user ID, if user ID is not set + * The anonymous id */ anonymousId: string /** diff --git a/packages/browser-destinations/src/destinations/ripe/page/index.ts b/packages/browser-destinations/src/destinations/ripe/page/index.ts index 97640d1d9c..a13cb38359 100644 --- a/packages/browser-destinations/src/destinations/ripe/page/index.ts +++ b/packages/browser-destinations/src/destinations/ripe/page/index.ts @@ -12,7 +12,7 @@ const action: BrowserActionDefinition = { anonymousId: { type: 'string', required: true, - description: 'The new user ID, if user ID is not set', + description: 'The anonymous id', label: 'Anonymous ID', default: { '@path': '$.anonymousId' } }, @@ -28,21 +28,33 @@ const action: BrowserActionDefinition = { required: false, description: 'The ID associated groupId', label: 'Group ID', - default: { '@path': '$.groupId' } + default: { '@path': '$.context.groupId' } }, category: { type: 'string', required: false, description: 'The category of the page', label: 'Category', - default: { '@path': '$.category' } + default: { + '@if': { + exists: { '@path': '$.category' }, + then: { '@path': '$.category' }, + else: { '@path': '$.context.category' } + } + } }, name: { type: 'string', required: false, description: 'The name of the page', label: 'Name', - default: { '@path': '$.name' } + default: { + '@if': { + exists: { '@path': '$.name' }, + then: { '@path': '$.name' }, + else: { '@path': '$.context.name' } + } + } }, properties: { type: 'object', diff --git a/packages/browser-destinations/src/destinations/ripe/track/generated-types.ts b/packages/browser-destinations/src/destinations/ripe/track/generated-types.ts index 4d87cccad5..9f035e260f 100644 --- a/packages/browser-destinations/src/destinations/ripe/track/generated-types.ts +++ b/packages/browser-destinations/src/destinations/ripe/track/generated-types.ts @@ -2,7 +2,7 @@ export interface Payload { /** - * The new user ID, if user ID is not set + * The anonymous id */ anonymousId: string /** diff --git a/packages/browser-destinations/src/destinations/ripe/track/index.ts b/packages/browser-destinations/src/destinations/ripe/track/index.ts index 61e9cf0599..5543963c24 100644 --- a/packages/browser-destinations/src/destinations/ripe/track/index.ts +++ b/packages/browser-destinations/src/destinations/ripe/track/index.ts @@ -12,7 +12,7 @@ const action: BrowserActionDefinition = { anonymousId: { type: 'string', required: true, - description: 'The new user ID, if user ID is not set', + description: 'The anonymous id', label: 'Anonymous ID', default: { '@path': '$.anonymousId' } }, @@ -28,7 +28,7 @@ const action: BrowserActionDefinition = { required: false, description: 'The ID associated groupId', label: 'Group ID', - default: { '@path': '$.groupId' } + default: { '@path': '$.context.groupId' } }, event: { type: 'string', diff --git a/packages/browser-destinations/src/destinations/ripe/types.ts b/packages/browser-destinations/src/destinations/ripe/types.ts index 93818aca42..9ba0298646 100644 --- a/packages/browser-destinations/src/destinations/ripe/types.ts +++ b/packages/browser-destinations/src/destinations/ripe/types.ts @@ -1,7 +1,6 @@ export interface RipeSDK { - alias: (userId: string) => Promise group: (groupId: string, traits?: Record) => Promise - identify: (userId?: string | undefined | null, traits?: Record) => Promise + identify: (anonymousId: string, userId?: string | undefined | null, traits?: Record) => Promise init: (apiKey: string) => Promise page: (category?: string, name?: string, properties?: Record) => Promise setIds: (anonymousId: string, userId?: string, groupId?: string) => Promise diff --git a/packages/browser-destinations/test/setup-after-env.ts b/packages/browser-destinations/test/setup-after-env.ts index a1c063ee0a..f7e09beabd 100644 --- a/packages/browser-destinations/test/setup-after-env.ts +++ b/packages/browser-destinations/test/setup-after-env.ts @@ -1,4 +1,14 @@ import { Crypto } from '@peculiar/webcrypto' +import { TextEncoder, TextDecoder } from 'util' +import { setImmediate } from 'timers' + +// fix: "ReferenceError: TextEncoder is not defined" after upgrading JSDOM +global.TextEncoder = TextEncoder +global.TextDecoder = TextDecoder +// fix: jsdom uses setImmediate under the hood for preflight XHR requests, +// and jest removed setImmediate, so we need to provide it to prevent console +// logging ReferenceErrors made by integration tests that call Amplitude. +global.setImmediate = setImmediate beforeEach(() => { jest.restoreAllMocks() diff --git a/packages/browser-destinations/tsconfig.json b/packages/browser-destinations/tsconfig.json index 97d2f22950..af9bff8d07 100644 --- a/packages/browser-destinations/tsconfig.json +++ b/packages/browser-destinations/tsconfig.json @@ -12,6 +12,5 @@ "@segment/destination-subscriptions": ["../destination-subscriptions/src"] } }, - "include": ["src", "test"], - "exclude": ["webpack.config.js"] + "exclude": ["webpack.config.js", "dist"] } diff --git a/packages/cli-internal/package.json b/packages/cli-internal/package.json index 66bb337f5a..aa733167a0 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.123.1", + "version": "3.127.0", "license": "MIT", "repository": { "type": "git", @@ -9,7 +9,7 @@ "directory": "packages/cli-internal" }, "engines": { - "node": "^14.16" + "node": "^18.12" }, "engineStrict": true, "files": [ @@ -53,8 +53,9 @@ "@oclif/config": "^1", "@oclif/errors": "^1", "@oclif/plugin-help": "^3.3", - "@segment/action-destinations": "^3.127.1", - "@segment/actions-core": "^3.49.0", + "@segment/action-destinations": "^3.131.0", + "@segment/actions-core": "^3.51.0", + "@types/node": "^18.11.15", "chalk": "^4.1.1", "chokidar": "^3.5.1", "dotenv": "^10.0.0", @@ -77,7 +78,7 @@ "tslib": "^2.3.1" }, "optionalDependencies": { - "@segment/browser-destinations": "^3.71.0", + "@segment/browser-destinations": "^3.74.0", "@segment/control-plane-service-client": "github:segmentio/control-plane-service-js-client.git#master" }, "oclif": { diff --git a/packages/cli/package.json b/packages/cli/package.json index ae33a2fcdd..f366efe509 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.123.1", + "version": "3.127.0", "license": "MIT", "repository": { "type": "git", @@ -9,7 +9,7 @@ "directory": "packages/cli" }, "engines": { - "node": "^14.16" + "node": "^18.12" }, "engineStrict": true, "bin": { @@ -56,8 +56,9 @@ "@oclif/config": "^1", "@oclif/errors": "^1", "@oclif/plugin-help": "^3.3", - "@segment/action-destinations": "^3.127.1", - "@segment/actions-core": "^3.49.0", + "@segment/action-destinations": "^3.131.0", + "@segment/actions-core": "^3.51.0", + "@types/node": "^18.11.15", "chalk": "^4.1.1", "chokidar": "^3.5.1", "dotenv": "^10.0.0", @@ -80,8 +81,8 @@ "tslib": "^2.3.1" }, "optionalDependencies": { - "@segment/actions-cli-internal": "^3.123.1", - "@segment/browser-destinations": "^3.71.0" + "@segment/actions-cli-internal": "^3.127.0", + "@segment/browser-destinations": "^3.74.0" }, "oclif": { "commands": "./dist/commands", diff --git a/packages/cli/templates/destinations/oauth2-auth/index.ts b/packages/cli/templates/destinations/oauth2-auth/index.ts index 3681f44eb7..426f43ad06 100644 --- a/packages/cli/templates/destinations/oauth2-auth/index.ts +++ b/packages/cli/templates/destinations/oauth2-auth/index.ts @@ -26,7 +26,7 @@ const destination: DestinationDefinition = { }) }) - return { accessToken: res.body.access_token } + return { accessToken: res.data.access_token } } }, extendRequest({ auth }) { diff --git a/packages/core/package.json b/packages/core/package.json index aabaae73c6..1ae5302ab0 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.49.0", + "version": "3.51.0", "repository": { "type": "git", "url": "https://github.com/segmentio/fab-5-engine", @@ -25,7 +25,7 @@ "package.json" ], "engines": { - "node": "^14.16" + "node": "^18.12" }, "engineStrict": true, "license": "MIT", @@ -46,7 +46,6 @@ }, "devDependencies": { "@types/btoa-lite": "^1.0.0", - "@types/express": "^4.17.11", "@types/jest": "^27.0.0", "@types/json-schema": "^7.0.7", "benny": "^3.7.0", @@ -56,7 +55,8 @@ "dependencies": { "@lukeed/uuid": "^2.0.0", "@segment/ajv-human-errors": "^2.2.0", - "@segment/destination-subscriptions": "^3.14.0", + "@segment/destination-subscriptions": "^3.15.0", + "@types/node": "^18.11.15", "abort-controller": "^3.0.0", "aggregate-error": "^3.1.0", "ajv": "^8.6.3", diff --git a/packages/core/src/__tests__/destination-kit.test.ts b/packages/core/src/__tests__/destination-kit.test.ts index 336bdee157..9493490467 100644 --- a/packages/core/src/__tests__/destination-kit.test.ts +++ b/packages/core/src/__tests__/destination-kit.test.ts @@ -126,7 +126,7 @@ describe('destination kit', () => { const testEvent: SegmentEvent = { type: 'track' } const testSettings = { apiSecret: 'test_key', subscription: { subscribe: 'typo', partnerAction: 'customEvent' } } const res = await destinationTest.onEvent(testEvent, testSettings) - expect(res).toEqual([{ output: "invalid subscription : Cannot read property 'type' of undefined" }]) + expect(res).toEqual([{ output: "invalid subscription : Cannot read properties of undefined (reading 'type')" }]) }) test('should return `not subscribed` when providing an empty event', async () => { diff --git a/packages/core/src/__tests__/get.iso.test.ts b/packages/core/src/__tests__/get.iso.test.ts index 10363ae778..40dce14f54 100644 --- a/packages/core/src/__tests__/get.iso.test.ts +++ b/packages/core/src/__tests__/get.iso.test.ts @@ -10,27 +10,49 @@ const obj = { }, h: null }, - u: undefined + u: undefined, + '[txt] non': true, + '[txt] nest': { + inner: true + } } -const fixtures: Record = { - '': obj, - a: obj.a, - 'a.b': obj.a.b, - 'a.b.c': obj.a.b.c, - 'a.b.d': obj.a.b.d, - 'a.b.e': obj.a.b.e, - 'a.b.f[0]': obj.a.b.f[0], - 'a.b.f[0].g': obj.a.b.f[0].g, - 'a.h': obj.a.h, - 'a.b.x': undefined, - u: undefined -} +// webkit does not support look behind ATM +const supportsLookBehind = (() => { + try { + new RegExp(`(?<=Y)`) + return true + } catch (e) { + return false + } +})() + +const fixtures = new Map([ + [undefined, undefined], + [null, undefined], + [['a', 'b'], obj.a.b], + ['', obj], + ['.', obj], + ['a', obj.a], + ['a.b', obj.a.b], + ["['a'].b", obj.a.b], + ['["a"].b', obj.a.b], + ['a.b.c', obj.a.b.c], + ['a.b.d', obj.a.b.d], + ['a.b.e', obj.a.b.e], + ['a.b.f[0]', obj.a.b.f[0]], + ['a.b.f[0].g', obj.a.b.f[0].g], + ['a.h', obj.a.h], + ['a.b.x', undefined], + ['u', undefined], + ['[txt] non', supportsLookBehind ? true : undefined], + ['[txt] nest.inner', supportsLookBehind ? true : undefined] +]) describe('get', () => { - for (const path of Object.keys(fixtures)) { + fixtures.forEach((expected, path) => { test(`"${path}"`, () => { - expect(get(obj, path)).toEqual(fixtures[path]) + expect(get(obj, path)).toEqual(expected) }) - } + }) }) diff --git a/packages/core/src/get.ts b/packages/core/src/get.ts index c6499b2a9d..de25a315e2 100644 --- a/packages/core/src/get.ts +++ b/packages/core/src/get.ts @@ -2,6 +2,17 @@ * Lightweight alternative to lodash.get with similar coverage * Supports basic path lookup via dot notation `'foo.bar[0].baz'` or an array ['foo', 'bar', '0', 'baz'] */ + +let arrayRe: RegExp + +try { + arrayRe = new RegExp(`\\[(?="|'|\\d)|\\.|(?<="|'|\\d)]+`, 'g') +} catch (e) { + //safari does not support lookbehind operator so we will default + //to a simpler approach wherein [bar] will not be a valid key + arrayRe = /\[|\.|]+/g +} + export function get( // eslint-disable-next-line @typescript-eslint/no-explicit-any obj: any, @@ -13,9 +24,17 @@ export function get( // Not defined if (path === null || path == undefined) return undefined - // Check if path is string or array. Regex : ensure that we do not have '.' and brackets. - // Regex explained: https://regexr.com/58j0k - const pathArray = Array.isArray(path) ? path : (path.match(/([^[.\]])+/g) as string[]) + // Check if path is string or array. Regex : splits the path into valid array of path chunks + // we support an extra edge case that lodash does not around brackets + // a['b'] = ['a','b'] + // a[b] = ['a[b]'] //brackets without numeric index or a string are considered part of the key + // Regex explained: https://regexr.com/74m2m + const pathArray = Array.isArray(path) + ? path + : path + .split(arrayRe) + .filter((f) => f) + .map((s) => s.replace(/'|"/g, '')) // Find value if exist return otherwise return undefined value return pathArray.reduce((prevObj, key) => prevObj && prevObj[key], obj) diff --git a/packages/core/src/mapping-kit/__tests__/index.iso.test.ts b/packages/core/src/mapping-kit/__tests__/index.iso.test.ts index a38c5840eb..c3893d4f23 100644 --- a/packages/core/src/mapping-kit/__tests__/index.iso.test.ts +++ b/packages/core/src/mapping-kit/__tests__/index.iso.test.ts @@ -520,20 +520,6 @@ describe('@path', () => { expect(output).toStrictEqual({ neat: 'bar' }) }) - test('invalid bracket-spaced nested value', () => { - const output = transform( - { neat: { '@path': "$.integrations['Actions Amplitude'].session_id" } }, - { - integrations: { - 'Actions Amplitude': { - session_id: 'bar' - } - } - } - ) - expect(output).toStrictEqual({}) - }) - test('invalid nested value type', () => { const output = transform({ neat: { '@path': '$.foo.bar.baz' } }, { foo: 'bar' }) expect(output).toStrictEqual({}) diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 38e85284c0..b51b59cd80 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -7,6 +7,6 @@ "@segment/destination-subscriptions": ["../destination-subscriptions/src"] } }, - "exclude": [], + "exclude": ["dist"], "include": ["src", "test", "benchmarks"] } diff --git a/packages/core/tsconfig.karma.json b/packages/core/tsconfig.karma.json index e86e375800..dc6a5539b9 100644 --- a/packages/core/tsconfig.karma.json +++ b/packages/core/tsconfig.karma.json @@ -8,5 +8,5 @@ "outDir": "./dist/test" }, "include": ["src", "test"], - "exclude": ["node_modules"] + "exclude": ["dist"] } diff --git a/packages/destination-actions/package.json b/packages/destination-actions/package.json index 7827a686b2..87d13ce546 100644 --- a/packages/destination-actions/package.json +++ b/packages/destination-actions/package.json @@ -1,7 +1,7 @@ { "name": "@segment/action-destinations", "description": "Destination Actions engine and definitions.", - "version": "3.127.1", + "version": "3.131.0", "repository": { "type": "git", "url": "https://github.com/segmentio/action-destinations", @@ -14,7 +14,7 @@ "package.json" ], "engines": { - "node": "^14.16" + "node": "^18.12" }, "engineStrict": true, "license": "MIT", @@ -31,6 +31,7 @@ "typecheck": "tsc -p tsconfig.build.json --noEmit" }, "devDependencies": { + "@types/google-libphonenumber": "^7.4.23", "@types/jest": "^27.0.0", "jest": "^27.3.1", "nock": "^13.1.4" @@ -38,11 +39,13 @@ "dependencies": { "@amplitude/ua-parser-js": "^0.7.25", "@segment/a1-notation": "^2.1.4", - "@segment/actions-core": "^3.49.0", - "@segment/actions-shared": "^1.31.0", + "@segment/actions-core": "^3.51.0", + "@segment/actions-shared": "^1.33.0", + "@types/node": "^18.11.15", "cheerio": "^1.0.0-rc.10", "dayjs": "^1.10.7", "escape-goat": "^3", + "google-libphonenumber": "^3.2.31", "liquidjs": "^9.37.0", "lodash": "^4.17.21" }, diff --git a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/__tests__/__snapshots__/snapshot.test.ts.snap new file mode 100644 index 0000000000..4a48abe88e --- /dev/null +++ b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/__tests__/__snapshots__/snapshot.test.ts.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Testing snapshot for actions-blackbaud-raisers-edge-nxt destination: createOrUpdateIndividualConstituent action - all fields 1`] = `""`; + +exports[`Testing snapshot for actions-blackbaud-raisers-edge-nxt destination: createOrUpdateIndividualConstituent action - required fields 1`] = `""`; + +exports[`Testing snapshot for actions-blackbaud-raisers-edge-nxt destination: createOrUpdateIndividualConstituent action - required fields 2`] = ` +Headers { + Symbol(map): Object { + "authorization": Array [ + "Bearer undefined", + ], + "bb-api-subscription-key": Array [ + "hUXnkT]ixm7mm*HX", + ], + "user-agent": Array [ + "Segment (Actions)", + ], + }, +} +`; diff --git a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/__tests__/index.test.ts b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/__tests__/index.test.ts new file mode 100644 index 0000000000..8aab190af5 --- /dev/null +++ b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/__tests__/index.test.ts @@ -0,0 +1,21 @@ +import nock from 'nock' +// import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import { createTestIntegration } from '@segment/actions-core' +import { SKY_API_BASE_URL } from '../constants' +import Definition from '../index' + +const testDestination = createTestIntegration(Definition) + +describe("Blackbaud Raiser's Edge NXT", () => { + describe('testAuthentication', () => { + it('should validate authentication inputs', async () => { + nock(SKY_API_BASE_URL).get('/emailaddresstypes').reply(200, {}) + + const settings = { + bbApiSubscriptionKey: 'subscription_key' + } + + await expect(testDestination.testAuthentication(settings)).resolves.not.toThrowError() + }) + }) +}) diff --git a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/__tests__/snapshot.test.ts new file mode 100644 index 0000000000..6d3d45a07f --- /dev/null +++ b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/__tests__/snapshot.test.ts @@ -0,0 +1,81 @@ +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import { generateTestData } from '../../../lib/test-data' +import destination from '../index' +import nock from 'nock' + +const testDestination = createTestIntegration(destination) +const destinationSlug = 'actions-blackbaud-raisers-edge-nxt' + +describe(`Testing snapshot for ${destinationSlug} destination:`, () => { + for (const actionSlug in destination.actions) { + it(`${actionSlug} action - required fields`, async () => { + const seedName = `${destinationSlug}#${actionSlug}` + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, true) + + eventData.last = 'Smith' + + nock(/.*/).persist().get(/.*/).reply(200, {}) + nock(/.*/).persist().patch(/.*/).reply(200, {}) + nock(/.*/).persist().post(/.*/).reply(200, {}) + nock(/.*/).persist().put(/.*/).reply(200, {}) + + const event = createTestEvent({ + properties: eventData + }) + + const responses = await testDestination.testAction(actionSlug, { + event: event, + mapping: event.properties, + settings: settingsData, + auth: undefined + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + + expect(request.headers).toMatchSnapshot() + }) + + it(`${actionSlug} action - all fields`, async () => { + const seedName = `${destinationSlug}#${actionSlug}` + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, false) + + nock(/.*/).persist().get(/.*/).reply(200, {}) + nock(/.*/).persist().patch(/.*/).reply(200, {}) + nock(/.*/).persist().post(/.*/).reply(200, {}) + nock(/.*/).persist().put(/.*/).reply(200, {}) + + const event = createTestEvent({ + properties: eventData + }) + + const responses = await testDestination.testAction(actionSlug, { + event: event, + mapping: event.properties, + settings: settingsData, + auth: undefined + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + }) + } +}) diff --git a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/api/index.ts b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/api/index.ts new file mode 100644 index 0000000000..30be7c5ea1 --- /dev/null +++ b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/api/index.ts @@ -0,0 +1,156 @@ +import type { RequestClient, ModifiedResponse } from '@segment/actions-core' +import { SKY_API_BASE_URL } from '../constants' + +export class BlackbaudSkyApi { + request: RequestClient + + constructor(request: RequestClient) { + this.request = request + } + + async getExistingConstituents(searchField: string, searchText: string): Promise { + return this.request( + `${SKY_API_BASE_URL}/constituents/search?search_field=${searchField}&search_text=${searchText}`, + { + method: 'get' + } + ) + } + + async createConstituent(constituentData: object): Promise { + return this.request(`${SKY_API_BASE_URL}/constituents`, { + method: 'post', + json: constituentData + }) + } + + async updateConstituent(constituentId: string, constituentData: object): Promise { + return this.request(`${SKY_API_BASE_URL}/constituents/${constituentId}`, { + method: 'patch', + json: constituentData, + throwHttpErrors: false + }) + } + + async getConstituentAddressList(constituentId: string): Promise { + return this.request(`${SKY_API_BASE_URL}/constituents/${constituentId}/addresses?include_inactive=true`, { + method: 'get', + throwHttpErrors: false + }) + } + + async createConstituentAddress(constituentId: string, constituentAddressData: object): Promise { + return this.request(`${SKY_API_BASE_URL}/addresses`, { + method: 'post', + json: { + ...constituentAddressData, + constituent_id: constituentId + }, + throwHttpErrors: false + }) + } + + async updateConstituentAddressById(addressId: string, constituentAddressData: object): Promise { + return this.request(`${SKY_API_BASE_URL}/addresses/${addressId}`, { + method: 'patch', + json: { + ...constituentAddressData, + inactive: false + }, + throwHttpErrors: false + }) + } + + async getConstituentEmailList(constituentId: string): Promise { + return this.request(`${SKY_API_BASE_URL}/constituents/${constituentId}/emailaddresses?include_inactive=true`, { + method: 'get', + throwHttpErrors: false + }) + } + + async createConstituentEmail(constituentId: string, constituentEmailData: object): Promise { + return this.request(`${SKY_API_BASE_URL}/emailaddresses`, { + method: 'post', + json: { + ...constituentEmailData, + constituent_id: constituentId + }, + throwHttpErrors: false + }) + } + + async updateConstituentEmailById(emailId: string, constituentEmailData: object): Promise { + return this.request(`${SKY_API_BASE_URL}/emailaddresses/${emailId}`, { + method: 'patch', + json: { + ...constituentEmailData, + inactive: false + }, + throwHttpErrors: false + }) + } + + async getConstituentOnlinePresenceList(constituentId: string): Promise { + return this.request(`${SKY_API_BASE_URL}/constituents/${constituentId}/onlinepresences?include_inactive=true`, { + method: 'get', + throwHttpErrors: false + }) + } + + async createConstituentOnlinePresence( + constituentId: string, + constituentOnlinePresenceData: object + ): Promise { + return this.request(`${SKY_API_BASE_URL}/onlinepresences`, { + method: 'post', + json: { + ...constituentOnlinePresenceData, + constituent_id: constituentId + }, + throwHttpErrors: false + }) + } + + async updateConstituentOnlinePresenceById( + onlinePresenceId: string, + constituentOnlinePresenceData: object + ): Promise { + return this.request(`${SKY_API_BASE_URL}/onlinepresences/${onlinePresenceId}`, { + method: 'patch', + json: { + ...constituentOnlinePresenceData, + inactive: false + }, + throwHttpErrors: false + }) + } + + async getConstituentPhoneList(constituentId: string): Promise { + return this.request(`${SKY_API_BASE_URL}/constituents/${constituentId}/phones?include_inactive=true`, { + method: 'get', + throwHttpErrors: false + }) + } + + async createConstituentPhone(constituentId: string, constituentPhoneData: object): Promise { + return this.request(`${SKY_API_BASE_URL}/phones`, { + method: 'post', + json: { + ...constituentPhoneData, + constituent_id: constituentId + }, + throwHttpErrors: false + }) + } + + async updateConstituentPhoneById(phoneId: string, constituentPhoneData: object): Promise { + return this.request(`${SKY_API_BASE_URL}/phones/${phoneId}`, { + method: 'patch', + json: { + ...constituentPhoneData, + inactive: false + }, + throwHttpErrors: false + }) + } +} diff --git a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/constants/index.ts b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/constants/index.ts new file mode 100644 index 0000000000..2edb2bac05 --- /dev/null +++ b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/constants/index.ts @@ -0,0 +1,2 @@ +export const SKY_API_BASE_URL = 'https://api.sky.blackbaud.com/constituent/v1' +export const SKY_OAUTH2_TOKEN_URL = 'https://oauth2.sky.blackbaud.com/token' diff --git a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/__tests__/__snapshots__/snapshot.test.ts.snap new file mode 100644 index 0000000000..67d00bfa49 --- /dev/null +++ b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/__tests__/__snapshots__/snapshot.test.ts.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Testing snapshot for BlackbaudRaisersEdgeNxt's createOrUpdateIndividualConstituent destination action: all fields 1`] = `""`; + +exports[`Testing snapshot for BlackbaudRaisersEdgeNxt's createOrUpdateIndividualConstituent destination action: required fields 1`] = `""`; + +exports[`Testing snapshot for BlackbaudRaisersEdgeNxt's createOrUpdateIndividualConstituent destination action: required fields 2`] = ` +Headers { + Symbol(map): Object { + "authorization": Array [ + "Bearer undefined", + ], + "bb-api-subscription-key": Array [ + "H*Z39ROa", + ], + "user-agent": Array [ + "Segment (Actions)", + ], + }, +} +`; diff --git a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/__tests__/index.test.ts b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/__tests__/index.test.ts new file mode 100644 index 0000000000..036aa2cc5e --- /dev/null +++ b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/__tests__/index.test.ts @@ -0,0 +1,601 @@ +import nock from 'nock' +import { createTestEvent, createTestIntegration, IntegrationError, RetryableError } from '@segment/actions-core' +import Destination from '../../index' +import { SKY_API_BASE_URL } from '../../constants' +import { + identifyEventData, + identifyEventDataNoEmail, + identifyEventDataNoLastName, + identifyEventDataWithInvalidWebsite, + identifyEventDataWithLookupId, + identifyEventDataUpdated +} from '../fixtures' + +const testDestination = createTestIntegration(Destination) + +const mapping = { + address: { + address_lines: { + '@path': '$.traits.address.street' + }, + city: { + '@path': '$.traits.address.city' + }, + country: { + '@path': '$.traits.address.country' + }, + postal_code: { + '@path': '$.traits.address.postalCode' + }, + state: { + '@path': '$.traits.address.state' + }, + type: { + '@path': '$.traits.addressType' + } + }, + email: { + address: { + '@path': '$.traits.email' + }, + type: { + '@path': '$.traits.emailType' + } + }, + lookup_id: { + '@path': '$.traits.lookup_id' + }, + online_presence: { + address: { + '@path': '$.traits.website' + }, + type: { + '@path': '$.traits.websiteType' + } + }, + phone: { + number: { + '@path': '$.traits.phone' + }, + type: { + '@path': '$.traits.phoneType' + } + } +} + +describe('BlackbaudRaisersEdgeNxt.createOrUpdateIndividualConstituent', () => { + test('should create a new constituent successfully', async () => { + const event = createTestEvent(identifyEventData) + + nock(SKY_API_BASE_URL) + .get('/constituents/search?search_field=email_address&search_text=john@example.biz') + .reply(200, { + count: 0, + value: [] + }) + + nock(SKY_API_BASE_URL).post('/constituents').reply(200, { + id: '123' + }) + + await expect( + testDestination.testAction('createOrUpdateIndividualConstituent', { + event, + mapping, + useDefaultMappings: true + }) + ).resolves.not.toThrowError() + }) + + test('should create a new constituent without email or lookup_id successfully', async () => { + const event = createTestEvent(identifyEventDataNoEmail) + + nock(SKY_API_BASE_URL).post('/constituents').reply(200, { + id: '456' + }) + + await expect( + testDestination.testAction('createOrUpdateIndividualConstituent', { + event, + mapping, + useDefaultMappings: true + }) + ).resolves.not.toThrowError() + }) + + test('should update an existing constituent matched by email successfully', async () => { + const event = createTestEvent(identifyEventDataUpdated) + + nock(SKY_API_BASE_URL) + .get('/constituents/search?search_field=email_address&search_text=john@example.biz') + .reply(200, { + count: 1, + value: [ + { + id: '123', + address: 'PO Box 963\r\nNew York City, NY 10108', + email: 'john@example.biz', + fundraiser_status: 'None', + name: 'John Doe' + } + ] + }) + + nock(SKY_API_BASE_URL).patch('/constituents/123').reply(200) + + nock(SKY_API_BASE_URL) + .get('/constituents/123/addresses?include_inactive=true') + .reply(200, { + count: 1, + value: [ + { + id: '1000', + address_lines: 'PO Box 963', + city: 'New York City', + constituent_id: '123', + date_added: '2023-01-01T01:01:01.000-05:00', + date_modified: '2023-01-01T01:01:01.000-05:00', + do_not_mail: false, + formatted_address: 'PO Box 963\r\nNew York City, NY 10108', + inactive: false, + postal_code: '10108', + preferred: true, + state: 'NY', + type: 'Home' + } + ] + }) + + nock(SKY_API_BASE_URL).post('/addresses').reply(200, { + id: '1001' + }) + + nock(SKY_API_BASE_URL) + .get('/constituents/123/emailaddresses?include_inactive=true') + .reply(200, { + count: 1, + value: [ + { + id: '2000', + address: 'john@example.biz', + constituent_id: '123', + date_added: '2023-01-01T01:01:01.000-05:00', + date_modified: '2023-01-01T01:01:01.000-05:00', + do_not_email: false, + inactive: false, + primary: true, + type: 'Home' + } + ] + }) + + nock(SKY_API_BASE_URL).patch('/emailaddresses/2000').reply(200) + + nock(SKY_API_BASE_URL) + .get('/constituents/123/onlinepresences?include_inactive=true') + .reply(200, { + count: 1, + value: [ + { + id: '3000', + address: 'https://www.facebook.com/john.doe', + constituent_id: '123', + inactive: false, + primary: true, + type: 'Facebook' + } + ] + }) + + nock(SKY_API_BASE_URL).post('/onlinepresences').reply(200, { + id: '3001' + }) + + nock(SKY_API_BASE_URL) + .get('/constituents/123/phones?include_inactive=true') + .reply(200, { + count: 1, + value: [ + { + id: '4000', + constituent_id: '123', + do_not_call: false, + inactive: false, + number: '+18774466722', + primary: true, + type: 'Home' + } + ] + }) + + nock(SKY_API_BASE_URL).post('/phones').reply(200, { + id: '4001' + }) + + await expect( + testDestination.testAction('createOrUpdateIndividualConstituent', { + event, + mapping, + useDefaultMappings: true + }) + ).resolves.not.toThrowError() + }) + + test('should update an existing constituent matched by lookup_id successfully', async () => { + const event = createTestEvent(identifyEventDataWithLookupId) + + nock(SKY_API_BASE_URL) + .get('/constituents/search?search_field=lookup_id&search_text=abcd1234') + .reply(200, { + count: 1, + value: [ + { + id: '123', + address: '11 Wall St\r\nNew York, NY 1005', + email: 'john@example.biz', + fundraiser_status: 'None', + name: 'John Doe' + } + ] + }) + + nock(SKY_API_BASE_URL).patch('/constituents/123').reply(200) + + nock(SKY_API_BASE_URL) + .get('/constituents/123/addresses?include_inactive=true') + .reply(200, { + count: 2, + value: [ + { + id: '1000', + address_lines: 'PO Box 963', + city: 'New York City', + constituent_id: '123', + date_added: '2023-01-01T01:01:01.000-05:00', + date_modified: '2023-01-01T01:01:01.000-05:00', + do_not_mail: false, + formatted_address: 'PO Box 963\r\nNew York City, NY 10108', + inactive: false, + postal_code: '10108', + preferred: true, + state: 'NY', + type: 'Home' + }, + { + id: '1001', + address_lines: '11 Wall St', + city: 'New York', + constituent_id: '123', + date_added: '2023-01-02T01:01:01.000-05:00', + date_modified: '2023-01-02T01:01:01.000-05:00', + do_not_mail: false, + formatted_address: '11 Wall Street\r\nNew York, NY 10005', + inactive: false, + postal_code: '10005', + preferred: true, + state: 'NY', + type: 'Work' + } + ] + }) + + nock(SKY_API_BASE_URL).post('/addresses').reply(200, { + id: '1002' + }) + + nock(SKY_API_BASE_URL) + .get('/constituents/123/emailaddresses?include_inactive=true') + .reply(200, { + count: 1, + value: [ + { + id: '2000', + address: 'john@example.biz', + constituent_id: '123', + date_added: '2023-01-01T01:01:01.000-05:00', + date_modified: '2023-01-01T01:01:01.000-05:00', + do_not_email: false, + inactive: false, + primary: true, + type: 'Work' + } + ] + }) + + nock(SKY_API_BASE_URL).post('/emailaddresses').reply(200, { + id: '2001' + }) + + nock(SKY_API_BASE_URL) + .get('/constituents/123/onlinepresences?include_inactive=true') + .reply(200, { + count: 1, + value: [ + { + id: '3001', + address: 'https://www.example.biz', + constituent_id: '123', + inactive: false, + primary: true, + type: 'Website' + } + ] + }) + + nock(SKY_API_BASE_URL) + .get('/constituents/123/phones?include_inactive=true') + .reply(200, { + count: 1, + value: [ + { + id: '4001', + constituent_id: '123', + do_not_call: false, + inactive: false, + number: '+18774466723', + primary: true, + type: 'Work' + } + ] + }) + + await expect( + testDestination.testAction('createOrUpdateIndividualConstituent', { + event, + mapping, + useDefaultMappings: true + }) + ).resolves.not.toThrowError() + }) + + test('should throw an IntegrationError if multiple records matched', async () => { + const event = createTestEvent(identifyEventData) + + nock(SKY_API_BASE_URL) + .get('/constituents/search?search_field=email_address&search_text=john@example.biz') + .reply(200, { + count: 2, + value: [ + { + id: '123', + address: '11 Wall Street\r\nNew York, NY 10005', + email: 'john@example.biz', + fundraiser_status: 'None', + name: 'John Doe' + }, + { + id: '1234', + address: '100 Main St\r\nLos Angeles, CA 90210', + email: 'john@example.biz', + fundraiser_status: 'None', + name: 'John Doe' + } + ] + }) + + await expect( + testDestination.testAction('createOrUpdateIndividualConstituent', { + event, + mapping, + useDefaultMappings: true + }) + ).rejects.toThrowError( + new IntegrationError('Multiple records returned for given traits', 'MULTIPLE_EXISTING_RECORDS', 400) + ) + }) + + test('should throw an IntegrationError if new constituent has no last name', async () => { + const event = createTestEvent(identifyEventDataNoLastName) + + nock(SKY_API_BASE_URL) + .get('/constituents/search?search_field=email_address&search_text=john@example.org') + .reply(200, { + count: 0, + value: [] + }) + + await expect( + testDestination.testAction('createOrUpdateIndividualConstituent', { + event, + mapping, + useDefaultMappings: true + }) + ).rejects.toThrowError(new IntegrationError('Missing last name value', 'MISSING_REQUIRED_FIELD', 400)) + }) + + test('should throw an IntegrationError if one or more request returns a 400 when updating an existing constituent', async () => { + const event = createTestEvent(identifyEventDataWithInvalidWebsite) + + nock(SKY_API_BASE_URL) + .get('/constituents/search?search_field=email_address&search_text=john@example.biz') + .reply(200, { + count: 1, + value: [ + { + id: '123', + address: 'PO Box 963\r\nNew York City, NY 10108', + email: 'john@example.biz', + fundraiser_status: 'None', + name: 'John Doe' + } + ] + }) + + nock(SKY_API_BASE_URL).patch('/constituents/123').reply(200) + + nock(SKY_API_BASE_URL) + .get('/constituents/123/addresses?include_inactive=true') + .reply(200, { + count: 1, + value: [ + { + id: '1000', + address_lines: 'PO Box 963', + city: 'New York City', + constituent_id: '123', + date_added: '2023-01-01T01:01:01.000-05:00', + date_modified: '2023-01-01T01:01:01.000-05:00', + do_not_mail: false, + formatted_address: 'PO Box 963\r\nNew York City, NY 10108', + inactive: false, + postal_code: '10108', + preferred: true, + state: 'NY', + type: 'Home' + } + ] + }) + + nock(SKY_API_BASE_URL).post('/addresses').reply(200, { + id: '1001' + }) + + nock(SKY_API_BASE_URL) + .get('/constituents/123/emailaddresses?include_inactive=true') + .reply(200, { + count: 1, + value: [ + { + id: '2000', + address: 'john@example.biz', + constituent_id: '123', + date_added: '2023-01-01T01:01:01.000-05:00', + date_modified: '2023-01-01T01:01:01.000-05:00', + do_not_email: false, + inactive: false, + primary: true, + type: 'Home' + } + ] + }) + + nock(SKY_API_BASE_URL).patch('/emailaddresses/2000').reply(200) + + nock(SKY_API_BASE_URL) + .get('/constituents/123/onlinepresences?include_inactive=true') + .reply(200, { + count: 1, + value: [ + { + id: '3000', + address: 'https://www.facebook.com/john.doe', + constituent_id: '123', + inactive: false, + primary: true, + type: 'Facebook' + } + ] + }) + + nock(SKY_API_BASE_URL).post('/onlinepresences').reply(400) + + nock(SKY_API_BASE_URL) + .get('/constituents/123/phones?include_inactive=true') + .reply(200, { + count: 1, + value: [ + { + id: '4000', + constituent_id: '123', + do_not_call: false, + inactive: false, + number: '+18774466722', + primary: true, + type: 'Home' + } + ] + }) + + nock(SKY_API_BASE_URL).post('/phones').reply(200, { + id: '4001' + }) + + await expect( + testDestination.testAction('createOrUpdateIndividualConstituent', { + event, + mapping, + useDefaultMappings: true + }) + ).rejects.toThrowError( + new IntegrationError( + 'One or more errors occurred when updating existing constituent: 400 error occurred when updating constituent online presence', + 'UPDATE_CONSTITUENT_ERROR', + 500 + ) + ) + }) + + test('should throw a RetryableError if a request returns a 429 when updating an existing constituent', async () => { + const event = createTestEvent(identifyEventDataUpdated) + + nock(SKY_API_BASE_URL) + .get('/constituents/search?search_field=email_address&search_text=john@example.biz') + .reply(200, { + count: 1, + value: [ + { + id: '123', + address: 'PO Box 963\r\nNew York City, NY 10108', + email: 'john@example.biz', + fundraiser_status: 'None', + name: 'John Doe' + } + ] + }) + + nock(SKY_API_BASE_URL).patch('/constituents/123').reply(200) + + nock(SKY_API_BASE_URL) + .get('/constituents/123/onlinepresences?include_inactive=true') + .reply(200, { + count: 1, + value: [ + { + id: '3001', + address: 'https://www.example.biz', + constituent_id: '123', + inactive: false, + primary: true, + type: 'Website' + } + ] + }) + + nock(SKY_API_BASE_URL) + .get('/constituents/123/addresses?include_inactive=true') + .reply(200, { + count: 1, + value: [ + { + id: '1000', + address_lines: 'PO Box 963', + city: 'New York City', + constituent_id: '123', + date_added: '2023-01-01T01:01:01.000-05:00', + date_modified: '2023-01-01T01:01:01.000-05:00', + do_not_mail: false, + formatted_address: 'PO Box 963\r\nNew York City, NY 10108', + inactive: false, + postal_code: '10108', + preferred: true, + state: 'NY', + type: 'Home' + } + ] + }) + + nock(SKY_API_BASE_URL).post('/addresses').reply(200, { + id: '1001' + }) + + nock(SKY_API_BASE_URL).get('/constituents/123/emailaddresses?include_inactive=true').reply(429) + + await expect( + testDestination.testAction('createOrUpdateIndividualConstituent', { + event, + mapping, + useDefaultMappings: true + }) + ).rejects.toThrowError(new RetryableError('429 error occurred when updating constituent email')) + }) +}) diff --git a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/__tests__/snapshot.test.ts new file mode 100644 index 0000000000..95e842c64d --- /dev/null +++ b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/__tests__/snapshot.test.ts @@ -0,0 +1,79 @@ +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import { generateTestData } from '../../../../lib/test-data' +import destination from '../../index' +import nock from 'nock' + +const testDestination = createTestIntegration(destination) +const actionSlug = 'createOrUpdateIndividualConstituent' +const destinationSlug = 'BlackbaudRaisersEdgeNxt' +const seedName = `${destinationSlug}#${actionSlug}` + +describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination action:`, () => { + it('required fields', async () => { + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, true) + + eventData.last = 'Smith' + + nock(/.*/).persist().get(/.*/).reply(200, {}) + nock(/.*/).persist().patch(/.*/).reply(200, {}) + nock(/.*/).persist().post(/.*/).reply(200, {}) + nock(/.*/).persist().put(/.*/).reply(200, {}) + + const event = createTestEvent({ + properties: eventData + }) + + const responses = await testDestination.testAction(actionSlug, { + event: event, + mapping: event.properties, + settings: settingsData, + auth: undefined + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + + expect(request.headers).toMatchSnapshot() + }) + + it('all fields', async () => { + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, false) + + nock(/.*/).persist().get(/.*/).reply(200, {}) + nock(/.*/).persist().patch(/.*/).reply(200, {}) + nock(/.*/).persist().post(/.*/).reply(200, {}) + nock(/.*/).persist().put(/.*/).reply(200, {}) + + const event = createTestEvent({ + properties: eventData + }) + + const responses = await testDestination.testAction(actionSlug, { + event: event, + mapping: event.properties, + settings: settingsData, + auth: undefined + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + }) +}) diff --git a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/fixtures.ts b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/fixtures.ts new file mode 100644 index 0000000000..96d7a7820e --- /dev/null +++ b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/fixtures.ts @@ -0,0 +1,169 @@ +import { SegmentEvent } from '@segment/actions-core' + +// identify events +export const identifyEventData: Partial = { + type: 'identify', + traits: { + address: { + city: 'New York City', + postalCode: '10108', + state: 'NY', + street: 'PO Box 963' + }, + addressType: 'Home', + email: 'john@example.biz', + emailType: 'Personal', + firstName: 'John', + lastName: 'Doe', + phone: '+18774466722', + phoneType: 'Home', + website: 'https://www.facebook.com/john.doe', + websiteType: 'Facebook' + } +} + +export const identifyEventDataNoEmail: Partial = { + type: 'identify', + traits: { + firstName: 'John', + lastName: 'Doe', + phone: '+18774466722', + phoneType: 'Home' + } +} + +export const identifyEventDataNoLastName: Partial = { + type: 'identify', + traits: { + email: 'john@example.org' + } +} + +export const identifyEventDataUpdated: Partial = { + ...identifyEventData, + traits: { + ...identifyEventData.traits, + address: { + city: 'New York', + postalCode: '10005', + state: 'NY', + street: '11 Wall St' + }, + addressType: 'Work', + emailType: 'Work', + phone: '+18774466723', + phoneType: 'Work', + website: 'https://www.example.biz', + websiteType: 'Website' + } +} + +export const identifyEventDataWithLookupId: Partial = { + ...identifyEventDataUpdated, + traits: { + ...identifyEventDataUpdated.traits, + address: { + ...(typeof identifyEventDataUpdated.traits?.address === 'object' ? identifyEventDataUpdated.traits.address : {}), + street: '11 Wall Street' + }, + birthday: '2001-01-01T01:01:01-05:00', + email: 'john.doe@aol.com', + emailType: 'Personal', + lookup_id: 'abcd1234' + } +} + +export const identifyEventDataWithInvalidWebsite: Partial = { + ...identifyEventDataUpdated, + traits: { + ...identifyEventDataUpdated.traits, + websiteType: 'Invalid' + } +} + +// constituent data +export const constituentPayload = { + address: { + address_lines: 'PO Box 963', + city: 'New York City', + state: 'NY', + postalCode: '10108', + type: 'Home' + }, + email: { + address: 'john@example.biz', + type: 'Personal' + }, + first: 'John', + last: 'Doe', + online_presence: { + address: 'https://www.facebook.com/john.doe', + type: 'Facebook' + }, + phone: { + number: '+18774466722', + type: 'Home' + }, + type: 'Individual' +} + +export const constituentPayloadNoEmail = { + first: 'John', + last: 'Doe', + phone: { + number: '+18774466722', + type: 'Home' + }, + type: 'Individual' +} + +export const constituentPayloadWithLookupId = { + birthdate: { + d: '1', + m: '1', + y: '2001' + }, + first: 'John', + last: 'Doe', + lookup_id: 'abcd1234' +} + +// address data +export const addressPayloadUpdated = { + address_lines: '11 Wall St', + city: 'New York', + state: 'NY', + postalCode: '10005', + type: 'Work' +} + +export const addressPayloadWithUpdatedStreet = { + address_lines: '11 Wall Street', + city: 'New York', + state: 'NY', + postalCode: '10005', + type: 'Work' +} + +// email data +export const emailPayloadUpdated = { + address: 'john@example.biz', + type: 'Work' +} + +export const emailPayloadPersonal = { + address: 'john.doe@aol.com', + type: 'Personal' +} + +// online presence data +export const onlinePresencePayloadUpdated = { + address: 'https://www.example.biz', + type: 'Website' +} + +// phone data +export const phonePayloadUpdated = { + number: '+18774466723', + type: 'Work' +} diff --git a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/generated-types.ts b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/generated-types.ts new file mode 100644 index 0000000000..d43337cd2f --- /dev/null +++ b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/generated-types.ts @@ -0,0 +1,67 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * The constituent's address. + */ + address?: { + address_lines?: string + city?: string + country?: string + do_not_mail?: boolean + postal_code?: string + primary?: boolean + state?: string + type?: string + } + /** + * The constituent's birthdate. + */ + birthdate?: string | number + /** + * The constituent's email address. + */ + email?: { + address?: string + do_not_email?: boolean + primary?: boolean + type?: string + } + /** + * The constituent's first name up to 50 characters. + */ + first?: string + /** + * The constituent's gender. + */ + gender?: string + /** + * The constituent's income. + */ + income?: string + /** + * The constituent's last name up to 100 characters. This is required to create a constituent. + */ + last?: string + /** + * The organization-defined identifier for the constituent. + */ + lookup_id?: string + /** + * The constituent's online presence. + */ + online_presence?: { + address?: string + primary?: boolean + type?: string + } + /** + * The constituent's phone number. + */ + phone?: { + do_not_call?: boolean + number?: string + primary?: boolean + type?: string + } +} diff --git a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/index.ts b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/index.ts new file mode 100644 index 0000000000..c523538b77 --- /dev/null +++ b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createOrUpdateIndividualConstituent/index.ts @@ -0,0 +1,751 @@ +import { ActionDefinition, IntegrationError, RetryableError } from '@segment/actions-core' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' +import { BlackbaudSkyApi } from '../api' +import { + Address, + Constituent, + Email, + ExistingAddress, + ExistingEmail, + ExistingOnlinePresence, + ExistingPhone, + OnlinePresence, + Phone +} from '../types' +import { dateStringToFuzzyDate, filterObjectListByMatchFields, isRequestErrorRetryable } from '../utils' + +const action: ActionDefinition = { + title: 'Create or Update Individual Constituent', + description: "Create or update an Individual Constituent record in Raiser's Edge NXT.", + defaultSubscription: 'type = "identify"', + fields: { + address: { + label: 'Address', + description: "The constituent's address.", + type: 'object', + properties: { + address_lines: { + label: 'Address Lines', + type: 'string' + }, + city: { + label: 'City', + type: 'string' + }, + country: { + label: 'Country', + type: 'string' + }, + do_not_mail: { + label: 'Do Not Mail', + type: 'boolean' + }, + postal_code: { + label: 'ZIP/Postal Code', + type: 'string' + }, + primary: { + label: 'Is Primary', + type: 'boolean' + }, + state: { + label: 'State/Province', + type: 'string' + }, + type: { + label: 'Address Type', + type: 'string' + } + }, + default: { + address_lines: { + '@if': { + exists: { + '@path': '$.traits.address.street' + }, + then: { + '@path': '$.traits.address.street' + }, + else: { + '@path': '$.properties.address.street' + } + } + }, + city: { + '@if': { + exists: { + '@path': '$.traits.address.city' + }, + then: { + '@path': '$.traits.address.city' + }, + else: { + '@path': '$.properties.address.city' + } + } + }, + country: { + '@if': { + exists: { + '@path': '$.traits.address.country' + }, + then: { + '@path': '$.traits.address.country' + }, + else: { + '@path': '$.properties.address.country' + } + } + }, + do_not_mail: '', + postal_code: { + '@if': { + exists: { + '@path': '$.traits.address.postalCode' + }, + then: { + '@path': '$.traits.address.postalCode' + }, + else: { + '@path': '$.properties.address.postalCode' + } + } + }, + primary: '', + state: { + '@if': { + exists: { + '@path': '$.traits.address.state' + }, + then: { + '@path': '$.traits.address.state' + }, + else: { + '@path': '$.properties.address.state' + } + } + }, + type: '' + } + }, + birthdate: { + label: 'Birthdate', + description: "The constituent's birthdate.", + type: 'datetime', + default: { + '@if': { + exists: { + '@path': '$.traits.birthday' + }, + then: { + '@path': '$.traits.birthday' + }, + else: { + '@path': '$.properties.birthday' + } + } + } + }, + email: { + label: 'Email', + description: "The constituent's email address.", + type: 'object', + properties: { + address: { + label: 'Email Address', + type: 'string' + }, + do_not_email: { + label: 'Do Not Email', + type: 'boolean' + }, + primary: { + label: 'Is Primary', + type: 'boolean' + }, + type: { + label: 'Email Type', + type: 'string' + } + }, + default: { + address: { + '@if': { + exists: { + '@path': '$.traits.email' + }, + then: { + '@path': '$.traits.email' + }, + else: { + '@path': '$.properties.email' + } + } + }, + do_not_email: '', + primary: '', + type: '' + } + }, + first: { + label: 'First Name', + description: "The constituent's first name up to 50 characters.", + type: 'string', + default: { + '@if': { + exists: { + '@path': '$.traits.firstName' + }, + then: { + '@path': '$.traits.firstName' + }, + else: { + '@path': '$.properties.firstName' + } + } + } + }, + gender: { + label: 'Gender', + description: "The constituent's gender.", + type: 'string', + default: { + '@if': { + exists: { + '@path': '$.traits.gender' + }, + then: { + '@path': '$.traits.gender' + }, + else: { + '@path': '$.properties.gender' + } + } + } + }, + income: { + label: 'Income', + description: "The constituent's income.", + type: 'string' + }, + last: { + label: 'Last Name', + description: "The constituent's last name up to 100 characters. This is required to create a constituent.", + type: 'string', + default: { + '@if': { + exists: { + '@path': '$.traits.lastName' + }, + then: { + '@path': '$.traits.lastName' + }, + else: { + '@path': '$.properties.lastName' + } + } + } + }, + lookup_id: { + label: 'Lookup ID', + description: 'The organization-defined identifier for the constituent.', + type: 'string', + default: '' + }, + online_presence: { + label: 'Online Presence', + description: "The constituent's online presence.", + type: 'object', + properties: { + address: { + label: 'Web Address', + type: 'string' + }, + primary: { + label: 'Is Primary', + type: 'boolean' + }, + type: { + label: 'Online Presence Type', + type: 'string' + } + }, + default: { + address: { + '@if': { + exists: { + '@path': '$.traits.website' + }, + then: { + '@path': '$.traits.website' + }, + else: { + '@path': '$.properties.website' + } + } + }, + primary: '', + type: '' + } + }, + phone: { + label: 'Phone', + description: "The constituent's phone number.", + type: 'object', + properties: { + do_not_call: { + label: 'Do Not Call', + type: 'boolean' + }, + number: { + label: 'Phone Number', + type: 'string' + }, + primary: { + label: 'Is Primary', + type: 'boolean' + }, + type: { + label: 'Phone Type', + type: 'string' + } + }, + default: { + do_not_call: '', + number: { + '@if': { + exists: { + '@path': '$.traits.phone' + }, + then: { + '@path': '$.traits.phone' + }, + else: { + '@path': '$.properties.phone' + } + } + }, + primary: '', + type: '' + } + } + }, + perform: async (request, { payload }) => { + const blackbaudSkyApiClient: BlackbaudSkyApi = new BlackbaudSkyApi(request) + + // search for existing constituent + let constituentId = undefined + if (payload.email?.address || payload.lookup_id) { + // default to searching by email + let searchField = 'email_address' + let searchText = payload.email?.address || '' + + if (payload.lookup_id) { + // search by lookup_id if one is provided + searchField = 'lookup_id' + searchText = payload.lookup_id + } + + const constituentSearchResponse = await blackbaudSkyApiClient.getExistingConstituents(searchField, searchText) + const constituentSearchResults = await constituentSearchResponse.json() + + if (constituentSearchResults.count > 1) { + // multiple existing constituents, throw an error + throw new IntegrationError('Multiple records returned for given traits', 'MULTIPLE_EXISTING_RECORDS', 400) + } else if (constituentSearchResults.count === 1) { + // existing constituent + constituentId = constituentSearchResults.value[0].id + } + } + + // data for constituent call + const constituentData: Constituent = { + first: payload.first, + gender: payload.gender, + income: payload.income, + last: payload.last, + lookup_id: payload.lookup_id + } + Object.keys(constituentData).forEach((key) => { + if (!constituentData[key as keyof Constituent]) { + delete constituentData[key as keyof Constituent] + } + }) + if (payload.birthdate) { + const birthdateFuzzyDate = dateStringToFuzzyDate(payload.birthdate) + if (birthdateFuzzyDate) { + constituentData.birthdate = birthdateFuzzyDate + } + } + + // data for address call + let constituentAddressData: Address = {} + if ( + payload.address && + (payload.address.address_lines || + payload.address.city || + payload.address.country || + payload.address.postal_code || + payload.address.state) && + payload.address.type + ) { + constituentAddressData = payload.address + } + + // data for email call + let constituentEmailData: Email = {} + if (payload.email && payload.email.address && payload.email.type) { + constituentEmailData = payload.email + } + + // data for online presence call + let constituentOnlinePresenceData: OnlinePresence = {} + if (payload.online_presence && payload.online_presence.address && payload.online_presence.type) { + constituentOnlinePresenceData = payload.online_presence + } + + // data for phone call + let constituentPhoneData: Phone = {} + if (payload.phone && payload.phone.number && payload.phone.type) { + constituentPhoneData = payload.phone + } + + if (!constituentId) { + // new constituent + // hardcode type + constituentData.type = 'Individual' + if (!constituentData.last) { + // last name is required to create a new constituent + // no last name, throw an error + throw new IntegrationError('Missing last name value', 'MISSING_REQUIRED_FIELD', 400) + } else { + // request has last name + // append other data objects to constituent + if (Object.keys(constituentAddressData).length > 0) { + constituentData.address = constituentAddressData + } + if (Object.keys(constituentEmailData).length > 0) { + constituentData.email = constituentEmailData + } + if (Object.keys(constituentOnlinePresenceData).length > 0) { + constituentData.online_presence = constituentOnlinePresenceData + } + if (Object.keys(constituentPhoneData).length > 0) { + constituentData.phone = constituentPhoneData + } + + // create constituent + await blackbaudSkyApiClient.createConstituent(constituentData) + } + + return + } else { + // existing constituent + // aggregate all errors + const integrationErrors = [] + if (Object.keys(constituentData).length > 0) { + // request has at least one constituent field to update + // update constituent + const updateConstituentResponse = await blackbaudSkyApiClient.updateConstituent(constituentId, constituentData) + if (updateConstituentResponse.status !== 200) { + const statusCode = updateConstituentResponse.status + const errorMessage = statusCode + ? `${statusCode} error occurred when updating constituent` + : 'Error occurred when updating constituent' + if (isRequestErrorRetryable(statusCode)) { + throw new RetryableError(errorMessage) + } else { + integrationErrors.push(errorMessage) + } + } + } + + if (Object.keys(constituentAddressData).length > 0) { + // request has address data + // get existing addresses + const getConstituentAddressListResponse = await blackbaudSkyApiClient.getConstituentAddressList(constituentId) + let updateAddressErrorCode = undefined + if (getConstituentAddressListResponse.status !== 200) { + updateAddressErrorCode = getConstituentAddressListResponse.status + } else { + const constituentAddressListResults = await getConstituentAddressListResponse.json() + + // check address list for one that matches request + let existingAddress: ExistingAddress | undefined = undefined + if (constituentAddressListResults.count > 0) { + existingAddress = filterObjectListByMatchFields( + constituentAddressListResults.value, + constituentAddressData, + ['address_lines', 'city', 'postal_code', 'state'] + ) as ExistingAddress | undefined + } + + if (!existingAddress) { + // new address + // if this is the only address, make it primary + if (constituentAddressData.primary !== false && constituentAddressListResults.count === 0) { + constituentAddressData.primary = true + } + // create address + const createConstituentAddressResponse = await blackbaudSkyApiClient.createConstituentAddress( + constituentId, + constituentAddressData + ) + if (createConstituentAddressResponse.status !== 200) { + updateAddressErrorCode = createConstituentAddressResponse.status + } + } else { + // existing address + if ( + existingAddress.inactive || + (constituentAddressData.do_not_mail !== undefined && + constituentAddressData.do_not_mail !== existingAddress.do_not_mail) || + (constituentAddressData.primary !== undefined && + constituentAddressData.primary && + constituentAddressData.primary !== existingAddress.primary) || + constituentAddressData.type !== existingAddress.type + ) { + // request has at least one address field to update + // update address + const updateConstituentAddressByIdResponse = await blackbaudSkyApiClient.updateConstituentAddressById( + existingAddress.id, + constituentAddressData + ) + if (updateConstituentAddressByIdResponse.status !== 200) { + updateAddressErrorCode = updateConstituentAddressByIdResponse.status + } + } + } + } + + if (updateAddressErrorCode) { + const errorMessage = updateAddressErrorCode + ? `${updateAddressErrorCode} error occurred when updating constituent address` + : 'Error occurred when updating constituent address' + if (isRequestErrorRetryable(updateAddressErrorCode)) { + throw new RetryableError(errorMessage) + } else { + integrationErrors.push(errorMessage) + } + } + } + + if (Object.keys(constituentEmailData).length > 0) { + // request has email data + // get existing addresses + const getConstituentEmailListResponse = await blackbaudSkyApiClient.getConstituentEmailList(constituentId) + let updateEmailErrorCode = undefined + if (getConstituentEmailListResponse.status !== 200) { + updateEmailErrorCode = getConstituentEmailListResponse.status + } else { + const constituentEmailListResults = await getConstituentEmailListResponse.json() + + // check email list for one that matches request + let existingEmail: ExistingEmail | undefined = undefined + if (constituentEmailListResults.count > 0) { + existingEmail = filterObjectListByMatchFields(constituentEmailListResults.value, constituentEmailData, [ + 'address' + ]) as ExistingEmail | undefined + } + + if (!existingEmail) { + // new email + // if this is the only email, make it primary + if (constituentEmailData.primary !== false && constituentEmailListResults.count === 0) { + constituentEmailData.primary = true + } + // create email + const createConstituentEmailResponse = await blackbaudSkyApiClient.createConstituentEmail( + constituentId, + constituentEmailData + ) + if (createConstituentEmailResponse.status !== 200) { + updateEmailErrorCode = createConstituentEmailResponse.status + } + } else { + // existing email + if ( + existingEmail.inactive || + (constituentEmailData.do_not_email !== undefined && + constituentEmailData.do_not_email !== existingEmail.do_not_email) || + (constituentEmailData.primary !== undefined && + constituentEmailData.primary && + constituentEmailData.primary !== existingEmail.primary) || + constituentEmailData.type !== existingEmail.type + ) { + // request has at least one email field to update + // update email + const updateConstituentEmailByIdResponse = await blackbaudSkyApiClient.updateConstituentEmailById( + existingEmail.id, + constituentEmailData + ) + if (updateConstituentEmailByIdResponse.status !== 200) { + updateEmailErrorCode = updateConstituentEmailByIdResponse.status + } + } + } + } + + if (updateEmailErrorCode) { + const errorMessage = updateEmailErrorCode + ? `${updateEmailErrorCode} error occurred when updating constituent email` + : 'Error occurred when updating constituent email' + if (isRequestErrorRetryable(updateEmailErrorCode)) { + throw new RetryableError(errorMessage) + } else { + integrationErrors.push(errorMessage) + } + } + } + + if (Object.keys(constituentOnlinePresenceData).length > 0) { + // request has online presence data + // get existing online presences + const getConstituentOnlinePresenceListResponse = await blackbaudSkyApiClient.getConstituentOnlinePresenceList( + constituentId + ) + let updateOnlinePresenceErrorCode = undefined + if (getConstituentOnlinePresenceListResponse.status !== 200) { + updateOnlinePresenceErrorCode = getConstituentOnlinePresenceListResponse.status + } else { + const constituentOnlinePresenceListResults = await getConstituentOnlinePresenceListResponse.json() + + // check online presence list for one that matches request + let existingOnlinePresence: ExistingOnlinePresence | undefined = undefined + if (constituentOnlinePresenceListResults.count > 0) { + existingOnlinePresence = filterObjectListByMatchFields( + constituentOnlinePresenceListResults.value, + constituentOnlinePresenceData, + ['address'] + ) as ExistingOnlinePresence | undefined + } + + if (!existingOnlinePresence) { + // new online presence + // if this is the only online presence, make it primary + if (constituentOnlinePresenceData.primary !== false && constituentOnlinePresenceListResults.count === 0) { + constituentOnlinePresenceData.primary = true + } + // create online presence + const createConstituentOnlinePresenceResponse = await blackbaudSkyApiClient.createConstituentOnlinePresence( + constituentId, + constituentOnlinePresenceData + ) + if (createConstituentOnlinePresenceResponse.status !== 200) { + updateOnlinePresenceErrorCode = createConstituentOnlinePresenceResponse.status + } + } else { + // existing online presence + if ( + existingOnlinePresence.inactive || + (constituentOnlinePresenceData.primary !== undefined && + constituentOnlinePresenceData.primary !== existingOnlinePresence.primary) || + constituentOnlinePresenceData.type !== existingOnlinePresence.type + ) { + // request has at least one online presence field to update + // update online presence + const updateConstituentOnlinePresenceByIdResponse = + await blackbaudSkyApiClient.updateConstituentOnlinePresenceById( + existingOnlinePresence.id, + constituentOnlinePresenceData + ) + if (updateConstituentOnlinePresenceByIdResponse.status !== 200) { + updateOnlinePresenceErrorCode = updateConstituentOnlinePresenceByIdResponse.status + } + } + } + } + + if (updateOnlinePresenceErrorCode) { + const errorMessage = updateOnlinePresenceErrorCode + ? `${updateOnlinePresenceErrorCode} error occurred when updating constituent online presence` + : 'Error occurred when updating constituent online presence' + if (isRequestErrorRetryable(updateOnlinePresenceErrorCode)) { + throw new RetryableError(errorMessage) + } else { + integrationErrors.push(errorMessage) + } + } + } + + if (Object.keys(constituentPhoneData).length > 0) { + // request has phone data + // get existing phones + const getConstituentPhoneListResponse = await blackbaudSkyApiClient.getConstituentPhoneList(constituentId) + let updatePhoneErrorCode = undefined + if (getConstituentPhoneListResponse.status !== 200) { + updatePhoneErrorCode = getConstituentPhoneListResponse.status + } else { + const constituentPhoneListResults = await getConstituentPhoneListResponse.json() + + // check phone list for one that matches request + let existingPhone: ExistingPhone | undefined = undefined + if (constituentPhoneListResults.count > 0) { + existingPhone = filterObjectListByMatchFields(constituentPhoneListResults.value, constituentPhoneData, [ + 'int:number' + ]) as ExistingPhone | undefined + } + + if (!existingPhone) { + // new phone + // if this is the only phone, make it primary + if (constituentPhoneData.primary !== false && constituentPhoneListResults.count === 0) { + constituentPhoneData.primary = true + } + // create phone + const createConstituentPhoneResponse = await blackbaudSkyApiClient.createConstituentPhone( + constituentId, + constituentPhoneData + ) + if (createConstituentPhoneResponse.status !== 200) { + updatePhoneErrorCode = createConstituentPhoneResponse.status + } + } else { + // existing phone + if ( + existingPhone.inactive || + (constituentPhoneData.do_not_call !== undefined && + constituentPhoneData.do_not_call !== existingPhone.do_not_call) || + (constituentPhoneData.primary !== undefined && constituentPhoneData.primary !== existingPhone.primary) || + constituentPhoneData.type !== existingPhone.type + ) { + // request has at least one phone field to update + // update phone + const updateConstituentPhoneByIdResponse = await blackbaudSkyApiClient.updateConstituentPhoneById( + existingPhone.id, + constituentPhoneData + ) + if (updateConstituentPhoneByIdResponse.status !== 200) { + updatePhoneErrorCode = updateConstituentPhoneByIdResponse.status + } + } + } + } + + if (updatePhoneErrorCode) { + const errorMessage = updatePhoneErrorCode + ? `${updatePhoneErrorCode} error occurred when updating constituent online presence` + : 'Error occurred when updating constituent online presence' + if (isRequestErrorRetryable(updatePhoneErrorCode)) { + throw new RetryableError(errorMessage) + } else { + integrationErrors.push(errorMessage) + } + } + } + + if (integrationErrors.length > 0) { + throw new IntegrationError( + 'One or more errors occurred when updating existing constituent: ' + integrationErrors.join(', '), + 'UPDATE_CONSTITUENT_ERROR', + 500 + ) + } + + return + } + } +} + +export default action diff --git a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/generated-types.ts b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/generated-types.ts new file mode 100644 index 0000000000..0f7daac31a --- /dev/null +++ b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/generated-types.ts @@ -0,0 +1,8 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Settings { + /** + * The access key found on your Blackbaud "My subscriptions" page. + */ + bbApiSubscriptionKey: string +} diff --git a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/index.ts b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/index.ts new file mode 100644 index 0000000000..280cc3651b --- /dev/null +++ b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/index.ts @@ -0,0 +1,61 @@ +import type { DestinationDefinition } from '@segment/actions-core' +import type { Settings } from './generated-types' +import { SKY_API_BASE_URL, SKY_OAUTH2_TOKEN_URL } from './constants' +import { RefreshTokenResponse } from './types' +import createOrUpdateIndividualConstituent from './createOrUpdateIndividualConstituent' + +const destination: DestinationDefinition = { + name: "Blackbaud Raiser's Edge NXT", + slug: 'actions-blackbaud-raisers-edge-nxt', + mode: 'cloud', + + authentication: { + scheme: 'oauth-managed', + fields: { + bbApiSubscriptionKey: { + label: 'Blackbaud API Subscription Key', + description: 'The access key found on your Blackbaud "My subscriptions" page.', + type: 'string', + required: true + } + }, + testAuthentication: (request) => { + return request(`${SKY_API_BASE_URL}/emailaddresstypes`) + }, + refreshAccessToken: async (request, { auth }) => { + // Return a request that refreshes the access_token if the API supports it + const res = await request(SKY_OAUTH2_TOKEN_URL, { + method: 'POST', + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: auth.refreshToken, + client_id: auth.clientId, + client_secret: auth.clientSecret + }) + }) + + return { accessToken: res.data.access_token } + } + }, + extendRequest({ auth, settings }) { + return { + headers: { + authorization: `Bearer ${auth?.accessToken}`, + 'Bb-Api-Subscription-Key': `${settings.bbApiSubscriptionKey}` + } + } + }, + + //onDelete: async (request, { settings, payload }) => { + onDelete: async () => { + // Return a request that performs a GDPR delete for the provided Segment userId or anonymousId + // provided in the payload. If your destination does not support GDPR deletion you should not + // implement this function and should remove it completely. + }, + + actions: { + createOrUpdateIndividualConstituent + } +} + +export default destination diff --git a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/types/index.ts b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/types/index.ts new file mode 100644 index 0000000000..5d25799dd2 --- /dev/null +++ b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/types/index.ts @@ -0,0 +1,78 @@ +export interface RefreshTokenResponse { + access_token: string +} + +export interface StringIndexedObject { + [key: string]: any +} + +export interface FuzzyDate { + d: string + m: string + y: string +} + +export interface Constituent { + address?: Address + birthdate?: FuzzyDate + email?: Email + first?: string + gender?: string + income?: string + last?: string + lookup_id?: string + online_presence?: OnlinePresence + phone?: Phone + type?: string +} + +export interface Address { + address_lines?: string + city?: string + country?: string + do_not_mail?: boolean + postal_code?: string + primary?: boolean + state?: string + type?: string + inactive?: boolean +} + +export interface ExistingAddress extends Address { + id: string +} + +export interface Email { + address?: string + do_not_email?: boolean + primary?: boolean + type?: string + inactive?: boolean +} + +export interface ExistingEmail extends Email { + id: string +} + +export interface OnlinePresence { + address?: string + primary?: boolean + type?: string + inactive?: boolean +} + +export interface ExistingOnlinePresence extends OnlinePresence { + id: string +} + +export interface Phone { + do_not_call?: boolean + number?: string + primary?: boolean + type?: string + inactive?: boolean +} + +export interface ExistingPhone extends Phone { + id: string +} diff --git a/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/utils/index.ts b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/utils/index.ts new file mode 100644 index 0000000000..6c49070771 --- /dev/null +++ b/packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/utils/index.ts @@ -0,0 +1,48 @@ +import { StringIndexedObject } from '../types' + +export const dateStringToFuzzyDate = (dateString: string | number) => { + const date = new Date(dateString) + if (isNaN(date.getTime())) { + // invalid date object + return false + } else { + // valid date object + // convert date to a "Fuzzy date" + // https://developer.blackbaud.com/skyapi/renxt/constituent/entities#FuzzyDate + return { + d: date.getDate().toString(), + m: (date.getMonth() + 1).toString(), + y: date.getFullYear().toString() + } + } +} + +export const filterObjectListByMatchFields = ( + list: StringIndexedObject[], + data: StringIndexedObject, + matchFields: string[] +) => { + return list.find((item: StringIndexedObject) => { + let isMatch: boolean | undefined = undefined + matchFields.forEach((field: string) => { + if (isMatch !== false) { + let fieldName = field + if (field.startsWith('int:')) { + fieldName = field.split('int:')[1] + } + let itemValue = item[fieldName] ? item[fieldName].toLowerCase() : '' + let dataValue = data[fieldName] ? data[fieldName].toLowerCase() : '' + if (field.startsWith('int:')) { + itemValue = itemValue.replace(/\D/g, '') + dataValue = dataValue.replace(/\D/g, '') + } + isMatch = itemValue === dataValue + } + }) + return isMatch + }) +} + +export const isRequestErrorRetryable = (statusCode: number) => { + return statusCode === 429 || statusCode >= 500 +} diff --git a/packages/destination-actions/src/destinations/braze-cohorts/syncAudiences/generated-types.ts b/packages/destination-actions/src/destinations/braze-cohorts/syncAudiences/generated-types.ts index d48c2e9fd4..eb15fa0ee9 100644 --- a/packages/destination-actions/src/destinations/braze-cohorts/syncAudiences/generated-types.ts +++ b/packages/destination-actions/src/destinations/braze-cohorts/syncAudiences/generated-types.ts @@ -6,14 +6,14 @@ export interface Payload { */ external_id?: string /** - * A user alias object. See [the docs](https://www.braze.com/docs/api/objects_filters/user_alias_object/). + * Alternate unique user identifier, this is required if External User ID or Device ID is not set. Refer [Braze Documentation](https://www.braze.com/docs/api/objects_filters/user_alias_object) for more details. */ user_alias?: { alias_name: string alias_label: string } /** - * The unique device Identifier + * Device IDs can be used to add and remove only anonymous users to/from a cohort. However, users with an assigned User ID cannot use Device ID to sync to a cohort. */ device_id?: string /** @@ -33,7 +33,7 @@ export interface Payload { */ personas_audience_key: string /** - * Properties of the event + * Displays properties of the event to add/remove users to a cohort and the traits of the specific user */ event_properties: { [k: string]: unknown diff --git a/packages/destination-actions/src/destinations/braze-cohorts/syncAudiences/index.ts b/packages/destination-actions/src/destinations/braze-cohorts/syncAudiences/index.ts index 29b3404deb..0ea3662245 100644 --- a/packages/destination-actions/src/destinations/braze-cohorts/syncAudiences/index.ts +++ b/packages/destination-actions/src/destinations/braze-cohorts/syncAudiences/index.ts @@ -23,7 +23,7 @@ const action: ActionDefinition = { user_alias: { label: 'User Alias Object', description: - 'A user alias object. See [the docs](https://www.braze.com/docs/api/objects_filters/user_alias_object/).', + 'Alternate unique user identifier, this is required if External User ID or Device ID is not set. Refer [Braze Documentation](https://www.braze.com/docs/api/objects_filters/user_alias_object) for more details.', type: 'object', properties: { alias_name: { @@ -40,7 +40,8 @@ const action: ActionDefinition = { }, device_id: { label: 'Device ID', - description: 'The unique device Identifier', + description: + 'Device IDs can be used to add and remove only anonymous users to/from a cohort. However, users with an assigned User ID cannot use Device ID to sync to a cohort.', type: 'string' }, cohort_id: { @@ -76,7 +77,8 @@ const action: ActionDefinition = { }, event_properties: { label: 'Event Properties', - description: 'Properties of the event', + description: + 'Displays properties of the event to add/remove users to a cohort and the traits of the specific user', type: 'object', required: true, default: { diff --git a/packages/destination-actions/src/destinations/customerio/__tests__/createUpdateObject.test.ts b/packages/destination-actions/src/destinations/customerio/__tests__/createUpdateObject.test.ts index 07637936fd..0524402d25 100644 --- a/packages/destination-actions/src/destinations/customerio/__tests__/createUpdateObject.test.ts +++ b/packages/destination-actions/src/destinations/customerio/__tests__/createUpdateObject.test.ts @@ -60,8 +60,8 @@ describe('CustomerIO', () => { type: 'object', action: 'identify', identifiers: { - type_id: traits.object_type_id, - id: groupId + object_type_id: traits.object_type_id, + object_id: groupId }, cio_relationships: [{ identifiers: { id: userId } }] }) @@ -117,8 +117,8 @@ describe('CustomerIO', () => { type: 'object', action: 'identify', identifiers: { - type_id: traits.object_type_id, - id: groupId + object_type_id: traits.object_type_id, + object_id: groupId }, cio_relationships: [{ identifiers: { id: userId } }] }) @@ -172,8 +172,8 @@ describe('CustomerIO', () => { type: 'object', action: 'identify', identifiers: { - type_id: traits.object_type_id, - id: groupId + object_type_id: traits.object_type_id, + object_id: groupId }, cio_relationships: [{ identifiers: { id: userId } }] }) @@ -226,8 +226,8 @@ describe('CustomerIO', () => { type: 'object', action: 'identify_anonymous', identifiers: { - type_id: traits.object_type_id, - id: groupId + object_type_id: traits.object_type_id, + object_id: groupId }, cio_relationships: [{ identifiers: { anonymous_id: anonymousId } }] }) @@ -279,8 +279,8 @@ describe('CustomerIO', () => { type: 'object', action: 'identify', identifiers: { - type_id: '1', - id: groupId + object_type_id: '1', + object_id: groupId }, cio_relationships: [{ identifiers: { id: userId } }] }) @@ -327,8 +327,8 @@ describe('CustomerIO', () => { type: 'object', action: 'identify', identifiers: { - type_id: typeId, - id: groupId + object_type_id: typeId, + object_id: groupId }, cio_relationships: [{ identifiers: { id: userId } }] }) diff --git a/packages/destination-actions/src/destinations/customerio/__tests__/createUpdatePerson.test.ts b/packages/destination-actions/src/destinations/customerio/__tests__/createUpdatePerson.test.ts index a89b4207f8..08ae92f62a 100644 --- a/packages/destination-actions/src/destinations/customerio/__tests__/createUpdatePerson.test.ts +++ b/packages/destination-actions/src/destinations/customerio/__tests__/createUpdatePerson.test.ts @@ -463,7 +463,7 @@ describe('CustomerIO', () => { }, cio_relationships: { action: 'add_relationships', - relationships: [{ identifiers: { type_id: '1', id: groupId } }] + relationships: [{ identifiers: { object_type_id: '1', object_id: groupId } }] } }) }) diff --git a/packages/destination-actions/src/destinations/customerio/createUpdateObject/generated-types.ts b/packages/destination-actions/src/destinations/customerio/createUpdateObject/generated-types.ts index d18aeb3f35..c1dec3a34c 100644 --- a/packages/destination-actions/src/destinations/customerio/createUpdateObject/generated-types.ts +++ b/packages/destination-actions/src/destinations/customerio/createUpdateObject/generated-types.ts @@ -26,7 +26,7 @@ export interface Payload { /** * The ID used to uniquely identify a custom object type in Customer.io. [Learn more](https://customer.io/docs/object-relationships). */ - type_id?: string + object_type_id?: string /** * Convert dates to Unix timestamps (seconds since Epoch). */ diff --git a/packages/destination-actions/src/destinations/customerio/createUpdateObject/index.ts b/packages/destination-actions/src/destinations/customerio/createUpdateObject/index.ts index 2bde18c7fd..064708f006 100644 --- a/packages/destination-actions/src/destinations/customerio/createUpdateObject/index.ts +++ b/packages/destination-actions/src/destinations/customerio/createUpdateObject/index.ts @@ -6,7 +6,7 @@ import { convertAttributeTimestamps, convertValidTimestamp, trackApiEndpoint } f const action: ActionDefinition = { title: 'Create or Update Object', description: 'Create an object in Customer.io or update them if they exist.', - defaultSubscription: 'type = "object"', + defaultSubscription: 'type = "group"', fields: { id: { label: 'Object ID', @@ -53,13 +53,14 @@ const action: ActionDefinition = { '@path': '$.anonymousId' } }, - type_id: { + + object_type_id: { label: 'Object Type Id', description: 'The ID used to uniquely identify a custom object type in Customer.io. [Learn more](https://customer.io/docs/object-relationships).', type: 'string', default: { - '@path': '$.typeId' + '@path': '$.objectTypeId' } }, convert_timestamp: { @@ -72,7 +73,7 @@ const action: ActionDefinition = { perform: (request, { settings, payload }) => { let createdAt: string | number | undefined = payload.created_at let customAttributes = payload.custom_attributes - const typeID = payload.type_id + const objectTypeID = payload.object_type_id const userID = payload.user_id const objectID = payload.id const anonymousId = payload.anonymous_id @@ -92,7 +93,7 @@ const action: ActionDefinition = { body.created_at = createdAt } body.type = 'object' - body.identifiers = { type_id: typeID ?? '1', id: objectID } + body.identifiers = { object_type_id: objectTypeID ?? '1', object_id: objectID } if (userID) { body.action = 'identify' diff --git a/packages/destination-actions/src/destinations/customerio/createUpdatePerson/index.ts b/packages/destination-actions/src/destinations/customerio/createUpdatePerson/index.ts index 67b6585c24..cf6bfc3a15 100644 --- a/packages/destination-actions/src/destinations/customerio/createUpdatePerson/index.ts +++ b/packages/destination-actions/src/destinations/customerio/createUpdatePerson/index.ts @@ -53,7 +53,7 @@ const action: ActionDefinition = { 'The ID used to uniquely identify an object in Customer.io. [Learn more](https://customer.io/docs/object-relationships).', type: 'string', default: { - '@template': '{{context.groupId}}' + '@path': '$.context.groupId' } }, custom_attributes: { @@ -112,7 +112,7 @@ const action: ActionDefinition = { if (objectId) { body.cio_relationships = { action: 'add_relationships', - relationships: [{ identifiers: { type_id: objectTypeId ?? '1', id: objectId } }] + relationships: [{ identifiers: { object_type_id: objectTypeId ?? '1', object_id: objectId } }] } } diff --git a/packages/destination-actions/src/destinations/engage-messaging-twilio/__tests__/send-whatsapp.test.ts b/packages/destination-actions/src/destinations/engage-messaging-twilio/__tests__/send-whatsapp.test.ts new file mode 100644 index 0000000000..969d606295 --- /dev/null +++ b/packages/destination-actions/src/destinations/engage-messaging-twilio/__tests__/send-whatsapp.test.ts @@ -0,0 +1,379 @@ +import nock from 'nock' +import { createTestIntegration } from '@segment/actions-core' +import { createMessagingTestEvent } from '../../../lib/engage-test-data/create-messaging-test-event' +import Twilio from '..' + +const twilio = createTestIntegration(Twilio) +const timestamp = new Date().toISOString() +const defaultTemplateSid = 'my_template' +const defaultTo = 'whatsapp:+1234567891' + +describe.each(['stage', 'production'])('%s environment', (environment) => { + const settings = { + twilioAccountSID: 'a', + twilioApiKeySID: 'f', + twilioApiKeySecret: 'b', + profileApiEnvironment: environment, + profileApiAccessToken: 'c', + spaceId: 'd', + sourceId: 'e' + } + const getDefaultMapping = (overrides?: any) => { + return { + userId: { '@path': '$.userId' }, + from: 'MG1111222233334444', + contentSid: defaultTemplateSid, + send: true, + traitEnrichment: true, + externalIds: [ + { type: 'email', id: 'test@twilio.com', subscriptionStatus: 'subscribed' }, + { type: 'phone', id: '+1234567891', subscriptionStatus: 'subscribed' } + ], + ...overrides + } + } + + afterEach(() => { + twilio.responses = [] + nock.cleanAll() + }) + + describe('send WhatsApp', () => { + it('should abort when there is no `phone` external ID in the payload', async () => { + const responses = await twilio.testAction('sendWhatsApp', { + event: createMessagingTestEvent({ + timestamp, + event: 'Audience Entered', + userId: 'jane' + }), + settings, + mapping: getDefaultMapping({ + externalIds: [{ type: 'email', id: 'test@twilio.com', subscriptionStatus: 'subscribed' }] + }) + }) + + expect(responses.length).toEqual(0) + }) + + it('should send WhatsApp', async () => { + const expectedTwilioRequest = new URLSearchParams({ + ContentSid: defaultTemplateSid, + From: 'MG1111222233334444', + To: defaultTo + }) + + const twilioRequest = nock('https://api.twilio.com/2010-04-01/Accounts/a') + .post('/Messages.json', expectedTwilioRequest.toString()) + .reply(201, {}) + + const actionInputData = { + event: createMessagingTestEvent({ + timestamp, + event: 'Audience Entered', + userId: 'jane' + }), + settings, + mapping: getDefaultMapping() + } + + const responses = await twilio.testAction('sendWhatsApp', actionInputData) + expect(responses.map((response) => response.url)).toStrictEqual([ + 'https://api.twilio.com/2010-04-01/Accounts/a/Messages.json' + ]) + expect(twilioRequest.isDone()).toEqual(true) + }) + + it('should send WhatsApp for custom hostname', async () => { + const expectedTwilioRequest = new URLSearchParams({ + ContentSid: defaultTemplateSid, + From: 'MG1111222233334444', + To: defaultTo + }) + + const twilioHostname = 'api.nottwilio.com' + + const twilioRequest = nock(`https://${twilioHostname}/2010-04-01/Accounts/a`) + .post('/Messages.json', expectedTwilioRequest.toString()) + .reply(201, {}) + + const actionInputData = { + event: createMessagingTestEvent({ + timestamp, + event: 'Audience Entered', + userId: 'jane' + }), + settings: { + ...settings, + twilioHostname + }, + mapping: getDefaultMapping() + } + + const responses = await twilio.testAction('sendWhatsApp', actionInputData) + expect(responses.map((response) => response.url)).toStrictEqual([ + `https://${twilioHostname}/2010-04-01/Accounts/a/Messages.json` + ]) + expect(twilioRequest.isDone()).toEqual(true) + }) + + it('should send WhatsApp with custom metadata', async () => { + const expectedTwilioRequest = new URLSearchParams({ + ContentSid: defaultTemplateSid, + From: 'MG1111222233334444', + To: defaultTo, + StatusCallback: + 'http://localhost/?foo=bar&space_id=d&__segment_internal_external_id_key__=phone&__segment_internal_external_id_value__=%2B1234567891#rp=all&rc=5' + }) + const twilioRequest = nock('https://api.twilio.com/2010-04-01/Accounts/a') + .post('/Messages.json', expectedTwilioRequest.toString()) + .reply(201, {}) + + const actionInputData = { + event: createMessagingTestEvent({ + timestamp, + event: 'Audience Entered', + userId: 'jane' + }), + settings: { + ...settings, + webhookUrl: 'http://localhost', + connectionOverrides: 'rp=all&rc=5' + }, + mapping: getDefaultMapping({ customArgs: { foo: 'bar' } }) + } + + const responses = await twilio.testAction('sendWhatsApp', actionInputData) + + expect(responses.map((response) => response.url)).toStrictEqual([ + 'https://api.twilio.com/2010-04-01/Accounts/a/Messages.json' + ]) + expect(twilioRequest.isDone()).toEqual(true) + }) + + it('should fail on invalid webhook url', async () => { + const actionInputData = { + event: createMessagingTestEvent({ + timestamp, + event: 'Audience Entered', + userId: 'jane' + }), + settings: { + ...settings, + webhookUrl: 'foo' + }, + mapping: getDefaultMapping({ customArgs: { foo: 'bar' } }) + } + await expect(twilio.testAction('sendWhatsApp', actionInputData)).rejects.toHaveProperty('code', 'ERR_INVALID_URL') + }) + }) + describe('subscription handling', () => { + it.each(['subscribed', true])('sends an WhatsApp when subscriptonStatus ="%s"', async (subscriptionStatus) => { + const expectedTwilioRequest = new URLSearchParams({ + ContentSid: defaultTemplateSid, + From: 'MG1111222233334444', + To: defaultTo + }) + + const twilioRequest = nock('https://api.twilio.com/2010-04-01/Accounts/a') + .post('/Messages.json', expectedTwilioRequest.toString()) + .reply(201, {}) + + const actionInputData = { + event: createMessagingTestEvent({ + timestamp, + event: 'Audience Entered', + userId: 'jane' + }), + settings, + mapping: getDefaultMapping({ externalIds: [{ type: 'phone', id: '+1234567891', subscriptionStatus }] }) + } + + const responses = await twilio.testAction('sendWhatsApp', actionInputData) + expect(responses.map((response) => response.url)).toStrictEqual([ + 'https://api.twilio.com/2010-04-01/Accounts/a/Messages.json' + ]) + expect(twilioRequest.isDone()).toEqual(true) + }) + + it.each(['unsubscribed', 'did not subscribed', false, null])( + 'does NOT send an WhatsApp when subscriptonStatus ="%s"', + async (subscriptionStatus) => { + const expectedTwilioRequest = new URLSearchParams({ + ContentSid: defaultTemplateSid, + From: 'MG1111222233334444', + To: defaultTo + }) + + const twilioRequest = nock('https://api.twilio.com/2010-04-01/Accounts/a') + .post('/Messages.json', expectedTwilioRequest.toString()) + .reply(201, {}) + + const actionInputData = { + event: createMessagingTestEvent({ + timestamp, + event: 'Audience Entered', + userId: 'jane' + }), + settings, + mapping: getDefaultMapping({ externalIds: [{ type: 'phone', id: '+1234567891', subscriptionStatus }] }) + } + + const responses = await twilio.testAction('sendWhatsApp', actionInputData) + expect(responses).toHaveLength(0) + expect(twilioRequest.isDone()).toEqual(false) + } + ) + + it('throws an error when subscriptionStatus is unrecognizable"', async () => { + const randomSubscriptionStatusPhrase = 'some-subscription-enum' + + const expectedTwilioRequest = new URLSearchParams({ + ContentSid: defaultTemplateSid, + From: 'MG1111222233334444', + To: defaultTo + }) + + nock('https://api.twilio.com/2010-04-01/Accounts/a') + .post('/Messages.json', expectedTwilioRequest.toString()) + .reply(201, {}) + + const actionInputData = { + event: createMessagingTestEvent({ + timestamp, + event: 'Audience Entered', + userId: 'jane' + }), + settings, + mapping: getDefaultMapping({ + externalIds: [{ type: 'phone', id: '+1234567891', subscriptionStatus: randomSubscriptionStatusPhrase }] + }) + } + + const response = twilio.testAction('sendWhatsApp', actionInputData) + await expect(response).rejects.toThrowError( + `Failed to recognize the subscriptionStatus in the payload: "${randomSubscriptionStatusPhrase}".` + ) + }) + + it('formats the to number correctly for whatsapp', async () => { + const from = 'whatsapp:+19876543210' + const expectedTwilioRequest = new URLSearchParams({ + ContentSid: defaultTemplateSid, + From: from, + To: 'whatsapp:+19195551234' + }) + + const twilioRequest = nock('https://api.twilio.com/2010-04-01/Accounts/a') + .post('/Messages.json', expectedTwilioRequest.toString()) + .reply(201, {}) + + const actionInputData = { + event: createMessagingTestEvent({ + timestamp, + event: 'Audience Entered', + userId: 'jane' + }), + settings, + mapping: getDefaultMapping({ + from: from, + contentSid: defaultTemplateSid, + externalIds: [{ type: 'phone', id: '(919) 555 1234', subscriptionStatus: true }] + }) + } + + const responses = await twilio.testAction('sendWhatsApp', actionInputData) + expect(responses.map((response) => response.url)).toStrictEqual([ + 'https://api.twilio.com/2010-04-01/Accounts/a/Messages.json' + ]) + expect(twilioRequest.isDone()).toEqual(true) + }) + + it('throws an error when whatsapp number cannot be formatted', async () => { + const actionInputData = { + event: createMessagingTestEvent({ + timestamp, + event: 'Audience Entered', + userId: 'jane' + }), + settings, + mapping: getDefaultMapping({ + externalIds: [{ type: 'phone', id: 'abcd', subscriptionStatus: true }] + }) + } + + const response = twilio.testAction('sendWhatsApp', actionInputData) + await expect(response).rejects.toThrowError( + 'The string supplied did not seem to be a phone number. Phone number must be able to be formatted to e164 for whatsapp.' + ) + }) + + it('formats and sends content variables', async () => { + const expectedTwilioRequest = new URLSearchParams({ + ContentSid: defaultTemplateSid, + From: 'MG1111222233334444', + To: defaultTo, + ContentVariables: JSON.stringify({ '1': 'Soap', '2': '360 Scope St' }) + }) + + const twilioRequest = nock('https://api.twilio.com/2010-04-01/Accounts/a') + .post('/Messages.json', expectedTwilioRequest.toString()) + .reply(201, {}) + + const actionInputData = { + event: createMessagingTestEvent({ + timestamp, + event: 'Audience Entered', + userId: 'jane' + }), + settings, + mapping: getDefaultMapping({ + contentVariables: { '1': '{{profile.traits.firstName}}', '2': '{{profile.traits.address.street}}' }, + traits: { + firstName: 'Soap', + address: { + street: '360 Scope St' + } + } + }) + } + + const responses = await twilio.testAction('sendWhatsApp', actionInputData) + expect(responses.map((response) => response.url)).toStrictEqual([ + 'https://api.twilio.com/2010-04-01/Accounts/a/Messages.json' + ]) + expect(twilioRequest.isDone()).toEqual(true) + }) + + it('sends content variables as is when traits are not enriched', async () => { + const expectedTwilioRequest = new URLSearchParams({ + ContentSid: defaultTemplateSid, + From: 'MG1111222233334444', + To: defaultTo, + ContentVariables: JSON.stringify({ '1': 'Soap', '2': '360 Scope St' }) + }) + + const twilioRequest = nock('https://api.twilio.com/2010-04-01/Accounts/a') + .post('/Messages.json', expectedTwilioRequest.toString()) + .reply(201, {}) + + const actionInputData = { + event: createMessagingTestEvent({ + timestamp, + event: 'Audience Entered', + userId: 'jane' + }), + settings, + mapping: getDefaultMapping({ + contentVariables: { '1': 'Soap', '2': '360 Scope St' }, + traitEnrichment: false + }) + } + + const responses = await twilio.testAction('sendWhatsApp', actionInputData) + expect(responses.map((response) => response.url)).toStrictEqual([ + 'https://api.twilio.com/2010-04-01/Accounts/a/Messages.json' + ]) + expect(twilioRequest.isDone()).toEqual(true) + }) + }) +}) diff --git a/packages/destination-actions/src/destinations/engage-messaging-twilio/index.ts b/packages/destination-actions/src/destinations/engage-messaging-twilio/index.ts index e8d591d3b5..8e4b90347c 100644 --- a/packages/destination-actions/src/destinations/engage-messaging-twilio/index.ts +++ b/packages/destination-actions/src/destinations/engage-messaging-twilio/index.ts @@ -1,6 +1,7 @@ import type { DestinationDefinition, InputField } from '@segment/actions-core' import type { Settings } from './generated-types' import sendSms from './sendSms' +import sendWhatsApp from './sendWhatsApp' const getRange = (val: number): { value: number; label: string }[] => { return Array(val) @@ -167,7 +168,8 @@ const destination: DestinationDefinition = { // } // }, actions: { - sendSms + sendSms, + sendWhatsApp } } diff --git a/packages/destination-actions/src/destinations/engage-messaging-twilio/sendSms/index.ts b/packages/destination-actions/src/destinations/engage-messaging-twilio/sendSms/index.ts index 6cfbbe8cc7..785fffaafb 100644 --- a/packages/destination-actions/src/destinations/engage-messaging-twilio/sendSms/index.ts +++ b/packages/destination-actions/src/destinations/engage-messaging-twilio/sendSms/index.ts @@ -1,50 +1,9 @@ -import { Liquid as LiquidJs } from 'liquidjs' - -import type { ActionDefinition, RequestOptions } from '@segment/actions-core' +/* eslint-disable @typescript-eslint/no-unsafe-call */ +import type { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { IntegrationError } from '@segment/actions-core' -import { StatsClient } from '@segment/actions-core/src/destination-kit' -const Liquid = new LiquidJs() - -const getProfileApiEndpoint = (environment: string): string => { - return `https://profiles.segment.${environment === 'production' ? 'com' : 'build'}` -} - -type RequestFn = (url: string, options?: RequestOptions) => Promise +import { SmsMessageSender } from './sms-sender' -const fetchProfileTraits = async ( - request: RequestFn, - settings: Settings, - profileId: string, - statsClient?: StatsClient | undefined, - tags?: string[] | undefined -): Promise> => { - try { - const endpoint = getProfileApiEndpoint(settings.profileApiEnvironment) - const response = await request( - `${endpoint}/v1/spaces/${settings.spaceId}/collections/users/profiles/user_id:${profileId}/traits?limit=200`, - { - headers: { - authorization: `Basic ${Buffer.from(settings.profileApiAccessToken + ':').toString('base64')}`, - 'content-type': 'application/json' - } - } - ) - tags?.push(`profile_status_code:${response.status}`) - statsClient?.incr('actions-personas-messaging-twilio.profile_invoked', 1, tags) - const body = await response.json() - return body.traits - } catch (error: unknown) { - statsClient?.incr('actions-personas-messaging-twilio.profile_error', 1, tags) - throw new IntegrationError('Unable to get profile traits for SMS message', 'SMS trait fetch failure', 500) - } -} - -const EXTERNAL_ID_KEY = 'phone' -const DEFAULT_HOSTNAME = 'api.twilio.com' - -const DEFAULT_CONNECTION_OVERRIDES = 'rp=all&rc=5' const action: ActionDefinition = { title: 'Send SMS', description: 'Send SMS using Twilio', @@ -161,106 +120,8 @@ const action: ActionDefinition = { const statsClient = statsContext?.statsClient const tags = statsContext?.tags tags?.push(`space_id:${settings.spaceId}`, `projectid:${settings.sourceId}`) - if (!payload.send) { - statsClient?.incr('actions-personas-messaging-twilio.send-disabled', 1, tags) - return - } - const externalId = payload.externalIds?.find(({ type }) => type === 'phone') - if ( - !externalId?.subscriptionStatus || - ['unsubscribed', 'did not subscribed', 'false'].includes(externalId.subscriptionStatus) - ) { - statsClient?.incr('actions-personas-messaging-twilio.notsubscribed', 1, tags) - return - } else if (['subscribed', 'true'].includes(externalId.subscriptionStatus)) { - statsClient?.incr('actions-personas-messaging-twilio.subscribed', 1, tags) - const phone = payload.toNumber || externalId.id - if (!phone) { - return - } - - let traits - if (payload.traitEnrichment) { - traits = payload?.traits ? payload?.traits : JSON.parse('{}') - } else { - traits = await fetchProfileTraits(request, settings, payload.userId, statsClient, tags) - } - - const profile = { - user_id: payload.userId, - phone, - traits - } - - // TODO: GROW-259 remove this when we can extend the request - // and we no longer need to call the profiles API first - const token = Buffer.from(`${settings.twilioApiKeySID}:${settings.twilioApiKeySecret}`).toString('base64') - let parsedBody - - try { - parsedBody = await Liquid.parseAndRender(payload.body, { profile }) - } catch (error: unknown) { - throw new IntegrationError(`Unable to parse templating in SMS`, `SMS templating parse failure`, 400) - } - const body = new URLSearchParams({ - Body: parsedBody, - From: payload.from, - To: phone - }) - - const webhookUrl = settings.webhookUrl - const connectionOverrides = settings.connectionOverrides - const customArgs: Record = { - ...payload.customArgs, - space_id: settings.spaceId, - __segment_internal_external_id_key__: EXTERNAL_ID_KEY, - __segment_internal_external_id_value__: phone - } - - if (webhookUrl && customArgs) { - // Webhook URL parsing has a potential of failing. I think it's better that - // we fail out of any invocation than silently not getting analytics - // data if that's what we're expecting. - const webhookUrlWithParams = new URL(webhookUrl) - for (const key of Object.keys(customArgs)) { - webhookUrlWithParams.searchParams.append(key, String(customArgs[key])) - } - - webhookUrlWithParams.hash = connectionOverrides || DEFAULT_CONNECTION_OVERRIDES - - body.append('StatusCallback', webhookUrlWithParams.toString()) - } - - const hostname = settings.twilioHostname ?? DEFAULT_HOSTNAME - const response = await request( - `https://${hostname}/2010-04-01/Accounts/${settings.twilioAccountSID}/Messages.json`, - { - method: 'POST', - headers: { - authorization: `Basic ${token}` - }, - body - } - ) - tags?.push(`twilio_status_code:${response.status}`) - statsClient?.incr('actions-personas-messaging-twilio.response', 1, tags) - if (payload?.eventOccurredTS != undefined) { - statsClient?.histogram( - 'actions-personas-messaging-twilio.eventDeliveryTS', - Date.now() - new Date(payload?.eventOccurredTS).getTime(), - tags - ) - } - return response - } else { - statsClient?.incr('actions-personas-messaging-twilio.twilio-error', 1, tags) - throw new IntegrationError( - `Failed to recognize the subscriptionStatus in the payload: "${externalId.subscriptionStatus}".`, - 'Invalid subscriptionStatus value', - 400 - ) - } + return new SmsMessageSender(request, payload, settings, statsClient, tags).send() } } diff --git a/packages/destination-actions/src/destinations/engage-messaging-twilio/sendSms/sms-sender.ts b/packages/destination-actions/src/destinations/engage-messaging-twilio/sendSms/sms-sender.ts new file mode 100644 index 0000000000..9e8633041a --- /dev/null +++ b/packages/destination-actions/src/destinations/engage-messaging-twilio/sendSms/sms-sender.ts @@ -0,0 +1,80 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +import { Liquid as LiquidJs } from 'liquidjs' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' +import { IntegrationError } from '@segment/actions-core' +import { StatsClient, StatsContext } from '@segment/actions-core/src/destination-kit' +import { MessageSender, RequestFn } from '../utils/message-sender' + +const Liquid = new LiquidJs() + +export class SmsMessageSender extends MessageSender { + constructor( + readonly request: RequestFn, + readonly payload: Payload, + readonly settings: Settings, + readonly statsClient: StatsClient | undefined, + readonly tags: StatsContext['tags'] | undefined + ) { + super(request, payload, settings, statsClient, tags) + } + + getBody = async (phone: string) => { + // TODO: GROW-259 remove this when we can extend the request + // and we no longer need to call the profiles API first + let traits + if (this.payload.traitEnrichment) { + traits = this.payload?.traits ? this.payload?.traits : {} + } else { + traits = await this.getProfileTraits() + } + + const profile = { + user_id: this.payload.userId, + phone, + traits + } + + let parsedBody + + try { + parsedBody = await Liquid.parseAndRender(this.payload.body, { profile }) + } catch (error: unknown) { + throw new IntegrationError(`Unable to parse templating in SMS`, `SMS templating parse failure`, 400) + } + + const body = new URLSearchParams({ + Body: parsedBody, + From: this.payload.from, + To: phone + }) + + return body + } + + private getProfileTraits = async () => { + try { + const endpoint = `https://profiles.segment.${ + this.settings.profileApiEnvironment === 'production' ? 'com' : 'build' + }` + const response = await this.request( + `${endpoint}/v1/spaces/${this.settings.spaceId}/collections/users/profiles/user_id:${encodeURIComponent( + this.payload.userId + )}/traits?limit=200`, + { + headers: { + authorization: `Basic ${Buffer.from(this.settings.profileApiAccessToken + ':').toString('base64')}`, + 'content-type': 'application/json' + } + } + ) + this.tags?.push(`profile_status_code:${response.status}`) + this.statsClient?.incr('actions-personas-messaging-twilio.profile_invoked', 1, this.tags) + const body = await response.json() + return body.traits + } catch (error: unknown) { + this.statsClient?.incr('actions-personas-messaging-twilio.profile_error', 1, this.tags) + throw new IntegrationError('Unable to get profile traits for SMS message', 'SMS trait fetch failure', 500) + } + } +} diff --git a/packages/destination-actions/src/destinations/engage-messaging-twilio/sendWhatsApp/generated-types.ts b/packages/destination-actions/src/destinations/engage-messaging-twilio/sendWhatsApp/generated-types.ts new file mode 100644 index 0000000000..cb2ac31fe3 --- /dev/null +++ b/packages/destination-actions/src/destinations/engage-messaging-twilio/sendWhatsApp/generated-types.ts @@ -0,0 +1,67 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * The template you sending through WhatsApp + */ + contentSid: string + /** + * Content personalization variables/merge tags for your WhatsApp message + */ + contentVariables?: { + [k: string]: unknown + } + /** + * Number to send WhatsApp to when testing + */ + toNumber?: string + /** + * The Twilio Phone Number, Short Code, or Messaging Service to send WhatsApp from. + */ + from: string + /** + * Additional custom arguments that will be opaquely sent back on webhook events + */ + customArgs?: { + [k: string]: unknown + } + /** + * Connection overrides are configuration supported by twilio webhook services. Must be passed as fragments on the callback url + */ + connectionOverrides?: string + /** + * Whether or not the message should actually get sent. + */ + send?: boolean + /** + * Whether or not trait enrich from event (i.e without profile api call) + */ + traitEnrichment?: boolean + /** + * An array of user profile identity information. + */ + externalIds?: { + /** + * A unique identifier for the collection. + */ + id?: string + /** + * The external ID contact type. + */ + type?: string + /** + * The subscription status for the identity. + */ + subscriptionStatus?: string + }[] + /** + * A user profile's traits + */ + traits?: { + [k: string]: unknown + } + /** + * Time of when the actual event happened. + */ + eventOccurredTS?: string +} diff --git a/packages/destination-actions/src/destinations/engage-messaging-twilio/sendWhatsApp/index.ts b/packages/destination-actions/src/destinations/engage-messaging-twilio/sendWhatsApp/index.ts new file mode 100644 index 0000000000..4bad43d208 --- /dev/null +++ b/packages/destination-actions/src/destinations/engage-messaging-twilio/sendWhatsApp/index.ts @@ -0,0 +1,127 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +import type { ActionDefinition } from '@segment/actions-core' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' +import { WhatsAppMessageSender } from './whatsapp-sender' + +const action: ActionDefinition = { + title: 'Send WhatsApp', + description: 'Send WhatsApp using Twilio', + defaultSubscription: 'type = "track" and event = "Audience Entered"', + fields: { + contentSid: { + label: 'WhatsApp template Content Sid', + description: 'The template you sending through WhatsApp', + type: 'string', + required: true + }, + contentVariables: { + label: 'WhatsApp template variables', + description: 'Content personalization variables/merge tags for your WhatsApp message', + type: 'object', + required: false + }, + toNumber: { + label: 'Test Number', + description: 'Number to send WhatsApp to when testing', + type: 'string' + }, + from: { + label: 'From', + description: 'The Twilio Phone Number, Short Code, or Messaging Service to send WhatsApp from.', + type: 'string', + required: true + }, + customArgs: { + label: 'Custom Arguments', + description: 'Additional custom arguments that will be opaquely sent back on webhook events', + type: 'object', + required: false + }, + connectionOverrides: { + label: 'Connection Overrides', + description: + 'Connection overrides are configuration supported by twilio webhook services. Must be passed as fragments on the callback url', + type: 'string', + required: false + }, + send: { + label: 'Send Message', + description: 'Whether or not the message should actually get sent.', + type: 'boolean', + required: false, + default: false + }, + traitEnrichment: { + label: 'Trait Enrich', + description: 'Whether or not trait enrich from event (i.e without profile api call)', + type: 'boolean', + required: false, + default: true + }, + externalIds: { + label: 'External IDs', + description: 'An array of user profile identity information.', + type: 'object', + multiple: true, + properties: { + id: { + label: 'ID', + description: 'A unique identifier for the collection.', + type: 'string' + }, + type: { + label: 'type', + description: 'The external ID contact type.', + type: 'string' + }, + subscriptionStatus: { + label: 'ID', + description: 'The subscription status for the identity.', + type: 'string' + } + }, + default: { + '@arrayPath': [ + '$.external_ids', + { + id: { + '@path': '$.id' + }, + type: { + '@path': '$.type' + }, + subscriptionStatus: { + '@path': '$.isSubscribed' + } + } + ] + } + }, + traits: { + label: 'Traits', + description: "A user profile's traits", + type: 'object', + required: false, + default: { '@path': '$.properties' } + }, + eventOccurredTS: { + label: 'Event Timestamp', + description: 'Time of when the actual event happened.', + type: 'string', + required: false, + default: { + '@path': '$.timestamp' + } + } + }, + perform: async (request, { settings, payload, statsContext }) => { + const statsClient = statsContext?.statsClient + const tags = statsContext?.tags + tags?.push(`space_id:${settings.spaceId}`, `projectid:${settings.sourceId}`) + + return new WhatsAppMessageSender(request, payload, settings, statsClient, tags).send() + } +} + +export default action diff --git a/packages/destination-actions/src/destinations/engage-messaging-twilio/sendWhatsApp/whatsapp-sender.ts b/packages/destination-actions/src/destinations/engage-messaging-twilio/sendWhatsApp/whatsapp-sender.ts new file mode 100644 index 0000000000..90464d663c --- /dev/null +++ b/packages/destination-actions/src/destinations/engage-messaging-twilio/sendWhatsApp/whatsapp-sender.ts @@ -0,0 +1,89 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +import { Liquid as LiquidJs } from 'liquidjs' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' +import { IntegrationError } from '@segment/actions-core' +import { RequestFn, MessageSender } from '../utils/message-sender' +import { PhoneNumberUtil, PhoneNumberFormat } from 'google-libphonenumber' +import { StatsClient, StatsContext } from '@segment/actions-core/src/destination-kit' + +const phoneUtil = PhoneNumberUtil.getInstance() +const Liquid = new LiquidJs() + +export class WhatsAppMessageSender extends MessageSender { + constructor( + readonly request: RequestFn, + readonly payload: Payload, + readonly settings: Settings, + readonly statsClient: StatsClient | undefined, + readonly tags: StatsContext['tags'] | undefined + ) { + super(request, payload, settings, statsClient, tags) + } + + getBody = async (phone: string) => { + let parsedPhone + + try { + // Defaulting to US for now as that's where most users will seemingly be. Though + // any number already given in e164 format should parse correctly even with the + // default region being US. + parsedPhone = phoneUtil.parse(phone, 'US') + parsedPhone = phoneUtil.format(parsedPhone, PhoneNumberFormat.E164) + parsedPhone = `whatsapp:${parsedPhone}` + } catch (error: unknown) { + this.tags?.push('type:invalid_phone_e164') + this.statsClient?.incr('actions-personas-messaging-twilio.error', 1, this.tags) + throw new IntegrationError( + 'The string supplied did not seem to be a phone number. Phone number must be able to be formatted to e164 for whatsapp.', + `INVALID_PHONE`, + 400 + ) + } + + if (!this.payload.contentSid) { + throw new IntegrationError('A valid whatsApp Content SID was not provided.', `INVALID_CONTENT_SID`, 400) + } + + const params: Record = { + ContentSid: this.payload.contentSid, + From: this.payload.from, + To: parsedPhone + } + const contentVariables = await this.getVariables() + + if (contentVariables) params['ContentVariables'] = contentVariables + + return new URLSearchParams(params) + } + + private getVariables = async (): Promise => { + try { + // contentVariables can be rendered to their respective values in the upstream actor + // before they send it to this action. + // to signify this behavior, traitsEnrichment will be passed as false by the upstream actor + if (!this.payload.traitEnrichment && this.payload.contentVariables) + return JSON.stringify(this.payload.contentVariables) ?? null + if (!this.payload.contentVariables || !this.payload.traits) return null + + const profile = { + profile: { + traits: this.payload.traits + } + } + + const mapping: Record = {} + for (const [key, val] of Object.entries(this.payload.contentVariables)) { + mapping[key] = await Liquid.parseAndRender(val as string, profile) + } + + return JSON.stringify(mapping) + } catch (error: unknown) { + throw new IntegrationError( + `Unable to parse templating in content variables`, + `Content variables templating parse failure`, + 400 + ) + } + } +} diff --git a/packages/destination-actions/src/destinations/engage-messaging-twilio/utils/message-sender.ts b/packages/destination-actions/src/destinations/engage-messaging-twilio/utils/message-sender.ts new file mode 100644 index 0000000000..d930aa4fc4 --- /dev/null +++ b/packages/destination-actions/src/destinations/engage-messaging-twilio/utils/message-sender.ts @@ -0,0 +1,141 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +import type { RequestOptions } from '@segment/actions-core' +import type { Settings } from '../generated-types' +import type { Payload } from '../sendSms/generated-types' +import { IntegrationError } from '@segment/actions-core' +import { StatsClient, StatsContext } from '@segment/actions-core/src/destination-kit' + +enum SendabilityStatus { + NoSenderPhone = 'no_sender_phone', + ShouldSend = 'should_send', + DoNotSend = 'do_not_send', + SendDisabled = 'send_disabled', + InvalidSubscriptionStatus = 'invalid_subscription_status' +} + +type SendabilityPayload = { sendabilityStatus: SendabilityStatus; phone: string | undefined } + +export type RequestFn = (url: string, options?: RequestOptions) => Promise + +type MinimalPayload = Pick< + Payload, + 'from' | 'toNumber' | 'customArgs' | 'externalIds' | 'traits' | 'send' | 'eventOccurredTS' +> + +export abstract class MessageSender { + private readonly EXTERNAL_ID_KEY = 'phone' + private readonly DEFAULT_HOSTNAME = 'api.twilio.com' + private readonly DEFAULT_CONNECTION_OVERRIDES = 'rp=all&rc=5' + + constructor( + readonly request: RequestFn, + readonly payload: SmsPayload, + readonly settings: Settings, + readonly statsClient: StatsClient | undefined, + readonly tags: StatsContext['tags'] | undefined + ) {} + + abstract getBody: (phone: string) => Promise + + send = async () => { + const { phone, sendabilityStatus } = this.getSendabilityPayload() + + if (sendabilityStatus !== SendabilityStatus.ShouldSend || !phone) { + return + } + + const body = await this.getBody(phone) + + const webhookUrlWithParams = this.getWebhookUrlWithParams(phone) + + if (webhookUrlWithParams) body.append('StatusCallback', webhookUrlWithParams) + + const twilioHostname = this.settings.twilioHostname ?? this.DEFAULT_HOSTNAME + const twilioToken = Buffer.from(`${this.settings.twilioApiKeySID}:${this.settings.twilioApiKeySecret}`).toString( + 'base64' + ) + const response = await this.request( + `https://${twilioHostname}/2010-04-01/Accounts/${this.settings.twilioAccountSID}/Messages.json`, + { + method: 'POST', + headers: { + authorization: `Basic ${twilioToken}` + }, + body + } + ) + this.tags?.push(`twilio_status_code:${response.status}`) + this.statsClient?.incr('actions-personas-messaging-twilio.response', 1, this.tags) + + if (this.payload.eventOccurredTS != undefined) { + this.statsClient?.histogram( + 'actions-personas-messaging-twilio.eventDeliveryTS', + Date.now() - new Date(this.payload.eventOccurredTS).getTime(), + this.tags + ) + } + return response + } + + private getSendabilityPayload = (): SendabilityPayload => { + const nonSendableStatuses = ['unsubscribed', 'did not subscribed', 'false'] + const sendableStatuses = ['subscribed', 'true'] + const externalId = this.payload.externalIds?.find(({ type }) => type === 'phone') + + let status: SendabilityStatus + + if (!this.payload.send) { + this.statsClient?.incr('actions-personas-messaging-twilio.send-disabled', 1, this.tags) + return { sendabilityStatus: SendabilityStatus.SendDisabled, phone: undefined } + } + + if (!externalId?.subscriptionStatus || nonSendableStatuses.includes(externalId.subscriptionStatus)) { + this.statsClient?.incr('actions-personas-messaging-twilio.notsubscribed', 1, this.tags) + status = SendabilityStatus.DoNotSend + } else if (sendableStatuses.includes(externalId.subscriptionStatus)) { + this.statsClient?.incr('actions-personas-messaging-twilio.subscribed', 1, this.tags) + status = SendabilityStatus.ShouldSend + } else { + this.statsClient?.incr('actions-personas-messaging-twilio.twilio-error', 1, this.tags) + throw new IntegrationError( + `Failed to recognize the subscriptionStatus in the payload: "${externalId.subscriptionStatus}".`, + 'Invalid subscriptionStatus value', + 400 + ) + } + + const phone = this.payload.toNumber || externalId?.id + if (!phone) { + status = SendabilityStatus.NoSenderPhone + } + + return { sendabilityStatus: status, phone } + } + + private getWebhookUrlWithParams = (phone: string): string | null => { + const webhookUrl = this.settings.webhookUrl + const connectionOverrides = this.settings.connectionOverrides + const customArgs: Record = { + ...this.payload.customArgs, + space_id: this.settings.spaceId, + __segment_internal_external_id_key__: this.EXTERNAL_ID_KEY, + __segment_internal_external_id_value__: phone + } + + if (webhookUrl && customArgs) { + // Webhook URL parsing has a potential of failing. I think it's better that + // we fail out of any invocation than silently not getting analytics + // data if that's what we're expecting. + const webhookUrlWithParams = new URL(webhookUrl) + for (const key of Object.keys(customArgs)) { + webhookUrlWithParams.searchParams.append(key, String(customArgs[key])) + } + + webhookUrlWithParams.hash = connectionOverrides || this.DEFAULT_CONNECTION_OVERRIDES + + return webhookUrlWithParams.toString() + } + + return null + } +} diff --git a/packages/destination-actions/src/destinations/google-enhanced-conversions/generated-types.ts b/packages/destination-actions/src/destinations/google-enhanced-conversions/generated-types.ts index e90652be3a..71572a6401 100644 --- a/packages/destination-actions/src/destinations/google-enhanced-conversions/generated-types.ts +++ b/packages/destination-actions/src/destinations/google-enhanced-conversions/generated-types.ts @@ -2,7 +2,7 @@ export interface Settings { /** - * You will find this information in the event snippet for your conversion action, for example `send_to: AW-CONVERSION_ID/AW-CONVERSION_LABEL`. In the sample snippet, AW-CONVERSION_ID stands for the conversion ID unique to your account. Enter the conversion Id, without the AW- prefix. **Required if you are using a mapping that sends data to the legacy Google Enhanced Conversions API.** + * You will find this information in the event snippet for your conversion action, for example `send_to: AW-CONVERSION_ID/AW-CONVERSION_LABEL`. In the sample snippet, AW-CONVERSION_ID stands for the conversion ID unique to your account. Enter the conversion ID, without the AW- prefix. **Required if you are using a mapping that sends data to the legacy Google Enhanced Conversions API (i.e. Upload Enhanced Conversion (Legacy) Action).** */ conversionTrackingId?: string /** diff --git a/packages/destination-actions/src/destinations/google-enhanced-conversions/index.ts b/packages/destination-actions/src/destinations/google-enhanced-conversions/index.ts index c0bf2b5f27..f1e5571d13 100644 --- a/packages/destination-actions/src/destinations/google-enhanced-conversions/index.ts +++ b/packages/destination-actions/src/destinations/google-enhanced-conversions/index.ts @@ -30,7 +30,7 @@ const destination: DestinationDefinition = { conversionTrackingId: { label: 'Conversion ID', description: - 'You will find this information in the event snippet for your conversion action, for example `send_to: AW-CONVERSION_ID/AW-CONVERSION_LABEL`. In the sample snippet, AW-CONVERSION_ID stands for the conversion ID unique to your account. Enter the conversion Id, without the AW- prefix. **Required if you are using a mapping that sends data to the legacy Google Enhanced Conversions API.**', + 'You will find this information in the event snippet for your conversion action, for example `send_to: AW-CONVERSION_ID/AW-CONVERSION_LABEL`. In the sample snippet, AW-CONVERSION_ID stands for the conversion ID unique to your account. Enter the conversion ID, without the AW- prefix. **Required if you are using a mapping that sends data to the legacy Google Enhanced Conversions API (i.e. Upload Enhanced Conversion (Legacy) Action).**', type: 'string' }, customerId: { diff --git a/packages/destination-actions/src/destinations/heap/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/heap/__tests__/__snapshots__/snapshot.test.ts.snap index d94e6cc065..99315202ab 100644 --- a/packages/destination-actions/src/destinations/heap/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/heap/__tests__/__snapshots__/snapshot.test.ts.snap @@ -19,13 +19,14 @@ Object { exports[`Testing snapshot for actions-heap destination: trackEvent action - all fields 1`] = ` Object { "app_id": "kFJwrNh$DP38FB", - "event": "kFJwrNh$DP38FB", + "event": "track", "idempotency_key": "kFJwrNh$DP38FB", "identity": "kFJwrNh$DP38FB", "properties": Object { - "segment_library": "kFJwrNh$DP38FB", + "segment_library": "cloud-mode-destination", "testType": "kFJwrNh$DP38FB", }, + "session_id": "kFJwrNh$DP38FB", "timestamp": "2021-02-01T00:00:00.000Z", } `; @@ -33,11 +34,12 @@ Object { exports[`Testing snapshot for actions-heap destination: trackEvent action - required fields 1`] = ` Object { "app_id": "kFJwrNh$DP38FB", - "event": "kFJwrNh$DP38FB", + "event": "track", "idempotency_key": "kFJwrNh$DP38FB", "identity": "kFJwrNh$DP38FB", "properties": Object { - "segment_library": "destinations-actions", + "segment_library": "cloud-mode-destination", }, + "session_id": "kFJwrNh$DP38FB", } `; diff --git a/packages/destination-actions/src/destinations/heap/constants.ts b/packages/destination-actions/src/destinations/heap/constants.ts index cbfd822ad5..411c4aaf1e 100644 --- a/packages/destination-actions/src/destinations/heap/constants.ts +++ b/packages/destination-actions/src/destinations/heap/constants.ts @@ -1 +1 @@ -export const HEAP_SEGMENT_LIBRARY_NAME = 'destinations-actions' +export const HEAP_SEGMENT_CLOUD_LIBRARY_NAME = 'cloud-mode-destination' diff --git a/packages/destination-actions/src/destinations/heap/index.ts b/packages/destination-actions/src/destinations/heap/index.ts index 15e9dde925..1ece2f8e57 100644 --- a/packages/destination-actions/src/destinations/heap/index.ts +++ b/packages/destination-actions/src/destinations/heap/index.ts @@ -8,7 +8,7 @@ import identifyUser from './identifyUser' const presets: DestinationDefinition['presets'] = [ { name: 'Track Calls', - subscribe: 'type = "track"', + subscribe: 'type = "track" or type = "page" or type = "screen"', partnerAction: 'trackEvent', mapping: defaultValues(trackEvent.fields) }, diff --git a/packages/destination-actions/src/destinations/heap/trackEvent/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/heap/trackEvent/__tests__/__snapshots__/snapshot.test.ts.snap index f07f0a71af..6dfded4e61 100644 --- a/packages/destination-actions/src/destinations/heap/trackEvent/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/heap/trackEvent/__tests__/__snapshots__/snapshot.test.ts.snap @@ -3,13 +3,14 @@ exports[`Testing snapshot for Heap's trackEvent destination action: all fields 1`] = ` Object { "app_id": "xqYHVWXiU0In", - "event": "xqYHVWXiU0In", + "event": "track", "idempotency_key": "xqYHVWXiU0In", "identity": "xqYHVWXiU0In", "properties": Object { - "segment_library": "xqYHVWXiU0In", + "segment_library": "cloud-mode-destination", "testType": "xqYHVWXiU0In", }, + "session_id": "xqYHVWXiU0In", "timestamp": "2021-02-01T00:00:00.000Z", } `; @@ -17,11 +18,12 @@ Object { exports[`Testing snapshot for Heap's trackEvent destination action: required fields 1`] = ` Object { "app_id": "xqYHVWXiU0In", - "event": "xqYHVWXiU0In", + "event": "track", "idempotency_key": "xqYHVWXiU0In", "identity": "xqYHVWXiU0In", "properties": Object { - "segment_library": "destinations-actions", + "segment_library": "cloud-mode-destination", }, + "session_id": "xqYHVWXiU0In", } `; diff --git a/packages/destination-actions/src/destinations/heap/trackEvent/__tests__/index.test.ts b/packages/destination-actions/src/destinations/heap/trackEvent/__tests__/index.test.ts index 70d065bee9..e42bdac598 100644 --- a/packages/destination-actions/src/destinations/heap/trackEvent/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/heap/trackEvent/__tests__/index.test.ts @@ -2,6 +2,7 @@ import nock from 'nock' import { createTestEvent, createTestIntegration, JSONValue, SegmentEvent } from '@segment/actions-core' import Destination from '../../index' import { flattenObject, embededObject } from '../../__tests__/flat.test' +import { HEAP_SEGMENT_CLOUD_LIBRARY_NAME } from '../../constants' describe('Heap.trackEvent', () => { const testDestination = createTestIntegration(Destination) @@ -27,7 +28,7 @@ describe('Heap.trackEvent', () => { event: eventName, idempotency_key: messageId, properties: { - segment_library: 'analytics.js' + segment_library: HEAP_SEGMENT_CLOUD_LIBRARY_NAME }, timestamp } @@ -107,7 +108,7 @@ describe('Heap.trackEvent', () => { body.user_id = 8325872782136936 body.properties = { - segment_library: 'analytics.js', + segment_library: HEAP_SEGMENT_CLOUD_LIBRARY_NAME, ...flattenObject() } nock('https://heapanalytics.com').post('/api/track', body).reply(200, {}) @@ -124,4 +125,29 @@ describe('Heap.trackEvent', () => { expect(responses[0].status).toBe(200) expect(responses[0].data).toMatchObject({}) }) + + it('should get event field for different event type', async () => { + const event: Partial = createTestEvent({ + timestamp, + event: undefined, + userId, + messageId, + name: 'Home Page', + type: 'page' + }) + body.identity = userId + body.event = 'Home Page' + nock('https://heapanalytics.com').post('/api/track', body).reply(200, body) + + const responses = await testDestination.testAction('trackEvent', { + event, + useDefaultMappings: true, + settings: { + appId: HEAP_TEST_APP_ID + } + }) + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) + expect(responses[0].data).toEqual(expect.objectContaining({ event: 'Home Page' })) + }) }) diff --git a/packages/destination-actions/src/destinations/heap/trackEvent/generated-types.ts b/packages/destination-actions/src/destinations/heap/trackEvent/generated-types.ts index d79f03f412..ac84e4fc93 100644 --- a/packages/destination-actions/src/destinations/heap/trackEvent/generated-types.ts +++ b/packages/destination-actions/src/destinations/heap/trackEvent/generated-types.ts @@ -14,9 +14,9 @@ export interface Payload { */ anonymous_id?: string | null /** - * The name of the event. Limited to 1024 characters. + * Name of the user action. This only exists on track events. Limited to 1024 characters. */ - event: string + event?: string /** * An object with key-value properties you want associated with the event. Each key and property must either be a number or string with fewer than 1024 characters. */ @@ -28,7 +28,15 @@ export interface Payload { */ timestamp?: string | number /** - * The name of the SDK used to send events + * A Heap session ID. The session ID can be retrived by calling getSessionId() on the heap api. If a session ID is not provided one will be created. */ - library_name?: string + session_id?: string + /** + * The type of call. Can be track, page, or screen. + */ + type?: string + /** + * The name of the page or screen being viewed. This only exists for page and screen events. + */ + name?: string } diff --git a/packages/destination-actions/src/destinations/heap/trackEvent/index.ts b/packages/destination-actions/src/destinations/heap/trackEvent/index.ts index d23983eb20..10f8144d6a 100644 --- a/packages/destination-actions/src/destinations/heap/trackEvent/index.ts +++ b/packages/destination-actions/src/destinations/heap/trackEvent/index.ts @@ -2,7 +2,7 @@ import type { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' import dayjs from '../../../lib/dayjs' -import { HEAP_SEGMENT_LIBRARY_NAME } from '../constants' +import { HEAP_SEGMENT_CLOUD_LIBRARY_NAME } from '../constants' import { getHeapUserId } from '../userIdHash' import { flat } from '../flat' import { IntegrationError } from '@segment/actions-core' @@ -11,18 +11,19 @@ type HeapEvent = { app_id: string identity?: string user_id?: number - event: string + event: string | undefined properties: { [k: string]: unknown } idempotency_key: string timestamp?: string + session_id?: string } const action: ActionDefinition = { title: 'Track Event', description: 'Send an event to Heap.', - defaultSubscription: 'type = "track"', + defaultSubscription: 'type = "track" or type = "page" or type = "screen"', fields: { message_id: { label: 'Message ID', @@ -53,10 +54,9 @@ const action: ActionDefinition = { } }, event: { - label: 'Event Type', + label: 'Track Event Type', type: 'string', - description: 'The name of the event. Limited to 1024 characters.', - required: true, + description: 'Name of the user action. This only exists on track events. Limited to 1024 characters.', default: { '@path': '$.event' } @@ -78,12 +78,29 @@ const action: ActionDefinition = { '@path': '$.timestamp' } }, - library_name: { - label: 'Library Name', + session_id: { + label: 'Session ID', + type: 'string', + description: + 'A Heap session ID. The session ID can be retrived by calling getSessionId() on the heap api. If a session ID is not provided one will be created.', + default: { + '@path': '$.session_id' + } + }, + type: { + label: 'Type', + type: 'string', + description: 'The type of call. Can be track, page, or screen.', + default: { + '@path': '$.type' + } + }, + name: { + label: 'Page or Screen Name', type: 'string', - description: 'The name of the SDK used to send events', + description: 'The name of the page or screen being viewed. This only exists for page and screen events.', default: { - '@path': '$.context.library.name' + '@path': '$.name' } } }, @@ -96,33 +113,61 @@ const action: ActionDefinition = { throw new IntegrationError('Either anonymous user id or identity should be specified.') } - const defaultEventProperties = { segment_library: payload.library_name || HEAP_SEGMENT_LIBRARY_NAME } + const defaultEventProperties = { segment_library: HEAP_SEGMENT_CLOUD_LIBRARY_NAME } const flatten = flat(payload.properties || {}) const eventProperties = Object.assign(defaultEventProperties, flatten) - const event: HeapEvent = { + + const heapPayload: HeapEvent = { app_id: settings.appId, - event: payload.event, + event: getEventName(payload), properties: eventProperties, idempotency_key: payload.message_id } if (payload.anonymous_id && !payload.identity) { - event.user_id = getHeapUserId(payload.anonymous_id) + heapPayload.user_id = getHeapUserId(payload.anonymous_id) } if (payload.identity) { - event.identity = payload.identity + heapPayload.identity = payload.identity } if (payload.timestamp && dayjs.utc(payload.timestamp).isValid()) { - event.timestamp = dayjs.utc(payload.timestamp).toISOString() + heapPayload.timestamp = dayjs.utc(payload.timestamp).toISOString() + } + + if (payload.session_id) { + heapPayload.session_id = payload.session_id } return request('https://heapanalytics.com/api/track', { method: 'post', - json: event + json: heapPayload }) } } +const getEventName = (payload: Payload) => { + let eventName: string | undefined + switch (payload.type) { + case 'track': + eventName = payload.event + break + case 'page': + eventName = payload.name ? payload.name : 'Page Viewed' + break + case 'screen': + eventName = payload.name ? payload.name : 'Screen Viewed' + break + default: + eventName = 'track' + break + } + + if (!eventName) { + return 'track' + } + return eventName +} + export default action diff --git a/packages/destination-actions/src/destinations/index.ts b/packages/destination-actions/src/destinations/index.ts index 430758d234..a14ec2b141 100644 --- a/packages/destination-actions/src/destinations/index.ts +++ b/packages/destination-actions/src/destinations/index.ts @@ -71,6 +71,7 @@ register('63936c37dbc54a052e34e30e', './google-sheets-dev') register('63872c01c0c112b9b4d75412', './braze-cohorts') register('639c2dbb1309fdcad13951b6', './segment-profiles') register('63bedc136a8484a53739e013', './vwo') +register('63d17a1e6ab3e62212278cd0', './saleswings') function register(id: MetadataId, destinationPath: string) { // eslint-disable-next-line @typescript-eslint/no-var-requires diff --git a/packages/destination-actions/src/destinations/ironclad/RecordAction/__tests__/index.test.ts b/packages/destination-actions/src/destinations/ironclad/RecordAction/__tests__/index.test.ts index efb14e9cf0..97c3148e50 100644 --- a/packages/destination-actions/src/destinations/ironclad/RecordAction/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/ironclad/RecordAction/__tests__/index.test.ts @@ -18,7 +18,7 @@ const settingsProd = { const payload = { sig: 'test-user-njs1haohmb', - group_id: '1335' + group_id: 1335 } describe('Ironclad.recordAction', () => { diff --git a/packages/destination-actions/src/destinations/ironclad/RecordAction/generated-types.ts b/packages/destination-actions/src/destinations/ironclad/RecordAction/generated-types.ts index 901f7a8fb9..fda5e1368e 100644 --- a/packages/destination-actions/src/destinations/ironclad/RecordAction/generated-types.ts +++ b/packages/destination-actions/src/destinations/ironclad/RecordAction/generated-types.ts @@ -10,9 +10,9 @@ export interface Payload { */ event_name?: string /** - * The ID of the Clickwrap Group associated with the acceptance event. + * The ID of the Clickwrap Group associated with the acceptance event. Needs to be an integer */ - group_id: string + group_id: number /** * The type of event being logged, the available choices are displayed, agreed, and disagreed. */ diff --git a/packages/destination-actions/src/destinations/ironclad/RecordAction/index.ts b/packages/destination-actions/src/destinations/ironclad/RecordAction/index.ts index 862279f813..317ed8a6d4 100644 --- a/packages/destination-actions/src/destinations/ironclad/RecordAction/index.ts +++ b/packages/destination-actions/src/destinations/ironclad/RecordAction/index.ts @@ -26,14 +26,13 @@ const action: ActionDefinition = { description: 'The name of the event coming from the source, this is an additional information field before the call goes to Ironclad.', type: 'string', - default: { '@path': 'event' }, + default: { '@path': '$.event' }, required: false }, group_id: { label: 'Clickwrap Group Id', - description: 'The ID of the Clickwrap Group associated with the acceptance event.', - type: 'string', - default: '', + description: 'The ID of the Clickwrap Group associated with the acceptance event. Needs to be an integer', + type: 'integer', required: true }, event_type: { diff --git a/packages/destination-actions/src/destinations/ironclad/generated-types.ts b/packages/destination-actions/src/destinations/ironclad/generated-types.ts index c775269a83..60ed5d378c 100644 --- a/packages/destination-actions/src/destinations/ironclad/generated-types.ts +++ b/packages/destination-actions/src/destinations/ironclad/generated-types.ts @@ -8,9 +8,9 @@ export interface Settings { /** * Turn this ON, to send requests to the staging server, ONLY if Clickwrap support instructs you to do so. */ - staging_endpoint: boolean + staging_endpoint?: boolean /** * Test Mode, whether or not to process the acceptance in test_mode. Defaults to false, Toggle to ON to enable it. */ - test_mode: boolean + test_mode?: boolean } diff --git a/packages/destination-actions/src/destinations/ironclad/index.ts b/packages/destination-actions/src/destinations/ironclad/index.ts index 0734de415a..cdf1e2c9ad 100644 --- a/packages/destination-actions/src/destinations/ironclad/index.ts +++ b/packages/destination-actions/src/destinations/ironclad/index.ts @@ -16,7 +16,6 @@ const destination: DestinationDefinition = { description: 'Site Access ID. An ID that’s unique for each site within your account. Information on finding your sid can be found in the authentication section.', type: 'string', - default: '', required: true }, staging_endpoint: { @@ -24,7 +23,6 @@ const destination: DestinationDefinition = { description: 'Turn this ON, to send requests to the staging server, ONLY if Clickwrap support instructs you to do so.', type: 'boolean', - required: true, default: false }, test_mode: { @@ -32,7 +30,6 @@ const destination: DestinationDefinition = { description: 'Test Mode, whether or not to process the acceptance in test_mode. Defaults to false, Toggle to ON to enable it.', type: 'boolean', - required: true, default: false } } diff --git a/packages/destination-actions/src/destinations/pipedrive/createUpdateActivity/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/pipedrive/createUpdateActivity/__tests__/snapshot.test.ts index b2f5b12845..b5d0ff0e39 100644 --- a/packages/destination-actions/src/destinations/pipedrive/createUpdateActivity/__tests__/snapshot.test.ts +++ b/packages/destination-actions/src/destinations/pipedrive/createUpdateActivity/__tests__/snapshot.test.ts @@ -7,11 +7,13 @@ const testDestination = createTestIntegration(destination) const actionSlug = 'createUpdateActivity' const destinationSlug = 'Pipedrive' const seedName = `${destinationSlug}#${actionSlug}` +const PIPEDRIVE_DOMAIN = 'companydomain' describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination action:`, () => { it('required fields', async () => { const action = destination.actions[actionSlug] const [eventData, settingsData] = generateTestData(seedName, destination, action, true) + settingsData.domain = PIPEDRIVE_DOMAIN nock(/.*/).persist().get(/.*/).reply(200) nock(/.*/).persist().post(/.*/).reply(200) @@ -45,6 +47,7 @@ describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination ac it('all fields', async () => { const action = destination.actions[actionSlug] const [eventData, settingsData] = generateTestData(seedName, destination, action, false) + settingsData.domain = PIPEDRIVE_DOMAIN nock(/.*/).persist().get(/.*/).reply(200) nock(/.*/).persist().post(/.*/).reply(200) diff --git a/packages/destination-actions/src/destinations/pipedrive/createUpdateActivity/generated-types.ts b/packages/destination-actions/src/destinations/pipedrive/createUpdateActivity/generated-types.ts index 5292b9125c..8c6e928b7d 100644 --- a/packages/destination-actions/src/destinations/pipedrive/createUpdateActivity/generated-types.ts +++ b/packages/destination-actions/src/destinations/pipedrive/createUpdateActivity/generated-types.ts @@ -42,7 +42,7 @@ export interface Payload { */ description?: string /** - * Note of the Activity (HTML format) + * Note of the Activity (Accepts plain text and HTML) */ note?: string /** @@ -50,7 +50,7 @@ export interface Payload { */ due_date?: string /** - * Due time of the Activity in UTC. Format: HH:MM + * Due time of the Activity. Format: HH:MM */ due_time?: string /** diff --git a/packages/destination-actions/src/destinations/pipedrive/createUpdateActivity/index.ts b/packages/destination-actions/src/destinations/pipedrive/createUpdateActivity/index.ts index 26bdb2d92d..8d9245dc6e 100644 --- a/packages/destination-actions/src/destinations/pipedrive/createUpdateActivity/index.ts +++ b/packages/destination-actions/src/destinations/pipedrive/createUpdateActivity/index.ts @@ -9,20 +9,23 @@ const fieldHandler = PipedriveClient.fieldHandler const action: ActionDefinition = { title: 'Create or update an Activity', description: "Update an Activity in Pipedrive or create one if it doesn't exist.", - defaultSubscription: 'type = "track"', + defaultSubscription: 'type = "track" and event = "Activity Upserted"', fields: { activity_id: { label: 'Activity ID', description: 'ID of Activity in Pipedrive to Update. If left empty, a new one will be created', type: 'integer', - required: false + required: false, + default: { + '@path': '$.properties.activity_id' + } }, person_match_field: { label: 'Person match field', description: 'If present, used instead of field in settings to find existing person in Pipedrive.', type: 'string', required: false, - dynamic: true, + dynamic: true }, person_match_value: { label: 'Person match value', @@ -33,13 +36,12 @@ const action: ActionDefinition = { '@path': '$.userId' } }, - organization_match_field: { label: 'Organization match field', description: 'If present, used instead of field in settings to find existing organization in Pipedrive.', type: 'string', required: false, - dynamic: true, + dynamic: true }, organization_match_value: { label: 'Organization match value', @@ -47,16 +49,15 @@ const action: ActionDefinition = { type: 'string', required: false, default: { - '@path': '$.userId' + '@path': '$.context.groupId' } }, - deal_match_field: { label: 'Deal match field', description: 'If present, used instead of field in settings to find existing deal in Pipedrive.', type: 'string', required: false, - dynamic: true, + dynamic: true }, deal_match_value: { label: 'Deal match value', @@ -64,60 +65,83 @@ const action: ActionDefinition = { type: 'string', required: false, default: { - '@path': '$.userId' + '@path': '$.properties.deal_id' } }, - subject: { label: 'Activity Subject', description: 'Subject of the Activity. When value for subject is not set, it will be given a default value `Call`.', type: 'string', - required: false + required: false, + default: { + '@path': '$.properties.subject' + } }, type: { label: 'Type', description: 'Type of the Activity. This is in correlation with the key_string parameter of ActivityTypes. When value for type is not set, it will be given a default value `Call`', type: 'string', - required: false + required: false, + default: { + '@path': '$.properties.type' + } }, description: { label: 'Description', description: 'Additional details about the Activity that is synced to your external calendar. Unlike the note added to the Activity, the description is publicly visible to any guests added to the Activity.', type: 'string', - required: false + required: false, + default: { + '@path': '$.properties.description' + } }, note: { label: 'Note', - description: 'Note of the Activity (HTML format)', + description: 'Note of the Activity (Accepts plain text and HTML)', type: 'string', - required: false + required: false, + default: { + '@path': '$.properties.note' + } }, due_date: { label: 'Due Date', description: 'Due date of the Activity. Format: YYYY-MM-DD', type: 'string', - required: false + required: false, + default: { + '@path': '$.properties.due_date' + } }, due_time: { label: 'Due Time', - description: 'Due time of the Activity in UTC. Format: HH:MM', + description: 'Due time of the Activity. Format: HH:MM', type: 'string', - required: false + required: false, + default: { + '@path': '$.properties.due_time' + } }, duration: { label: 'Duration', description: 'Duration of the Activity. Format: HH:MM', type: 'string', - required: false + required: false, + default: { + '@path': '$.properties.duration' + } }, done: { label: 'Done', description: 'Whether the Activity is done or not.', type: 'boolean', - required: false + required: false, + default: { + '@path': '$.properties.done' + } } }, diff --git a/packages/destination-actions/src/destinations/pipedrive/createUpdateDeal/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/pipedrive/createUpdateDeal/__tests__/snapshot.test.ts index 7bb4717795..060ec94ecd 100644 --- a/packages/destination-actions/src/destinations/pipedrive/createUpdateDeal/__tests__/snapshot.test.ts +++ b/packages/destination-actions/src/destinations/pipedrive/createUpdateDeal/__tests__/snapshot.test.ts @@ -7,12 +7,13 @@ const testDestination = createTestIntegration(destination) const actionSlug = 'createUpdateDeal' const destinationSlug = 'Pipedrive' const seedName = `${destinationSlug}#${actionSlug}` +const PIPEDRIVE_DOMAIN = 'companydomain' describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination action:`, () => { it('required fields', async () => { const action = destination.actions[actionSlug] const [eventData, settingsData] = generateTestData(seedName, destination, action, true) - + settingsData.domain = PIPEDRIVE_DOMAIN const basePath = `https://${settingsData.domain}.pipedrive.com` nock(basePath) .persist() @@ -55,7 +56,7 @@ describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination ac it('all fields', async () => { const action = destination.actions[actionSlug] const [eventData, settingsData] = generateTestData(seedName, destination, action, false) - + settingsData.domain = PIPEDRIVE_DOMAIN const basePath = `https://${settingsData.domain}.pipedrive.com` nock(basePath) .persist() diff --git a/packages/destination-actions/src/destinations/pipedrive/createUpdateDeal/index.ts b/packages/destination-actions/src/destinations/pipedrive/createUpdateDeal/index.ts index 92ee1222e5..b9505d71e1 100644 --- a/packages/destination-actions/src/destinations/pipedrive/createUpdateDeal/index.ts +++ b/packages/destination-actions/src/destinations/pipedrive/createUpdateDeal/index.ts @@ -9,9 +9,9 @@ import { addCustomFieldsFromPayloadToEntity } from '../utils' const fieldHandler = PipedriveClient.fieldHandler const action: ActionDefinition = { - title: 'Create or Update a Deal', + title: 'Create or update a Deal', description: "Update a Deal in Pipedrive or create it if it doesn't exist yet.", - defaultSubscription: 'type = "track"', + defaultSubscription: 'type = "track" and event = "Deal Upserted"', fields: { deal_match_field: { label: 'Deal match field', @@ -26,7 +26,7 @@ const action: ActionDefinition = { type: 'string', required: false, default: { - '@path': '$.userId' + '@path': '$.properties.deal_id' } }, person_match_field: { @@ -45,7 +45,6 @@ const action: ActionDefinition = { '@path': '$.userId' } }, - organization_match_field: { label: 'Organization match field', description: 'If present, used instead of field in settings to find existing organization in Pipedrive.', @@ -59,35 +58,46 @@ const action: ActionDefinition = { type: 'string', required: false, default: { - '@path': '$.userId' + '@path': '$.context.groupId' } }, - title: { label: 'Title', description: 'Deal title (required for new Leads)', type: 'string', - required: true + required: true, + default: { + '@path': '$.properties.title' + } }, value: { label: 'Value', description: 'Value of the deal. If omitted, value will be set to 0.', type: 'string', - required: false + required: false, + default: { + '@path': '$.properties.value' + } }, currency: { label: 'Currency', description: 'Currency of the deal. Accepts a 3-character currency code. If omitted, currency will be set to the default currency of the authorized user.', type: 'string', - required: false + required: false, + default: { + '@path': '$.properties.currency' + } }, stage_id: { label: 'Stage ID', description: "The ID of a stage this Deal will be placed in a pipeline (note that you can't supply the ID of the pipeline as this will be assigned automatically based on stage_id). If omitted, the deal will be placed in the first stage of the default pipeline.", type: 'number', - required: false + required: false, + default: { + '@path': '$.properties.stage_id' + } }, status: { label: 'Status', @@ -105,20 +115,29 @@ const action: ActionDefinition = { label: 'Expected Close Date', description: 'The expected close date of the Deal. In ISO 8601 format: YYYY-MM-DD.', type: 'string', - required: false + required: false, + default: { + '@path': '$.properties.expected_close_date' + } }, probability: { label: 'Success Probability', description: 'Deal success probability percentage. Used/shown only when deal_probability for the pipeline of the deal is enabled.', type: 'number', - required: false + required: false, + default: { + '@path': '$.properties.success_probability' + } }, lost_reason: { label: 'Lost Reason', description: 'Optional message about why the deal was lost (to be used when status=lost)', type: 'string', - required: false + required: false, + default: { + '@path': '$.properties.lost_reason' + } }, visible_to: { label: 'Visible To', @@ -188,6 +207,10 @@ const action: ActionDefinition = { 400 ) } + if (!deal.id) + if (payload.deal_match_field && payload.deal_match_value) + // if there's no deal.id then we're doing a create operation, so we should include the deal_match field info so it's recorded on the new entity + Object.assign(deal, { [payload.deal_match_field]: payload.deal_match_value }) addCustomFieldsFromPayloadToEntity(payload, deal) diff --git a/packages/destination-actions/src/destinations/pipedrive/createUpdateLead/__tests__/index.test.ts b/packages/destination-actions/src/destinations/pipedrive/createUpdateLead/__tests__/index.test.ts index 0c6101a879..2ee211851a 100644 --- a/packages/destination-actions/src/destinations/pipedrive/createUpdateLead/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/pipedrive/createUpdateLead/__tests__/index.test.ts @@ -6,7 +6,7 @@ const testDestination = createTestIntegration(Destination) const PIPEDRIVE_API_KEY = 'random string' const PIPEDRIVE_DOMAIN = 'companydomain' -const LEAD_ID = 31337 +const LEAD_ID = '31337' describe('Pipedrive.createUpdateLead', () => { it('should create lead', async () => { @@ -34,12 +34,12 @@ describe('Pipedrive.createUpdateLead', () => { it('should update lead', async () => { const scope = nock(`https://${PIPEDRIVE_DOMAIN}.pipedrive.com/api/v1`) - .put(`/leads/${LEAD_ID}`, { + .patch(`/leads/${LEAD_ID}`, { title: 'New Title', organization_id: 520, value: { - currency: 'EUR', - amount: 3256.41 + amount: 3256.41, + currency: 'EUR' } }) .query({ api_token: PIPEDRIVE_API_KEY }) @@ -59,10 +59,8 @@ describe('Pipedrive.createUpdateLead', () => { title: 'New Title', organization_match_field: 'someOrgField', organization_match_value: 'Pipedrive OÜ', - value: { - currency: 'EUR', - amount: 3256.41 - }, + amount: 3256.41, + currency: 'EUR', lead_id: LEAD_ID }, settings: { apiToken: PIPEDRIVE_API_KEY, domain: PIPEDRIVE_DOMAIN } diff --git a/packages/destination-actions/src/destinations/pipedrive/createUpdateLead/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/pipedrive/createUpdateLead/__tests__/snapshot.test.ts index 8135a0b4b4..5160127a25 100644 --- a/packages/destination-actions/src/destinations/pipedrive/createUpdateLead/__tests__/snapshot.test.ts +++ b/packages/destination-actions/src/destinations/pipedrive/createUpdateLead/__tests__/snapshot.test.ts @@ -7,11 +7,13 @@ const testDestination = createTestIntegration(destination) const actionSlug = 'createUpdateLead' const destinationSlug = 'Pipedrive' const seedName = `${destinationSlug}#${actionSlug}` +const PIPEDRIVE_DOMAIN = 'companydomain' describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination action:`, () => { it('required fields', async () => { const action = destination.actions[actionSlug] const [eventData, settingsData] = generateTestData(seedName, destination, action, true) + settingsData.domain = PIPEDRIVE_DOMAIN Object.keys(eventData) .filter((f) => !action.fields[f].required) .forEach((f) => { @@ -60,7 +62,7 @@ describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination ac it('all fields', async () => { const action = destination.actions[actionSlug] const [eventData, settingsData] = generateTestData(seedName, destination, action, false) - + settingsData.domain = PIPEDRIVE_DOMAIN const basePath = `https://${settingsData.domain}.pipedrive.com` nock(basePath) .persist() @@ -69,7 +71,7 @@ describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination ac .reply(200, { data: [{ id: 42 }] }) - nock(basePath).persist().put(/.*/).reply(200) + nock(basePath).persist().patch(/.*/).reply(200) const event = createTestEvent({ properties: eventData diff --git a/packages/destination-actions/src/destinations/pipedrive/createUpdateLead/generated-types.ts b/packages/destination-actions/src/destinations/pipedrive/createUpdateLead/generated-types.ts index dfa0422595..d4f0e16e8b 100644 --- a/packages/destination-actions/src/destinations/pipedrive/createUpdateLead/generated-types.ts +++ b/packages/destination-actions/src/destinations/pipedrive/createUpdateLead/generated-types.ts @@ -4,7 +4,7 @@ export interface Payload { /** * ID of Lead in Pipedrive to Update. If left empty, a new one will be created */ - lead_id?: number + lead_id?: string /** * If present, used instead of field in settings to find existing person in Pipedrive. */ @@ -28,13 +28,11 @@ export interface Payload { /** * Potential value of the lead */ - value?: { - amount?: number - /** - * Three-letter code of the currency, e.g. USD - */ - currency?: string - } + amount?: number + /** + * Three-letter code of the currency, e.g. USD + */ + currency?: string /** * The date of when the Deal which will be created from the Lead is expected to be closed. In ISO 8601 format: YYYY-MM-DD. */ @@ -47,10 +45,4 @@ export interface Payload { * If the lead is created, use this timestamp as the creation timestamp. Format: YYY-MM-DD HH:MM:SS */ add_time?: string | number - /** - * New values for custom fields. - */ - custom_fields?: { - [k: string]: unknown - } } diff --git a/packages/destination-actions/src/destinations/pipedrive/createUpdateLead/index.ts b/packages/destination-actions/src/destinations/pipedrive/createUpdateLead/index.ts index 0d1d9635c8..e8f5757dc3 100644 --- a/packages/destination-actions/src/destinations/pipedrive/createUpdateLead/index.ts +++ b/packages/destination-actions/src/destinations/pipedrive/createUpdateLead/index.ts @@ -2,22 +2,28 @@ import type { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' import PipedriveClient from '../pipedriveApi/pipedrive-client' -import { createUpdateLead, Lead } from '../pipedriveApi/leads' +import { createUpdateLead, Lead, LeadValue } from '../pipedriveApi/leads' import { IntegrationError } from '@segment/actions-core' -import { addCustomFieldsFromPayloadToEntity } from '../utils' const fieldHandler = PipedriveClient.fieldHandler const action: ActionDefinition = { - title: 'Create or Update a Lead', + title: 'Create or update Lead', description: "Update a Lead in Pipedrive or create it if it doesn't exist yet.", defaultSubscription: 'type = "identify"', fields: { lead_id: { label: 'Lead ID', description: 'ID of Lead in Pipedrive to Update. If left empty, a new one will be created', - type: 'integer', - required: false + type: 'string', + required: false, + default: { + '@if': { + exists: { '@path': '$.traits.lead_id' }, + then: { '@path': '$.traits.lead_id' }, + else: { '@path': '$.properties.lead_id' } + } + } }, person_match_field: { label: 'Person match field', @@ -35,7 +41,6 @@ const action: ActionDefinition = { '@path': '$.userId' } }, - organization_match_field: { label: 'Organization match field', description: 'If present, used instead of field in settings to find existing organization in Pipedrive.', @@ -49,29 +54,45 @@ const action: ActionDefinition = { type: 'string', required: false, default: { - '@path': '$.userId' + '@path': '$.context.groupId' } }, - title: { label: 'Title', description: 'The name of the Lead', type: 'string', - required: true + required: true, + default: { + '@if': { + exists: { '@path': '$.traits.title' }, + then: { '@path': '$.traits.title' }, + else: { '@path': '$.properties.title' } + } + } }, - value: { - type: 'object', - label: 'Value', + amount: { + label: 'Amount', description: 'Potential value of the lead', - properties: { - amount: { - label: 'Amount', - type: 'number' - }, - currency: { - label: 'Currency', - description: 'Three-letter code of the currency, e.g. USD', - type: 'string' + type: 'number', + required: false, + default: { + '@if': { + exists: { '@path': '$.traits.amount' }, + then: { '@path': '$.traits.amount' }, + else: { '@path': '$.properties.amount' } + } + } + }, + currency: { + label: 'Currency', + description: 'Three-letter code of the currency, e.g. USD', + type: 'string', + required: false, + default: { + '@if': { + exists: { '@path': '$.traits.currency' }, + then: { '@path': '$.traits.currency' }, + else: { '@path': '$.properties.currency' } } } }, @@ -80,7 +101,14 @@ const action: ActionDefinition = { description: 'The date of when the Deal which will be created from the Lead is expected to be closed. In ISO 8601 format: YYYY-MM-DD.', type: 'string', - required: false + required: false, + default: { + '@if': { + exists: { '@path': '$.traits.expected_close_date' }, + then: { '@path': '$.traits.expected_close_date' }, + else: { '@path': '$.properties.expected_close_date' } + } + } }, visible_to: { label: 'Visible To', @@ -98,13 +126,6 @@ const action: ActionDefinition = { description: 'If the lead is created, use this timestamp as the creation timestamp. Format: YYY-MM-DD HH:MM:SS', type: 'datetime', required: false - }, - - custom_fields: { - label: 'Custom fields', - description: 'New values for custom fields.', - type: 'object', - required: false } }, @@ -123,6 +144,11 @@ const action: ActionDefinition = { client.getId('organization', organizationSearchField, payload.organization_match_value) ]) + const leadValue: LeadValue = { + amount: payload.amount, + currency: payload.currency + } + const lead: Lead = { id: payload.lead_id, title: payload.title, @@ -130,7 +156,7 @@ const action: ActionDefinition = { visible_to: payload.visible_to, person_id: personId || undefined, organization_id: organizationId || undefined, - value: payload.value, + value: payload.amount && payload.currency ? leadValue : undefined, add_time: payload.add_time ? `${payload.add_time}` : undefined } @@ -142,8 +168,6 @@ const action: ActionDefinition = { ) } - addCustomFieldsFromPayloadToEntity(payload, lead) - return createUpdateLead(client, lead) } } diff --git a/packages/destination-actions/src/destinations/pipedrive/createUpdateNote/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/pipedrive/createUpdateNote/__tests__/snapshot.test.ts index d4817bd890..c346b08662 100644 --- a/packages/destination-actions/src/destinations/pipedrive/createUpdateNote/__tests__/snapshot.test.ts +++ b/packages/destination-actions/src/destinations/pipedrive/createUpdateNote/__tests__/snapshot.test.ts @@ -7,12 +7,13 @@ const testDestination = createTestIntegration(destination) const actionSlug = 'createUpdateNote' const destinationSlug = 'Pipedrive' const seedName = `${destinationSlug}#${actionSlug}` +const PIPEDRIVE_DOMAIN = 'companydomain' describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination action:`, () => { it('required fields', async () => { const action = destination.actions[actionSlug] const [eventData, settingsData] = generateTestData(seedName, destination, action, true) - + settingsData.domain = PIPEDRIVE_DOMAIN Object.keys(eventData) .filter((f) => !action.fields[f].required) .forEach((f) => { @@ -61,7 +62,7 @@ describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination ac it('all fields', async () => { const action = destination.actions[actionSlug] const [eventData, settingsData] = generateTestData(seedName, destination, action, false) - + settingsData.domain = PIPEDRIVE_DOMAIN const basePath = `https://${settingsData.domain}.pipedrive.com` nock(basePath) .persist() diff --git a/packages/destination-actions/src/destinations/pipedrive/createUpdateNote/generated-types.ts b/packages/destination-actions/src/destinations/pipedrive/createUpdateNote/generated-types.ts index 206b0a483d..12c04e50a9 100644 --- a/packages/destination-actions/src/destinations/pipedrive/createUpdateNote/generated-types.ts +++ b/packages/destination-actions/src/destinations/pipedrive/createUpdateNote/generated-types.ts @@ -6,9 +6,9 @@ export interface Payload { */ note_id?: number /** - * ID of Lead in Pipedrive to link to. One of Lead, Person, Organization or Deal must be linked! + * ID of Lead in Pipedrive to link to. One of Lead, Person, Organization or Deal must be linked! */ - lead_id?: number + lead_id?: string /** * If present, used instead of field in settings to find existing person in Pipedrive. */ @@ -34,7 +34,7 @@ export interface Payload { */ deal_match_value?: string /** - * Content of the note in HTML format. Subject to sanitization on the back-end. + * Content of the note in text or HTML format. Subject to sanitization on the back-end. */ content: string } diff --git a/packages/destination-actions/src/destinations/pipedrive/createUpdateNote/index.ts b/packages/destination-actions/src/destinations/pipedrive/createUpdateNote/index.ts index 9e285daff8..7bfa0af25b 100644 --- a/packages/destination-actions/src/destinations/pipedrive/createUpdateNote/index.ts +++ b/packages/destination-actions/src/destinations/pipedrive/createUpdateNote/index.ts @@ -8,28 +8,34 @@ import { IntegrationError } from '@segment/actions-core' const fieldHandler = PipedriveClient.fieldHandler const action: ActionDefinition = { - title: 'Create or Update a Note', + title: 'Create or update a Note', description: "Update a Note in Pipedrive or create it if it doesn't exist yet.", - defaultSubscription: 'type = "track"', + defaultSubscription: 'type = "track" and event = "Note Upserted"', fields: { note_id: { label: 'Note ID', description: 'ID of Note in Pipedrive to Update. If left empty, a new one will be created', type: 'integer', - required: false + required: false, + default: { + '@path': '$.properties.note_id' + } }, lead_id: { label: 'Lead ID', - description: 'ID of Lead in Pipedrive to link to. One of Lead, Person, Organization or Deal must be linked!', - type: 'integer', - required: false + description: 'ID of Lead in Pipedrive to link to. One of Lead, Person, Organization or Deal must be linked!', + type: 'string', + required: false, + default: { + '@path': '$.properties.lead_id' + } }, person_match_field: { label: 'Person match field', description: 'If present, used instead of field in settings to find existing person in Pipedrive.', type: 'string', required: false, - dynamic: true, + dynamic: true }, person_match_value: { label: 'Person match value', @@ -40,13 +46,12 @@ const action: ActionDefinition = { '@path': '$.userId' } }, - organization_match_field: { label: 'Organization match field', description: 'If present, used instead of field in settings to find existing organization in Pipedrive.', type: 'string', required: false, - dynamic: true, + dynamic: true }, organization_match_value: { label: 'Organization match value', @@ -54,16 +59,15 @@ const action: ActionDefinition = { type: 'string', required: false, default: { - '@path': '$.userId' + '@path': '$.context.groupId' } }, - deal_match_field: { label: 'Deal match field', description: 'If present, used instead of field in settings to find existing deal in Pipedrive.', type: 'string', required: false, - dynamic: true, + dynamic: true }, deal_match_value: { label: 'Deal match value', @@ -71,15 +75,17 @@ const action: ActionDefinition = { type: 'string', required: false, default: { - '@path': '$.userId' + '@path': '$.properties.deal_id' } }, - content: { label: 'Note Content', - description: 'Content of the note in HTML format. Subject to sanitization on the back-end.', + description: 'Content of the note in text or HTML format. Subject to sanitization on the back-end.', type: 'string', - required: true + required: true, + default: { + '@path': '$.properties.content' + } } }, diff --git a/packages/destination-actions/src/destinations/pipedrive/createUpdateOrganization/__tests__/index.test.ts b/packages/destination-actions/src/destinations/pipedrive/createUpdateOrganization/__tests__/index.test.ts index 511661cbda..c3ad0f4c32 100644 --- a/packages/destination-actions/src/destinations/pipedrive/createUpdateOrganization/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/pipedrive/createUpdateOrganization/__tests__/index.test.ts @@ -11,21 +11,25 @@ const ORGANIZATION_ID = 33333 describe('Pipedrive.createUpdateOrganization', () => { it('should create organization if none exists', async () => { const scope = nock(`https://${PIPEDRIVE_DOMAIN}.pipedrive.com/api/v1`) - .post('/organizations', { name: 'Acme Corp' }) + .post('/organizations', { name: 'Acme Corp', custom_org_id: 'non_existant_custom_org_id_value' }) .query({ api_token: PIPEDRIVE_API_KEY }) .reply(200) scope .get(/.*/) .query((q) => { - return q.field_key === 'name' && q.field_type === 'organizationField' && q.term === 'Does not exist' + return ( + q.field_key === 'custom_org_id' && + q.field_type === 'organizationField' && + q.term === 'non_existant_custom_org_id_value' + ) }) .reply(200, { data: [] }) await testDestination.testAction('createUpdateOrganization', { - mapping: { name: 'Acme Corp', match_field: 'name', match_value: 'Does not exist' }, + mapping: { name: 'Acme Corp', match_field: 'custom_org_id', match_value: 'non_existant_custom_org_id_value' }, settings: { apiToken: PIPEDRIVE_API_KEY, domain: PIPEDRIVE_DOMAIN } }) diff --git a/packages/destination-actions/src/destinations/pipedrive/createUpdateOrganization/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/pipedrive/createUpdateOrganization/__tests__/snapshot.test.ts index 6a85cf2570..b5dddf707c 100644 --- a/packages/destination-actions/src/destinations/pipedrive/createUpdateOrganization/__tests__/snapshot.test.ts +++ b/packages/destination-actions/src/destinations/pipedrive/createUpdateOrganization/__tests__/snapshot.test.ts @@ -7,12 +7,13 @@ const testDestination = createTestIntegration(destination) const actionSlug = 'createUpdateOrganization' const destinationSlug = 'Pipedrive' const seedName = `${destinationSlug}#${actionSlug}` +const PIPEDRIVE_DOMAIN = 'companydomain' describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination action:`, () => { it('required fields', async () => { const action = destination.actions[actionSlug] const [eventData, settingsData] = generateTestData(seedName, destination, action, true) - + settingsData.domain = PIPEDRIVE_DOMAIN Object.keys(eventData) .filter((f) => !action.fields[f].required) .forEach((f) => { @@ -51,7 +52,7 @@ describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination ac it('required fields, update', async () => { const action = destination.actions[actionSlug] const [eventData, settingsData] = generateTestData(seedName, destination, action, true) - + settingsData.domain = PIPEDRIVE_DOMAIN Object.keys(eventData) .filter((f) => !action.fields[f].required) .forEach((f) => { @@ -99,7 +100,7 @@ describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination ac it('all fields', async () => { const action = destination.actions[actionSlug] const [eventData, settingsData] = generateTestData(seedName, destination, action, false) - + settingsData.domain = PIPEDRIVE_DOMAIN const basePath = `https://${settingsData.domain}.pipedrive.com` nock(basePath) .persist() diff --git a/packages/destination-actions/src/destinations/pipedrive/createUpdateOrganization/index.ts b/packages/destination-actions/src/destinations/pipedrive/createUpdateOrganization/index.ts index 4d4e140a69..13b87beab7 100644 --- a/packages/destination-actions/src/destinations/pipedrive/createUpdateOrganization/index.ts +++ b/packages/destination-actions/src/destinations/pipedrive/createUpdateOrganization/index.ts @@ -25,14 +25,17 @@ const action: ActionDefinition = { type: 'string', required: true, default: { - '@path': '$.userId' + '@path': '$.groupId' } }, name: { label: 'Organization Name', description: 'Name of the organization', type: 'string', - required: false + required: false, + default: { + '@path': '$.traits.name' + } }, visible_to: { label: 'Visible To', @@ -51,7 +54,6 @@ const action: ActionDefinition = { 'If the organization is created, use this timestamp as the creation timestamp. Format: YYY-MM-DD HH:MM:SS', type: 'datetime' }, - custom_fields: { label: 'Custom fields', description: 'New values for custom fields.', @@ -77,6 +79,11 @@ const action: ActionDefinition = { visible_to: payload.visible_to } + if (!organizationId) + if (payload.match_field && payload.match_value) + // if doing a create, write the match_field and match_value data to the new Organization object's custom field + Object.assign(organization, { [payload.match_field]: payload.match_value }) + addCustomFieldsFromPayloadToEntity(payload, organization) return createOrUpdateOrganizationById(request, settings.domain, organizationId, organization) diff --git a/packages/destination-actions/src/destinations/pipedrive/createUpdatePerson/__tests__/index.test.ts b/packages/destination-actions/src/destinations/pipedrive/createUpdatePerson/__tests__/index.test.ts index 1b16139fb5..f111694fa7 100644 --- a/packages/destination-actions/src/destinations/pipedrive/createUpdatePerson/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/pipedrive/createUpdatePerson/__tests__/index.test.ts @@ -11,21 +11,25 @@ const PERSON_ID = 33333 describe('Pipedrive.createUpdatePerson', () => { it('should create person if none exists', async () => { const scope = nock(`https://${PIPEDRIVE_DOMAIN}.pipedrive.com/api/v1`) - .post('/persons', { name: 'Acme Corp' }) + .post('/persons', { name: 'Acme Corp', person_external_id: 'test_person_external_id_value' }) .query({ api_token: PIPEDRIVE_API_KEY }) .reply(200) scope .get(/.*/) .query((q) => { - return q.field_key === 'name' && q.field_type === 'personField' && q.term === 'Does not exist' + return ( + q.field_key === 'person_external_id' && + q.field_type === 'personField' && + q.term === 'test_person_external_id_value' + ) }) .reply(200, { data: [] }) await testDestination.testAction('createUpdatePerson', { - mapping: { name: 'Acme Corp', match_field: 'name', match_value: 'Does not exist' }, + mapping: { name: 'Acme Corp', match_field: 'person_external_id', match_value: 'test_person_external_id_value' }, settings: { apiToken: PIPEDRIVE_API_KEY, domain: PIPEDRIVE_DOMAIN } }) diff --git a/packages/destination-actions/src/destinations/pipedrive/createUpdatePerson/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/pipedrive/createUpdatePerson/__tests__/snapshot.test.ts index f182072578..8b1bbf2ca5 100644 --- a/packages/destination-actions/src/destinations/pipedrive/createUpdatePerson/__tests__/snapshot.test.ts +++ b/packages/destination-actions/src/destinations/pipedrive/createUpdatePerson/__tests__/snapshot.test.ts @@ -8,11 +8,14 @@ const actionSlug = 'createUpdatePerson' const destinationSlug = 'Pipedrive' const seedName = `${destinationSlug}#${actionSlug}` +const PIPEDRIVE_DOMAIN = 'companydomain' + describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination action:`, () => { it('required fields', async () => { const action = destination.actions[actionSlug] const [eventData, settingsData] = generateTestData(seedName, destination, action, true) - + // generateTestData / chance sometimes generates a string that can't be parsed by nock + settingsData.domain = PIPEDRIVE_DOMAIN Object.keys(eventData) .filter((f) => !action.fields[f].required) .forEach((f) => { @@ -51,7 +54,8 @@ describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination ac it('required fields, update', async () => { const action = destination.actions[actionSlug] const [eventData, settingsData] = generateTestData(seedName, destination, action, true) - + // generateTestData / chance sometimes generates a string that can't be parsed by nock + settingsData.domain = PIPEDRIVE_DOMAIN Object.keys(eventData) .filter((f) => !action.fields[f].required) .forEach((f) => { @@ -99,7 +103,8 @@ describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination ac it('all fields', async () => { const action = destination.actions[actionSlug] const [eventData, settingsData] = generateTestData(seedName, destination, action, false) - + // generateTestData / chance sometimes generates a string that can't be parsed by nock + settingsData.domain = PIPEDRIVE_DOMAIN const basePath = `https://${settingsData.domain}.pipedrive.com` nock(basePath) .persist() diff --git a/packages/destination-actions/src/destinations/pipedrive/createUpdatePerson/index.ts b/packages/destination-actions/src/destinations/pipedrive/createUpdatePerson/index.ts index a23640e5cc..e832bf4421 100644 --- a/packages/destination-actions/src/destinations/pipedrive/createUpdatePerson/index.ts +++ b/packages/destination-actions/src/destinations/pipedrive/createUpdatePerson/index.ts @@ -32,21 +32,30 @@ const action: ActionDefinition = { label: 'Person Name', description: 'Name of the person', type: 'string', - required: false + required: false, + default: { + '@path': '$.traits.name' + } }, email: { label: 'Email Address', description: 'Email addresses for this person.', type: 'string', required: false, - multiple: true + multiple: true, + default: { + '@path': '$.traits.email' + } }, phone: { label: 'Phone Number', description: 'Phone numbers for the person.', type: 'string', required: false, - multiple: true + multiple: true, + default: { + '@path': '$.traits.phone' + } }, visible_to: { label: 'Visible To', @@ -92,6 +101,11 @@ const action: ActionDefinition = { visible_to: payload.visible_to } + if (!personId) + if (payload.match_field) + // if doing a create, include the match_field and match_value data so that it gets written to the new object + Object.assign(person, { [payload.match_field]: payload.match_value }) + addCustomFieldsFromPayloadToEntity(payload, person) return createOrUpdatePersonById(request, settings.domain, personId, person) diff --git a/packages/destination-actions/src/destinations/pipedrive/index.ts b/packages/destination-actions/src/destinations/pipedrive/index.ts index d6ebd40670..c18596f536 100644 --- a/packages/destination-actions/src/destinations/pipedrive/index.ts +++ b/packages/destination-actions/src/destinations/pipedrive/index.ts @@ -1,6 +1,6 @@ import createUpdateOrganization from './createUpdateOrganization' import createUpdatePerson from './createUpdatePerson' -import type { DestinationDefinition } from '@segment/actions-core' +import { defaultValues, DestinationDefinition } from '@segment/actions-core' import type { Settings } from './generated-types' import createUpdateActivity from './createUpdateActivity' @@ -78,7 +78,27 @@ const destination: DestinationDefinition = { createUpdateDeal, createUpdateLead, createUpdateNote - } + }, + presets: [ + { + name: 'Create or Update a Person', + subscribe: 'type = "identify"', + partnerAction: 'createUpdatePerson', + mapping: defaultValues(createUpdatePerson.fields) + }, + { + name: 'Create or Update an Organization', + subscribe: 'type = "group"', + partnerAction: 'createUpdateOrganization', + mapping: defaultValues(createUpdateOrganization.fields) + }, + { + name: 'Create or Update an Activity', + subscribe: 'type = "track" and event = "Activity Upserted"', + partnerAction: 'createUpdateActivity', + mapping: defaultValues(createUpdateActivity.fields) + } + ] } export default destination diff --git a/packages/destination-actions/src/destinations/pipedrive/pipedriveApi/leads.ts b/packages/destination-actions/src/destinations/pipedrive/pipedriveApi/leads.ts index 87cf9c67e9..48b6e4cd50 100644 --- a/packages/destination-actions/src/destinations/pipedrive/pipedriveApi/leads.ts +++ b/packages/destination-actions/src/destinations/pipedrive/pipedriveApi/leads.ts @@ -7,12 +7,17 @@ export interface Lead extends Record { expected_close_date?: string visible_to?: number - id?: number + id?: string person_id?: number organization_id?: number add_time?: string } +export type LeadValue = { + amount?: number + currency?: string +} + export async function createUpdateLead(client: PipedriveClient, lead: Lead): Promise { return client.createUpdate('leads', lead) } diff --git a/packages/destination-actions/src/destinations/pipedrive/pipedriveApi/notes.ts b/packages/destination-actions/src/destinations/pipedrive/pipedriveApi/notes.ts index 88e318784b..caf0dbbd06 100644 --- a/packages/destination-actions/src/destinations/pipedrive/pipedriveApi/notes.ts +++ b/packages/destination-actions/src/destinations/pipedrive/pipedriveApi/notes.ts @@ -5,7 +5,7 @@ export interface Note extends Record { content: string add_time?: string deal_id?: number - lead_id?: number + lead_id?: string person_id?: number org_id?: number } diff --git a/packages/destination-actions/src/destinations/pipedrive/pipedriveApi/pipedrive-client.ts b/packages/destination-actions/src/destinations/pipedrive/pipedriveApi/pipedrive-client.ts index 8af0c7e2cd..dbfee624ab 100644 --- a/packages/destination-actions/src/destinations/pipedrive/pipedriveApi/pipedrive-client.ts +++ b/packages/destination-actions/src/destinations/pipedrive/pipedriveApi/pipedrive-client.ts @@ -125,7 +125,8 @@ class PipedriveClient { if (item.id) { const id = item.id delete item['id'] - return this.put(`${itemPath}/${id}`, item) + if (itemPath == 'leads') return this.patch(`${itemPath}/${id}`, item) + else return this.put(`${itemPath}/${id}`, item) } return this.post(itemPath, item) } @@ -138,7 +139,11 @@ class PipedriveClient { return this.reqWithPayload(path, payload, 'put') } - async reqWithPayload(path: string, payload: Record, method: 'post' | 'put') { + async patch(path: string, payload: Record): Promise { + return this.reqWithPayload(path, payload, 'patch') + } + + async reqWithPayload(path: string, payload: Record, method: 'post' | 'put' | 'patch') { PipedriveClient.filterPayload(payload) const urlBase = `https://${this.settings.domain}.pipedrive.com/api/v1` return this._request(`${urlBase}/${path}`, { diff --git a/packages/destination-actions/src/destinations/qualtrics/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/qualtrics/__tests__/__snapshots__/snapshot.test.ts.snap index 7c78665a7c..a3cd199617 100644 --- a/packages/destination-actions/src/destinations/qualtrics/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/qualtrics/__tests__/__snapshots__/snapshot.test.ts.snap @@ -40,3 +40,26 @@ Headers { }, } `; + +exports[`Testing snapshot for actions-qualtrics destination: upsertContactTransaction action - all fields 1`] = ` +Object { + "": Object { + "contactId": "Z8AxKOF", + "data": Object { + "testType": "Z8AxKOF", + }, + "mailingListId": "Z8AxKOF", + "transactionDate": "2021-02-01 00:00:00", + }, +} +`; + +exports[`Testing snapshot for actions-qualtrics destination: upsertContactTransaction action - required fields 1`] = ` +Object { + "filter": Object { + "comparison": "eq", + "filterType": "extRef", + "value": "Z8AxKOF", + }, +} +`; diff --git a/packages/destination-actions/src/destinations/qualtrics/__tests__/index.test.ts b/packages/destination-actions/src/destinations/qualtrics/__tests__/index.test.ts index 8bbe9e46fb..8ac4fea8a0 100644 --- a/packages/destination-actions/src/destinations/qualtrics/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/qualtrics/__tests__/index.test.ts @@ -10,7 +10,11 @@ describe('Qualtrics', () => { nock('https://testdc.qualtrics.com') .get('/API/v3/whoami') .matchHeader('x-api-token', 'VALID_API_TOKEN_VALUE') - .reply(200, {}) + .reply(200, { + result: { + userName: 'abc' + } + }) const authData = { apiToken: 'VALID_API_TOKEN_VALUE', datacenter: 'testdc' diff --git a/packages/destination-actions/src/destinations/qualtrics/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/qualtrics/__tests__/snapshot.test.ts index 6cb31b1051..64e69246b2 100644 --- a/packages/destination-actions/src/destinations/qualtrics/__tests__/snapshot.test.ts +++ b/packages/destination-actions/src/destinations/qualtrics/__tests__/snapshot.test.ts @@ -8,13 +8,24 @@ const destinationSlug = 'actions-qualtrics' describe(`Testing snapshot for ${destinationSlug} destination:`, () => { for (const actionSlug in destination.actions) { + beforeEach(() => { + jest.spyOn(global.Math, 'random').mockReturnValue(1) + }) + + afterEach(() => { + jest.spyOn(global.Math, 'random').mockRestore() + }) + it(`${actionSlug} action - required fields`, async () => { const seedName = `${destinationSlug}#${actionSlug}` const action = destination.actions[actionSlug] const [eventData, settingsData] = generateTestData(seedName, destination, action, true) nock(/.*/).persist().get(/.*/).reply(200) - nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/) + .persist() + .post(/.*/) + .reply(200, { result: { elements: [{ id: 'CID_FOUND' }] } }) nock(/.*/).persist().put(/.*/).reply(200) const event = createTestEvent({ diff --git a/packages/destination-actions/src/destinations/qualtrics/addContactToXmd/index.ts b/packages/destination-actions/src/destinations/qualtrics/addContactToXmd/index.ts index af309177d7..f39c27954c 100644 --- a/packages/destination-actions/src/destinations/qualtrics/addContactToXmd/index.ts +++ b/packages/destination-actions/src/destinations/qualtrics/addContactToXmd/index.ts @@ -2,10 +2,12 @@ import type { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import QualtricsApiClient from '../qualtricsApiClient' import type { Payload } from './generated-types' +import { getDirectoryIds } from '../dynamicFields' const action: ActionDefinition = { - title: 'Add / Update Contact in XMD', - description: 'Add or update contact in XMD', + title: 'Create and/or update contact in XM Directory', + description: + 'Create and/or update contact in XM Directory. Updating is handled by contact deduplication in your [directory settings](https://www.qualtrics.com/support/iq-directory/directory-settings-tab/automatic-deduplication/). If deduplication is setup correctly this action will perform UPSERT operations on contacts', defaultSubscription: 'type = "identify"', fields: { directoryId: { @@ -13,9 +15,7 @@ const action: ActionDefinition = { type: 'string', description: 'Directory id. Also known as the Pool ID. POOL_XXX', required: true, - default: { - '@path': '$.traits.directoryId' - } + dynamic: true }, extRef: { label: 'External Data Reference', @@ -86,6 +86,9 @@ const action: ActionDefinition = { defaultObjectUI: 'keyvalue' } }, + dynamicFields: { + directoryId: getDirectoryIds + }, perform: (request, data) => { const apiClient = new QualtricsApiClient(data.settings.datacenter, data.settings.apiToken, request) const payload = { diff --git a/packages/destination-actions/src/destinations/qualtrics/dynamicFields.ts b/packages/destination-actions/src/destinations/qualtrics/dynamicFields.ts new file mode 100644 index 0000000000..00f6c6ab40 --- /dev/null +++ b/packages/destination-actions/src/destinations/qualtrics/dynamicFields.ts @@ -0,0 +1,41 @@ +import { DynamicFieldItem, DynamicFieldResponse } from '@segment/actions-core' +import QualtricsApiClient, { QualtricsApiErrorResponse } from './qualtricsApiClient' +import { RequestClient } from '@segment/actions-core' +import { ExecuteInput } from '@segment/actions-core' +import { Settings } from './generated-types' +import { HTTPError } from '@segment/actions-core' + +export async function getDirectoryIds( + request: RequestClient, + data: ExecuteInput +): Promise { + const choices: DynamicFieldItem[] = [] + const apiClient = new QualtricsApiClient(data.settings.datacenter, data.settings.apiToken, request) + try { + const response = await apiClient.listDirectories() + for (const directory of response.elements) { + choices.push({ + label: `${directory.name}`, + value: directory.directoryId + }) + } + } catch (err) { + return getError(err) + } + + return { + choices + } +} + +async function getError(err: unknown) { + const errResponse = (err as HTTPError)?.response + const errorBody = (await errResponse.json()) as QualtricsApiErrorResponse + return { + choices: [], + error: { + message: errorBody?.meta?.error?.errorMessage ?? 'Unknown Error', + code: errResponse?.status.toString() ?? '500' + } + } +} diff --git a/packages/destination-actions/src/destinations/qualtrics/index.ts b/packages/destination-actions/src/destinations/qualtrics/index.ts index 8f269aa9e8..120a3c7848 100644 --- a/packages/destination-actions/src/destinations/qualtrics/index.ts +++ b/packages/destination-actions/src/destinations/qualtrics/index.ts @@ -3,6 +3,7 @@ import type { Settings } from './generated-types' import { RequestClient } from '@segment/actions-core' import addContactToXmd from './addContactToXmd' +import upsertContactTransaction from './upsertContactTransaction' import triggerXflowWorkflow from './triggerXflowWorkflow' import QualtricsApiClient from './qualtricsApiClient' @@ -25,16 +26,19 @@ const destination: DestinationDefinition = { description: 'Qualtrics datacenter id that identifies where your qualtrics instance is located. Found under "Account settings" -> "Qualtrics IDs".', type: 'string', - required: true + required: true, + default: 'iad1' } }, - testAuthentication: (request: RequestClient, input) => { + testAuthentication: async (request: RequestClient, input) => { const apiClient = new QualtricsApiClient(input.settings.datacenter, input.settings.apiToken, request) - return apiClient.whoaAmI() + const response = await apiClient.whoaAmI() + return response.userName !== undefined } }, actions: { addContactToXmd, + upsertContactTransaction, triggerXflowWorkflow } } diff --git a/packages/destination-actions/src/destinations/qualtrics/qualtricsApiClient.ts b/packages/destination-actions/src/destinations/qualtrics/qualtricsApiClient.ts index 21c50cd3f4..ff4e5aebf6 100644 --- a/packages/destination-actions/src/destinations/qualtrics/qualtricsApiClient.ts +++ b/packages/destination-actions/src/destinations/qualtrics/qualtricsApiClient.ts @@ -1,4 +1,3 @@ -import { ModifiedResponse } from '@segment/actions-core' import { RequestClient } from '@segment/actions-core' export type SupportedMethods = 'get' | 'post' @@ -66,6 +65,55 @@ export type CreateDirectoryContactResponse = { id: string } +export type CreateContactTransactionRequest = Record< + string, + { + contactId: string + mailingListId: string + transactionDate: string // YYYY-MM-DD HH:MM:SS + data?: Record + } +> + +export type CreateContactTransactionResponse = { + createdTransactions: Record< + string, + { + id: string + } + > + unprocessedTransactions: Record< + string, + { + error: string + errorMessage: string + } + > +} + +export type SearchDirectoryForContactRequest = { + email?: string + extRef?: string + phone?: string +} + +export type SearchDirectoryContactResponse = { + id: string + creationDate: string + lastModified: string + firstName: string + lastName: string + email: string + phoneNumber: string + externalDataReference: string + language: string + unsubscribed: boolean + unsubscribeDate: string + stats: Record + embeddedData: Record + segmentMembership: Record +} + type StandardRequestParams = { headers: Record method: SupportedMethods @@ -84,27 +132,79 @@ export default class QualtricsApiClient { public async whoaAmI(): Promise { const endpoint = `/API/v3/whoami` - return ((await this.makeRequest(endpoint, 'get')).json() as unknown as QualtricsApiResponse) - .result as WhoAmiResponse + return (await this.makeRequest(endpoint, 'get')).result as WhoAmiResponse + } + + public async listDirectories(): Promise { + const endpoint = `/API/v3/directories/` + return (await this.makeRequest(endpoint, 'get')).result as ListDirectoriesResponse } public async createDirectoryContact( directoryId: string, body: CreateDirectoryContactRequest - ): Promise { + ): Promise { const endpoint = `/API/v3/directories/${directoryId}/contacts` - return await this.makeRequest(endpoint, 'post', body) + return (await this.makeRequest(endpoint, 'post', body)).result as CreateDirectoryContactResponse + } + + public async createContactTransaction( + directoryId: string, + body: CreateContactTransactionRequest + ): Promise { + const endpoint = `/API/v3/directories/${directoryId}/transactions` + return (await this.makeRequest(endpoint, 'post', body)).result as CreateContactTransactionResponse + } + + public async searchDirectoryForContact( + directoryId: string, + body: SearchDirectoryForContactRequest + ): Promise { + const endpoint = `/API/v3/directories/${directoryId}/contacts/search` + const filterBody = this.createFilterBody(body) + if (!filterBody) { + return [] + } + return (await this.makeRequest(endpoint, 'post', filterBody)).result?.elements as SearchDirectoryContactResponse[] } private async makeRequest( endpoint: string, method: SupportedMethods, body?: Record | undefined - ): Promise { - return await this.request(this.buildUrl(endpoint), { + ): Promise { + const response = await this.request(this.buildUrl(endpoint), { ...this.buildRequestParams(method), json: body }) + return response.data as QualtricsApiResponse + } + + private createFilterBody(body: SearchDirectoryForContactRequest) { + const filterArray: { filterType: string; comparison: string; value: string }[] = [] + const bodyKeys = Object.keys(body) + ;['extRef', 'phone', 'email'].forEach((key: string) => { + if (bodyKeys.includes(key) && body[key as keyof SearchDirectoryForContactRequest] !== undefined) { + filterArray.push({ + filterType: key, + comparison: 'eq', + value: body[key as keyof SearchDirectoryForContactRequest] as string + }) + } + }) + if (filterArray.length === 1) { + return { + filter: filterArray[0] + } + } else if (filterArray.length > 1) { + return { + filter: { + conjunction: 'and', + filters: filterArray + } + } + } + return } private buildUrl(endpoint: string): string { diff --git a/packages/destination-actions/src/destinations/qualtrics/triggerXflowWorkflow/index.ts b/packages/destination-actions/src/destinations/qualtrics/triggerXflowWorkflow/index.ts index 48f8796dd9..7762521de5 100644 --- a/packages/destination-actions/src/destinations/qualtrics/triggerXflowWorkflow/index.ts +++ b/packages/destination-actions/src/destinations/qualtrics/triggerXflowWorkflow/index.ts @@ -3,8 +3,8 @@ import type { Settings } from '../generated-types' import type { Payload } from './generated-types' const action: ActionDefinition = { - title: 'Trigger workflow in xflow', - description: 'This action triggers a workflow in Qualtrics xflow', + title: 'Start a workflow in Qualtrics', + description: 'This action is used to kick off a workflow in Qualtrics', fields: { workflowUrl: { label: 'Workflow URL', diff --git a/packages/destination-actions/src/destinations/qualtrics/upsertContactTransaction/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/qualtrics/upsertContactTransaction/__tests__/__snapshots__/snapshot.test.ts.snap new file mode 100644 index 0000000000..04ed608c2e --- /dev/null +++ b/packages/destination-actions/src/destinations/qualtrics/upsertContactTransaction/__tests__/__snapshots__/snapshot.test.ts.snap @@ -0,0 +1,24 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Testing snapshot for Qualtrics's upsertContactTransaction destination action: all fields 1`] = ` +Object { + "": Object { + "contactId": "8wZoSjxpvhD%EH8xG", + "data": Object { + "testType": "8wZoSjxpvhD%EH8xG", + }, + "mailingListId": "8wZoSjxpvhD%EH8xG", + "transactionDate": "2021-02-01 00:00:00", + }, +} +`; + +exports[`Testing snapshot for Qualtrics's upsertContactTransaction destination action: required fields 1`] = ` +Object { + "filter": Object { + "comparison": "eq", + "filterType": "extRef", + "value": "8wZoSjxpvhD%EH8xG", + }, +} +`; diff --git a/packages/destination-actions/src/destinations/qualtrics/upsertContactTransaction/__tests__/index.test.ts b/packages/destination-actions/src/destinations/qualtrics/upsertContactTransaction/__tests__/index.test.ts new file mode 100644 index 0000000000..957a6d1a0b --- /dev/null +++ b/packages/destination-actions/src/destinations/qualtrics/upsertContactTransaction/__tests__/index.test.ts @@ -0,0 +1,239 @@ +import nock from 'nock' +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import Destination from '../../index' + +const testDestination = createTestIntegration(Destination) +const SETTINGS = { + apiToken: 'VALID_API_TOKEN', + datacenter: 'testdc' +} +const DIRECTORY_ID = 'POOL_XXXX' +const CONTACT_ID = 'CID_XXXX' +const MAILING_LIST_ID = 'CG_XXXX' +const TRANSACTION_DATA = { Key: 'Value' } +const TRANSACTION_DATE = '2000-01-01 00:00:00' +const CONTACT_INFO = { + firstName: 'Jane', + lastName: 'Doe', + email: 'janedoe@email.com', + phone: '0005551234', + extRef: 'user_id_1', + language: 'en', + region: 'region', + unsubscribed: false +} +const FILTER_BODY = { + filter: { + conjunction: 'and', + filters: [ + { comparison: 'eq', filterType: 'extRef', value: CONTACT_INFO.extRef }, + { comparison: 'eq', filterType: 'phone', value: CONTACT_INFO.phone }, + { comparison: 'eq', filterType: 'email', value: CONTACT_INFO.email } + ] + } +} + +describe('upsertContactTransaction', () => { + it('should send a valid action when a contact id is supplied', async () => { + nock(`https://${SETTINGS.datacenter}.qualtrics.com`) + .post(`/API/v3/directories/${DIRECTORY_ID}/transactions`) + .matchHeader('x-api-token', SETTINGS.apiToken) + .reply(200, {}) + + const event = createTestEvent({ + userId: CONTACT_INFO.extRef, + traits: { + contactId: CONTACT_ID, + directoryId: DIRECTORY_ID, + mailingListId: MAILING_LIST_ID, + transactionData: TRANSACTION_DATA, + transactionDate: TRANSACTION_DATE + } + }) + + const response = await testDestination.testAction('upsertContactTransaction', { + event, + settings: SETTINGS, + useDefaultMappings: true, + mapping: { + directoryId: { + '@path': '$.traits.directoryId' + }, + mailingListId: { + '@path': '$.traits.mailingListId' + }, + contactId: { + '@path': '$.traits.contactId' + }, + transactionData: { + '@path': '$.traits.transactionData' + }, + transactionDate: { + '@path': '$.traits.transactionDate' + } + } + }) + const actualRequest = await response[0].request.json() + expect(actualRequest[Object.keys(actualRequest)[0]]).toMatchObject({ + contactId: CONTACT_ID, + data: TRANSACTION_DATA, + transactionDate: TRANSACTION_DATE, + mailingListId: MAILING_LIST_ID + }) + }) + it('should send a valid action when a contact id is not supplied and the contact search succeeds', async () => { + nock(`https://${SETTINGS.datacenter}.qualtrics.com`) + .post(`/API/v3/directories/${DIRECTORY_ID}/contacts/search`) + .matchHeader('x-api-token', SETTINGS.apiToken) + .reply(200, { result: { elements: [{ id: 'CID_FOUND' }] } }) + + nock(`https://${SETTINGS.datacenter}.qualtrics.com`) + .post(`/API/v3/directories/${DIRECTORY_ID}/transactions`) + .matchHeader('x-api-token', SETTINGS.apiToken) + .reply(200, {}) + + const event = createTestEvent({ + userId: CONTACT_INFO.extRef, + traits: { + directoryId: DIRECTORY_ID, + mailingListId: MAILING_LIST_ID, + transactionData: TRANSACTION_DATA, + transactionDate: TRANSACTION_DATE, + extRef: CONTACT_INFO.extRef, + email: CONTACT_INFO.email, + phone: CONTACT_INFO.phone + } + }) + + const response = await testDestination.testAction('upsertContactTransaction', { + event, + settings: SETTINGS, + useDefaultMappings: true, + mapping: { + directoryId: { + '@path': '$.traits.directoryId' + }, + mailingListId: { + '@path': '$.traits.mailingListId' + }, + email: { + '@path': '$.traits.email' + }, + phone: { + '@path': '$.traits.phone' + }, + extRef: { + '@path': '$.traits.extRef' + }, + transactionData: { + '@path': '$.traits.transactionData' + }, + transactionDate: { + '@path': '$.traits.transactionDate' + } + } + }) + const actualSearchRequest = await response[0].request.json() + expect(actualSearchRequest).toMatchObject(FILTER_BODY) + + const actualCreateTransactionRequest = await response[1].request.json() + expect(actualCreateTransactionRequest[Object.keys(actualCreateTransactionRequest)[0]]).toMatchObject({ + contactId: 'CID_FOUND', + data: TRANSACTION_DATA, + transactionDate: TRANSACTION_DATE, + mailingListId: MAILING_LIST_ID + }) + }) + it('should send a valid action when a contact id is not supplied, search fails to find and a new contact is created', async () => { + nock(`https://${SETTINGS.datacenter}.qualtrics.com`) + .post(`/API/v3/directories/${DIRECTORY_ID}/contacts/search`) + .matchHeader('x-api-token', SETTINGS.apiToken) + .reply(200, { result: { elements: [] } }) + + nock(`https://${SETTINGS.datacenter}.qualtrics.com`) + .post(`/API/v3/directories/${DIRECTORY_ID}/contacts`) + .matchHeader('x-api-token', SETTINGS.apiToken) + .reply(200, { result: { id: 'CID_CREATED' } }) + + nock(`https://${SETTINGS.datacenter}.qualtrics.com`) + .post(`/API/v3/directories/${DIRECTORY_ID}/transactions`) + .matchHeader('x-api-token', SETTINGS.apiToken) + .reply(200, {}) + + const event = createTestEvent({ + userId: CONTACT_INFO.extRef, + traits: { + directoryId: DIRECTORY_ID, + mailingListId: MAILING_LIST_ID, + transactionData: TRANSACTION_DATA, + transactionDate: TRANSACTION_DATE, + extRef: CONTACT_INFO.extRef, + firstName: CONTACT_INFO.firstName, + lastName: CONTACT_INFO.lastName, + email: CONTACT_INFO.email, + phone: CONTACT_INFO.phone, + language: CONTACT_INFO.language, + unsubscribed: CONTACT_INFO.unsubscribed + } + }) + + const response = await testDestination.testAction('upsertContactTransaction', { + event, + settings: SETTINGS, + useDefaultMappings: true, + mapping: { + directoryId: { + '@path': '$.traits.directoryId' + }, + mailingListId: { + '@path': '$.traits.mailingListId' + }, + email: { + '@path': '$.traits.email' + }, + phone: { + '@path': '$.traits.phone' + }, + extRef: { + '@path': '$.traits.extRef' + }, + firstName: { + '@path': '$.traits.firstName' + }, + lastName: { + '@path': '$.traits.lastName' + }, + language: { + '@path': '$.traits.language' + }, + transactionData: { + '@path': '$.traits.transactionData' + }, + transactionDate: { + '@path': '$.traits.transactionDate' + } + } + }) + const actualSearchRequest = await response[0].request.json() + expect(actualSearchRequest).toMatchObject(FILTER_BODY) + + const actualCreateContactRequest = await response[1].request.json() + expect(actualCreateContactRequest).toMatchObject({ + extRef: CONTACT_INFO.extRef, + firstName: CONTACT_INFO.firstName, + lastName: CONTACT_INFO.lastName, + email: CONTACT_INFO.email, + phone: CONTACT_INFO.phone, + language: CONTACT_INFO.language, + unsubscribed: CONTACT_INFO.unsubscribed + }) + + const actualCreateTransactionRequest = await response[2].request.json() + expect(actualCreateTransactionRequest[Object.keys(actualCreateTransactionRequest)[0]]).toMatchObject({ + contactId: 'CID_CREATED', + data: TRANSACTION_DATA, + transactionDate: TRANSACTION_DATE, + mailingListId: MAILING_LIST_ID + }) + }) +}) diff --git a/packages/destination-actions/src/destinations/qualtrics/upsertContactTransaction/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/qualtrics/upsertContactTransaction/__tests__/snapshot.test.ts new file mode 100644 index 0000000000..98ab56adab --- /dev/null +++ b/packages/destination-actions/src/destinations/qualtrics/upsertContactTransaction/__tests__/snapshot.test.ts @@ -0,0 +1,85 @@ +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import { generateTestData } from '../../../../lib/test-data' +import destination from '../../index' +import nock from 'nock' + +const testDestination = createTestIntegration(destination) +const actionSlug = 'upsertContactTransaction' +const destinationSlug = 'Qualtrics' +const seedName = `${destinationSlug}#${actionSlug}` + +describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination action:`, () => { + beforeEach(() => { + jest.spyOn(global.Math, 'random').mockReturnValue(1) + }) + + afterEach(() => { + jest.spyOn(global.Math, 'random').mockRestore() + }) + it('required fields', async () => { + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, true) + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/) + .persist() + .post(/.*/) + .reply(200, { result: { elements: [{ id: 'CID_FOUND' }] } }) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + properties: eventData + }) + + const responses = await testDestination.testAction(actionSlug, { + event: event, + mapping: event.properties, + settings: settingsData, + auth: undefined + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + + expect(request.headers).toMatchSnapshot() + }) + + it('all fields', async () => { + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, false) + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + properties: eventData + }) + + const responses = await testDestination.testAction(actionSlug, { + event: event, + mapping: event.properties, + settings: settingsData, + auth: undefined + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + }) +}) diff --git a/packages/destination-actions/src/destinations/qualtrics/upsertContactTransaction/generated-types.ts b/packages/destination-actions/src/destinations/qualtrics/upsertContactTransaction/generated-types.ts new file mode 100644 index 0000000000..fae244f2e0 --- /dev/null +++ b/packages/destination-actions/src/destinations/qualtrics/upsertContactTransaction/generated-types.ts @@ -0,0 +1,60 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * Directory id. Also known as the Pool ID. POOL_XXX + */ + directoryId: string + /** + * ID of the mailing list the contact belongs too. If not part of the event payload, create / use an existing mailing list from Qualtrics. Will have the form CG_xxx + */ + mailingListId: string + /** + * The id of the contact to add the transaction. if this field is not supplied, you must supply an external data reference, email and/or phone so a look can be performed. If the lookup does not find a contact, one will be created with these fields and including the optionally supplied firstName, lastName, language, subscribed and embeddedData + */ + contactId?: string + /** + * The external data reference which is a unique identifier for the user. This is only used to search for the contact and if a new contact needs to be created, it is not added to the transaction data. + */ + extRef: string + /** + * Email of contact. This is only used to search for the contact and if a new contact needs to be created, it is not added to the transaction data. + */ + email?: string + /** + * Phone number of contact. This is only used to search for the contact and if a new contact needs to be created, it is not added to the transaction data. + */ + phone?: string + /** + * First name of contact. This is only used if a new contact needs to be created and is not added to the transaction data. + */ + firstName?: string + /** + * Last name of contact. This is only used if a new contact needs to be created and is not added to the transaction data. + */ + lastName?: string + /** + * Language code of the contact. This is only used if a new contact needs to be created and is not added to the transaction data. + */ + language?: string + /** + * Should the contact be unsubscribed from correspondence. This is only used if a new contact needs to be created and is not added to the transaction. + */ + unsubscribed?: boolean + /** + * Contact embedded data (properties of the contact). These are added to the contact only if a new contact needs to be created not added to the transaction. + */ + embeddedData?: { + [k: string]: unknown + } + /** + * Date and time of when the transaction occurred. + */ + transactionDate?: string | number + /** + * Properties of the transaction too add to the users record + */ + transactionData?: { + [k: string]: unknown + } +} diff --git a/packages/destination-actions/src/destinations/qualtrics/upsertContactTransaction/index.ts b/packages/destination-actions/src/destinations/qualtrics/upsertContactTransaction/index.ts new file mode 100644 index 0000000000..44beec588b --- /dev/null +++ b/packages/destination-actions/src/destinations/qualtrics/upsertContactTransaction/index.ts @@ -0,0 +1,212 @@ +import type { ActionDefinition } from '@segment/actions-core' +import { IntegrationError } from '@segment/actions-core' +import type { Settings } from '../generated-types' +import QualtricsApiClient, { + CreateDirectoryContactRequest, + CreateDirectoryContactResponse, + SearchDirectoryContactResponse +} from '../qualtricsApiClient' +import { generateRandomId, parsedEmbeddedData, parsedTransactionDate } from '../utils' +import type { Payload } from './generated-types' +import { getDirectoryIds } from '../dynamicFields' + +async function searchOrCreateContact(payload: Payload, apiClient: QualtricsApiClient): Promise { + const contacts = await searchContact(payload, apiClient) + if (contacts.length === 0) { + const contact = await createContact(payload, apiClient) + return contact.id + } else if (contacts.length !== 1) { + throw new IntegrationError( + 'Unable to located one and only one contact in directory search', + 'directory_lookup_error', + 400 + ) + } else { + return contacts[0].id + } +} + +async function searchContact( + payload: Payload, + apiClient: QualtricsApiClient +): Promise { + if (!payload.email && !payload.extRef && !payload.phone) { + throw new IntegrationError( + 'Unable to lookup contact. At least 1 field is required: contactId, external data reference, email or phone', + 'directory_lookup_error', + 400 + ) + } + return await apiClient.searchDirectoryForContact(payload.directoryId, { + email: payload?.email, + extRef: payload?.extRef, + phone: payload?.phone + }) +} + +async function createContact(payload: Payload, apiClient: QualtricsApiClient): Promise { + const requestPayload: CreateDirectoryContactRequest = { + firstName: payload.firstName, + lastName: payload.lastName, + phone: payload.phone, + email: payload.email, + extRef: payload.extRef, + unsubscribed: payload.unsubscribed, + language: payload.language, + embeddedData: payload.embeddedData + } + return await apiClient.createDirectoryContact(payload.directoryId, requestPayload) +} + +const action: ActionDefinition = { + title: 'Upsert contact transaction', + description: + 'Add a transaction to a contact in Qualtrics directory. If the contact already exists, add the transaction. If the contact does not exist, create the contact first, then add the transaction record.', + defaultSubscription: 'type = "track", event = "Transaction Created"', + fields: { + directoryId: { + label: 'Directory ID', + type: 'string', + description: 'Directory id. Also known as the Pool ID. POOL_XXX', + required: true, + dynamic: true + }, + mailingListId: { + label: 'Mailing list ID', + type: 'string', + description: + 'ID of the mailing list the contact belongs too. If not part of the event payload, create / use an existing mailing list from Qualtrics. Will have the form CG_xxx', + required: true, + default: { + '@path': '$.traits.qualtricsMailingListId' + } + }, + contactId: { + label: 'Contact ID', + type: 'string', + description: + 'The id of the contact to add the transaction. if this field is not supplied, you must supply an external data reference, email and/or phone so a look can be performed. If the lookup does not find a contact, one will be created with these fields and including the optionally supplied firstName, lastName, language, subscribed and embeddedData', + default: { + '@path': '$.traits.qualtricsContactId' + } + }, + extRef: { + label: 'External Data Reference', + type: 'string', + description: + 'The external data reference which is a unique identifier for the user. This is only used to search for the contact and if a new contact needs to be created, it is not added to the transaction data.', + required: true, + default: { + '@if': { + exists: { '@path': '$.userId' }, + then: { '@path': '$.userId' }, + else: { '@path': '$.anonymousId' } + } + } + }, + email: { + label: 'Email', + type: 'string', + description: + 'Email of contact. This is only used to search for the contact and if a new contact needs to be created, it is not added to the transaction data.', + default: { + '@if': { + exists: { '@path': '$.email' }, + then: { '@path': '$.email' }, + else: { '@path': '$.traits.email' } + } + } + }, + phone: { + label: 'Phone number', + type: 'string', + description: + 'Phone number of contact. This is only used to search for the contact and if a new contact needs to be created, it is not added to the transaction data.', + default: { + '@path': '$.traits.phone' + } + }, + firstName: { + label: 'First name', + type: 'string', + description: + 'First name of contact. This is only used if a new contact needs to be created and is not added to the transaction data.', + default: { + '@path': '$.traits.firstName' + } + }, + lastName: { + label: 'Last name', + type: 'string', + description: + 'Last name of contact. This is only used if a new contact needs to be created and is not added to the transaction data.', + default: { + '@path': '$.traits.lastName' + } + }, + language: { + label: 'Language', + type: 'string', + description: + 'Language code of the contact. This is only used if a new contact needs to be created and is not added to the transaction data.', + default: { + '@if': { + exists: { '@path': '$.traits.language' }, + then: { '@path': '$.traits.language' }, + else: 'EN' + } + } + }, + unsubscribed: { + label: 'Contact is unsubscribed', + type: 'boolean', + description: + 'Should the contact be unsubscribed from correspondence. This is only used if a new contact needs to be created and is not added to the transaction.', + default: false + }, + embeddedData: { + label: 'Contact embedded data', + type: 'object', + description: + 'Contact embedded data (properties of the contact). These are added to the contact only if a new contact needs to be created not added to the transaction.', + defaultObjectUI: 'keyvalue' + }, + transactionDate: { + label: 'Date & time of transaction', + type: 'datetime', + description: 'Date and time of when the transaction occurred.', + default: { + '@path': '$.timestamp' + } + }, + transactionData: { + label: 'Transaction data', + type: 'object', + description: 'Properties of the transaction too add to the users record', + defaultObjectUI: 'keyvalue' + } + }, + dynamicFields: { + directoryId: getDirectoryIds + }, + perform: async (request, data) => { + let contactId = data.payload.contactId + const apiClient = new QualtricsApiClient(data.settings.datacenter, data.settings.apiToken, request) + if (!contactId) { + contactId = await searchOrCreateContact(data.payload, apiClient) + } + const parsedData = parsedEmbeddedData(data.payload?.transactionData) + const transactionDate = parsedTransactionDate(data.payload?.transactionDate) + const payload = { + [generateRandomId(16)]: { + contactId, + mailingListId: data.payload.mailingListId, + data: parsedData, + transactionDate + } + } + return apiClient.createContactTransaction(data.payload.directoryId, payload) + } +} + +export default action diff --git a/packages/destination-actions/src/destinations/qualtrics/utils.ts b/packages/destination-actions/src/destinations/qualtrics/utils.ts new file mode 100644 index 0000000000..13d2388c97 --- /dev/null +++ b/packages/destination-actions/src/destinations/qualtrics/utils.ts @@ -0,0 +1,49 @@ +export function parsedEmbeddedData( + data: { [key: string]: unknown } | undefined +): Record { + const parsedData: Record = {} + Object.keys(data || {}).forEach((key: string) => { + if (!data) { + return + } + if (typeof data[key] === 'string') { + parsedData[key] = data[key] as string + } else if (typeof data[key] === 'number') { + parsedData[key] = data[key] as number + } else if (typeof data[key] === 'boolean') { + parsedData[key] = data[key] as boolean + } else { + try { + parsedData[key] = JSON.stringify(data) + } catch (err) { + parsedData[key] = data.toString() + } + } + }) + return parsedData +} + +export function generateRandomId(length = 16): string { + const patternList = 'abcdefghijklmnopqrstuvwxyz123456789' + const result = [] + for (let i = 0; i < length; i++) { + result.push(patternList[Math.floor(Math.random() * patternList.length)]) + } + return result.join('') +} + +export function parsedTransactionDate(transactionDate: string | number | undefined): string { + let dateObject: Date = new Date() + if (typeof transactionDate === 'number') { + dateObject = new Date(transactionDate) + } else if (typeof transactionDate === 'string') { + const parsedDate = Date.parse(transactionDate) + if (!isNaN(parsedDate)) { + dateObject = new Date(parsedDate) + } + } + return dateObject + .toISOString() + .replace('T', ' ') + .replace(/\.\d{3}Z/, '') +} diff --git a/packages/destination-actions/src/destinations/salesforce/__tests__/account.test.ts b/packages/destination-actions/src/destinations/salesforce/__tests__/account.test.ts index e56b28173b..8e1dae5b00 100644 --- a/packages/destination-actions/src/destinations/salesforce/__tests__/account.test.ts +++ b/packages/destination-actions/src/destinations/salesforce/__tests__/account.test.ts @@ -6,7 +6,7 @@ import { API_VERSION } from '../sf-operations' const testDestination = createTestIntegration(Destination) const settings = { - instanceUrl: 'https://test.com/' + instanceUrl: 'https://test.salesforce.com/' } const auth = { refreshToken: 'xyz321', diff --git a/packages/destination-actions/src/destinations/salesforce/__tests__/cases.test.ts b/packages/destination-actions/src/destinations/salesforce/__tests__/cases.test.ts index 16facd2504..b83e724195 100644 --- a/packages/destination-actions/src/destinations/salesforce/__tests__/cases.test.ts +++ b/packages/destination-actions/src/destinations/salesforce/__tests__/cases.test.ts @@ -6,7 +6,7 @@ import { API_VERSION } from '../sf-operations' const testDestination = createTestIntegration(Destination) const settings = { - instanceUrl: 'https://test.com/' + instanceUrl: 'https://test.salesforce.com/' } const auth = { refreshToken: 'xyz123', diff --git a/packages/destination-actions/src/destinations/salesforce/__tests__/contact.test.ts b/packages/destination-actions/src/destinations/salesforce/__tests__/contact.test.ts index 40d1cd0ec8..2392149c2f 100644 --- a/packages/destination-actions/src/destinations/salesforce/__tests__/contact.test.ts +++ b/packages/destination-actions/src/destinations/salesforce/__tests__/contact.test.ts @@ -6,7 +6,7 @@ import { API_VERSION } from '../sf-operations' const testDestination = createTestIntegration(Destination) const settings = { - instanceUrl: 'https://test.com/' + instanceUrl: 'https://test.salesforce.com/' } const auth = { refreshToken: 'xyz321', diff --git a/packages/destination-actions/src/destinations/salesforce/__tests__/customObject.test.ts b/packages/destination-actions/src/destinations/salesforce/__tests__/customObject.test.ts index ef3809baa0..f401a0bb55 100644 --- a/packages/destination-actions/src/destinations/salesforce/__tests__/customObject.test.ts +++ b/packages/destination-actions/src/destinations/salesforce/__tests__/customObject.test.ts @@ -7,7 +7,7 @@ import { DynamicFieldResponse } from '@segment/actions-core' const testDestination = createTestIntegration(Destination) const settings = { - instanceUrl: 'https://test.com' + instanceUrl: 'https://test.salesforce.com' } const auth = { refreshToken: 'xyz321', diff --git a/packages/destination-actions/src/destinations/salesforce/__tests__/lead.test.ts b/packages/destination-actions/src/destinations/salesforce/__tests__/lead.test.ts index 02be325f8c..feec553ac5 100644 --- a/packages/destination-actions/src/destinations/salesforce/__tests__/lead.test.ts +++ b/packages/destination-actions/src/destinations/salesforce/__tests__/lead.test.ts @@ -6,7 +6,7 @@ import { API_VERSION } from '../sf-operations' const testDestination = createTestIntegration(Destination) const settings = { - instanceUrl: 'https://test.com/' + instanceUrl: 'https://test.salesforce.com/' } const auth = { refreshToken: 'xyz321', diff --git a/packages/destination-actions/src/destinations/salesforce/__tests__/opportunity.test.ts b/packages/destination-actions/src/destinations/salesforce/__tests__/opportunity.test.ts index 7658a4b7c4..db1f346348 100644 --- a/packages/destination-actions/src/destinations/salesforce/__tests__/opportunity.test.ts +++ b/packages/destination-actions/src/destinations/salesforce/__tests__/opportunity.test.ts @@ -6,7 +6,7 @@ import { API_VERSION } from '../sf-operations' const testDestination = createTestIntegration(Destination) const settings = { - instanceUrl: 'https://test.com/' + instanceUrl: 'https://test.salesforce.com/' } const auth = { refreshToken: 'xyz321', diff --git a/packages/destination-actions/src/destinations/salesforce/__tests__/sf-operations.test.ts b/packages/destination-actions/src/destinations/salesforce/__tests__/sf-operations.test.ts index 3e1d01131f..0e4299bc5f 100644 --- a/packages/destination-actions/src/destinations/salesforce/__tests__/sf-operations.test.ts +++ b/packages/destination-actions/src/destinations/salesforce/__tests__/sf-operations.test.ts @@ -5,7 +5,7 @@ import { API_VERSION } from '../sf-operations' import type { GenericPayload } from '../sf-types' const settings = { - instanceUrl: 'https://test.com/' + instanceUrl: 'https://test.salesforce.com/' } const requestClient = createRequestClient() diff --git a/packages/destination-actions/src/destinations/salesforce/__tests__/sf-utils.test.ts b/packages/destination-actions/src/destinations/salesforce/__tests__/sf-utils.test.ts index 7a350e7ee8..cb7a5e2875 100644 --- a/packages/destination-actions/src/destinations/salesforce/__tests__/sf-utils.test.ts +++ b/packages/destination-actions/src/destinations/salesforce/__tests__/sf-utils.test.ts @@ -1,5 +1,9 @@ import { GenericPayload } from '../sf-types' import { buildCSVData } from '../sf-utils' +import Salesforce from '../sf-operations' +import createRequestClient from '../../../../../core/src/create-request-client' + +const requestClient = createRequestClient() describe('Salesforce Utils', () => { describe('CSV', () => { @@ -246,4 +250,46 @@ describe('Salesforce Utils', () => { expect(csv).toEqual(expected) }) }) + + describe('Instance URL', () => { + const badInstanceUrls = [ + 'https://www.google.com', + 'http://how-to-salesforce.com', + 'http://thisisnotsalesforce.com', + 'http://salesforce-tips.co', + 'http://na1.salesforce.com/', + 'www.website.com', + 'ijoewhnukdsfj,' + ] + + // Note: These end in '/' to ensure that the instance URL we input matches the expected output + const validInstanceUrls = [ + 'https://na1.salesforce.com/', + 'https://krusty-krab.my.salesforce.com/', + 'https://sometesting-instanceurl-93244--staging.sandbox.my.salesforce.com/' + ] + + it('should throw an error if the instance URL is not provided', async () => { + const instanceUrl = '' + + expect(() => new Salesforce(instanceUrl, requestClient)).toThrow( + 'Empty Salesforce instance URL. Please login through OAuth.' + ) + }) + + it('should reject invalid instance URLs', async () => { + badInstanceUrls.forEach((instanceUrl) => { + expect(() => new Salesforce(instanceUrl, requestClient)).toThrow( + 'Invalid Salesforce instance URL. Please login through OAuth again.' + ) + }) + }) + + it('should accept valid instance URLs', async () => { + validInstanceUrls.forEach((instanceUrl) => { + const sf = new Salesforce(instanceUrl, requestClient) + expect(sf.instanceUrl).toEqual(instanceUrl) + }) + }) + }) }) diff --git a/packages/destination-actions/src/destinations/salesforce/sf-operations.ts b/packages/destination-actions/src/destinations/salesforce/sf-operations.ts index ff09f18516..2fd72df3d1 100644 --- a/packages/destination-actions/src/destinations/salesforce/sf-operations.ts +++ b/packages/destination-actions/src/destinations/salesforce/sf-operations.ts @@ -1,7 +1,7 @@ import { IntegrationError, RequestClient } from '@segment/actions-core' import type { GenericPayload } from './sf-types' import { mapObjectToShape } from './sf-object-to-shape' -import { buildCSVData } from './sf-utils' +import { buildCSVData, validateInstanceURL } from './sf-utils' import { DynamicFieldResponse } from '@segment/actions-core' export const API_VERSION = 'v53.0' @@ -70,9 +70,11 @@ export default class Salesforce { request: RequestClient constructor(instanceUrl: string, request: RequestClient) { + this.instanceUrl = validateInstanceURL(instanceUrl) + // If the instanceUrl does not end with '/' append it to the string. // This ensures that all request urls are constructed properly - this.instanceUrl = instanceUrl.concat(instanceUrl.slice(-1) === '/' ? '' : '/') + this.instanceUrl = this.instanceUrl.concat(instanceUrl.slice(-1) === '/' ? '' : '/') this.request = request } diff --git a/packages/destination-actions/src/destinations/salesforce/sf-utils.ts b/packages/destination-actions/src/destinations/salesforce/sf-utils.ts index adac5d5f67..b9e894451e 100644 --- a/packages/destination-actions/src/destinations/salesforce/sf-utils.ts +++ b/packages/destination-actions/src/destinations/salesforce/sf-utils.ts @@ -172,3 +172,26 @@ const snakeCaseToPascalCase = (key: string): string => { const token = camelCase(key) return token.charAt(0).toUpperCase() + token.slice(1) } + +export const validateInstanceURL = (instanceUrl: string): string => { + if (instanceUrl === undefined || instanceUrl === '') { + throw new IntegrationError( + 'Empty Salesforce instance URL. Please login through OAuth.', + 'INVALID_INSTANCE_URL', + 400 + ) + } + + const salesforceRegex = /^(https):\/\/.*\.salesforce\.com/ + const isValid = salesforceRegex.test(instanceUrl) + + if (isValid) { + return instanceUrl + } + + throw new IntegrationError( + 'Invalid Salesforce instance URL. Please login through OAuth again.', + 'INVALID_INSTANCE_URL', + 400 + ) +} diff --git a/packages/destination-actions/src/destinations/saleswings/__tests__/index.test.ts b/packages/destination-actions/src/destinations/saleswings/__tests__/index.test.ts new file mode 100644 index 0000000000..13a035dc99 --- /dev/null +++ b/packages/destination-actions/src/destinations/saleswings/__tests__/index.test.ts @@ -0,0 +1,21 @@ +import nock from 'nock' +import { createTestIntegration } from '@segment/actions-core' +import Definition from '../index' +import { apiBaseUrl } from '../api' + +const testDestination = createTestIntegration(Definition) + +describe('Saleswings', () => { + describe('testAuthentication', () => { + it('should validate authentication inputs', async () => { + nock(apiBaseUrl).get('/project/account').matchHeader('authorization', 'Bearer myApiKey').reply(200, {}) + await expect(testDestination.testAuthentication({ apiKey: 'myApiKey' })).resolves.not.toThrowError() + }) + + it('should reject invalid API key', async () => { + nock(apiBaseUrl).get('/project/account').matchHeader('authorization', 'Bearer myApiKey').reply(200, {}) + nock(apiBaseUrl).get('/project/account').reply(401, {}) + await expect(testDestination.testAuthentication({ apiKey: 'invalidApiKey' })).rejects.toThrowError() + }) + }) +}) diff --git a/packages/destination-actions/src/destinations/saleswings/api.ts b/packages/destination-actions/src/destinations/saleswings/api.ts new file mode 100644 index 0000000000..c8f6703ee2 --- /dev/null +++ b/packages/destination-actions/src/destinations/saleswings/api.ts @@ -0,0 +1,46 @@ +export const apiBaseUrl = 'https://helium.saleswings.pro/api/core' + +export type Event = TrackingEvent | PageVisitEvent + +export type EventBatch = { + events: Event[] +} + +export class TrackingEvent { + leadRefs: LeadRef[] + kind: string + data: string + url?: string + referrerUrl?: string + userAgent?: string + timestamp: number + values: ValueMap + readonly type: string = 'tracking' + + public constructor(fields?: Partial) { + Object.assign(this, fields) + } +} + +export class PageVisitEvent { + leadRefs: LeadRef[] + url: string + referrerUrl?: string + userAgent?: string + timestamp: number + readonly type: string = 'page-visit' + + public constructor(fields?: Partial) { + Object.assign(this, fields) + } +} + +export type Value = string | number | boolean +export type ValueMap = { [k: string]: Value } + +export type LeadRefType = 'email' | 'client-id' + +export type LeadRef = { + type: LeadRefType + value: string +} diff --git a/packages/destination-actions/src/destinations/saleswings/common.ts b/packages/destination-actions/src/destinations/saleswings/common.ts new file mode 100644 index 0000000000..6c2fd8235f --- /dev/null +++ b/packages/destination-actions/src/destinations/saleswings/common.ts @@ -0,0 +1,38 @@ +import { RequestFn } from '@segment/actions-core' +import { apiBaseUrl, Event, EventBatch } from './api' +import { Settings } from './generated-types' + +export function perform(convertEvent: (payload: Payload) => Event | undefined): RequestFn { + return (request, data) => { + const event = convertEvent(data.payload) + if (!event) return + return request(`${apiBaseUrl}/events`, { + method: 'post', + json: event, + headers: { Authorization: `Bearer ${data.settings.apiKey}` } + }) + } +} + +export function performBatch( + convertEvent: (payload: Payload) => Event | undefined +): RequestFn { + return (request, data) => { + const batch = convertEventBatch(data.payload, convertEvent) + if (!batch) return + return request(`${apiBaseUrl}/events/batches`, { + method: 'post', + json: batch, + headers: { Authorization: `Bearer ${data.settings.apiKey}` } + }) + } +} + +function convertEventBatch( + payloads: Payload[], + convertEvent: (payload: Payload) => Event | undefined +): EventBatch | undefined { + const events = payloads.map(convertEvent).filter((evt) => evt) as Event[] + if (events.length == 0) return undefined + return { events } +} diff --git a/packages/destination-actions/src/destinations/saleswings/converter.ts b/packages/destination-actions/src/destinations/saleswings/converter.ts new file mode 100644 index 0000000000..9c9c95e930 --- /dev/null +++ b/packages/destination-actions/src/destinations/saleswings/converter.ts @@ -0,0 +1,30 @@ +import { LeadRef, ValueMap } from './api' + +type EventPayload = { + userId?: string + anonymousId?: string + email?: string +} + +export const convertLeadRefs = (payload: EventPayload): LeadRef[] => { + const refs: LeadRef[] = [] + if (payload.userId) refs.push({ type: 'client-id', value: payload.userId }) + if (payload.anonymousId) refs.push({ type: 'client-id', value: payload.anonymousId }) + if (payload.email) refs.push({ type: 'email', value: payload.email }) + return refs +} + +export const convertValues = (source: { [k: string]: unknown } | undefined): ValueMap => { + const values: ValueMap = {} + if (!source) return values + Object.entries(source).forEach(([key, prop]) => { + if (typeof prop === 'number' || typeof prop === 'boolean' || typeof prop === 'string') values[key] = prop + }) + return values +} + +export const convertTimestamp = (timestamp: any): number => { + if (!timestamp) return Date.now() + if (typeof timestamp === 'number') return timestamp + return Date.parse(timestamp) +} diff --git a/packages/destination-actions/src/destinations/saleswings/fields.ts b/packages/destination-actions/src/destinations/saleswings/fields.ts new file mode 100644 index 0000000000..932e5652c7 --- /dev/null +++ b/packages/destination-actions/src/destinations/saleswings/fields.ts @@ -0,0 +1,109 @@ +import { InputField } from '@segment/actions-core' +import { Directive } from '@segment/actions-core/src/destination-kit/types' + +export const userId: InputField = { + label: 'Segment User ID', + description: 'Permanent identifier of a Segment user the event is attributed to.', + type: 'string', + dynamic: true, + default: { + '@path': '$.userId' + } +} + +export const anonymousId: InputField = { + label: 'Segment Anonymous User ID', + description: 'A pseudo-unique substitute for a Segment user ID the event is attributed to.', + type: 'string', + default: { + '@path': '$.anonymousId' + } +} + +export const email: InputField = { + label: 'Email', + description: 'Identified email of the Segment User.', + type: 'string', + default: { + '@if': { + exists: { '@path': '$.traits.email' }, + then: { '@path': '$.traits.email' }, + else: { '@path': '$.properties.email' } + } + } +} + +export const url: InputField = { + label: 'URL', + description: 'URL associated with the event.', + type: 'string', + default: { + '@if': { + exists: { '@path': '$.properties.url' }, + then: { '@path': '$.properties.url' }, + else: { '@path': '$.context.page.url' } + } + } +} + +export const referrerUrl: InputField = { + label: 'Referrer URL', + description: 'Referrer URL associated with the event.', + type: 'string', + default: { + '@if': { + exists: { '@path': '$.properties.referrer' }, + then: { '@path': '$.properties.referrer' }, + else: { '@path': '$.context.page.referrer' } + } + } +} + +export const userAgent: InputField = { + label: 'User Agent', + description: 'User Agent associated with the event.', + type: 'string', + default: { + '@path': '$.context.userAgent' + } +} + +export const timestamp: InputField = { + label: 'Event Timestamp', + description: 'When the event was sent.', + type: 'datetime', + default: { + '@path': '$.timestamp' + } +} + +export const kind = (defaultValue: string): InputField => { + return { + label: 'Custom Event Kind', + description: + 'Type of the SalesWings custom event (a custom event is visualized in SalesWings cockpit and SalesForce Lead Intent View as "[[Kind]] Data").', + type: 'string', + default: defaultValue, + required: true + } +} + +export const data = (defaultValue: Directive): InputField => { + return { + label: 'Custom Event Data', + description: + 'String description of the SalesWings custom event payload (a custom event is visualized in SalesWings cockpit and SalesForce Lead Intent View as "[[Kind]] Data").', + type: 'string', + default: defaultValue, + required: true + } +} + +export const values = (defaultValue: Directive): InputField => { + return { + label: 'Custom Attribute Values', + description: 'Custom attribute values associated with the SalesWings custom event.', + type: 'object', + default: defaultValue + } +} diff --git a/packages/destination-actions/src/destinations/saleswings/generated-types.ts b/packages/destination-actions/src/destinations/saleswings/generated-types.ts new file mode 100644 index 0000000000..f0eb97ee46 --- /dev/null +++ b/packages/destination-actions/src/destinations/saleswings/generated-types.ts @@ -0,0 +1,8 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Settings { + /** + * Segment.io API key for your SalesWings project. + */ + apiKey: string +} diff --git a/packages/destination-actions/src/destinations/saleswings/index.ts b/packages/destination-actions/src/destinations/saleswings/index.ts new file mode 100644 index 0000000000..e85287f3f3 --- /dev/null +++ b/packages/destination-actions/src/destinations/saleswings/index.ts @@ -0,0 +1,68 @@ +import { defaultValues, DestinationDefinition } from '@segment/actions-core' +import { apiBaseUrl } from './api' +import type { Settings } from './generated-types' + +import submitTrackEvent from './submitTrackEvent' +import submitIdentifyEvent from './submitIdentifyEvent' +import submitPageEvent from './submitPageEvent' +import submitScreenEvent from './submitScreenEvent' + +const destination: DestinationDefinition = { + name: 'Saleswings (Actions)', + slug: 'actions-saleswings', + mode: 'cloud', + + authentication: { + scheme: 'custom', + fields: { + apiKey: { + label: 'API Key', + description: 'Segment.io API key for your SalesWings project.', + type: 'password', + required: true + } + }, + testAuthentication: async (request, { settings }) => { + const resp = await request(`${apiBaseUrl}/project/account`, { + headers: { Authorization: `Bearer ${settings.apiKey}` } + }) + return resp.status == 200 + } + }, + + presets: [ + { + name: 'Submit Track Event', + subscribe: 'type = "track"', + partnerAction: 'submitTrackEvent', + mapping: defaultValues(submitTrackEvent.fields) + }, + { + name: 'Submit Identify Event', + subscribe: 'type = "identify"', + partnerAction: 'submitIdentifyEvent', + mapping: defaultValues(submitIdentifyEvent.fields) + }, + { + name: 'Submit Page Event', + subscribe: 'type = "page"', + partnerAction: 'submitPageEvent', + mapping: defaultValues(submitPageEvent.fields) + }, + { + name: 'Submit Screen Event', + subscribe: 'type = "screen"', + partnerAction: 'submitScreenEvent', + mapping: defaultValues(submitScreenEvent.fields) + } + ], + + actions: { + submitTrackEvent, + submitIdentifyEvent, + submitPageEvent, + submitScreenEvent + } +} + +export default destination diff --git a/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/__tests__/__snapshots__/snapshot.test.ts.snap new file mode 100644 index 0000000000..68160c6e62 --- /dev/null +++ b/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/__tests__/__snapshots__/snapshot.test.ts.snap @@ -0,0 +1,57 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Testing snapshot for 's submitIdentifyEvent destination action: all fields 1`] = ` +Object { + "data": "peter@example.com", + "kind": "Identify", + "leadRefs": Array [ + Object { + "type": "client-id", + "value": "user1234", + }, + Object { + "type": "client-id", + "value": "anonId1234", + }, + Object { + "type": "email", + "value": "peter@example.com", + }, + ], + "timestamp": 1671371213652, + "type": "tracking", + "url": "Cge8DWzjce", + "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1", + "values": Object { + "email": "peter@example.com", + }, +} +`; + +exports[`Testing snapshot for 's submitIdentifyEvent destination action: required fields 1`] = ` +Object { + "data": "peter@example.com", + "kind": "Identify", + "leadRefs": Array [ + Object { + "type": "client-id", + "value": "user1234", + }, + Object { + "type": "client-id", + "value": "anonId1234", + }, + Object { + "type": "email", + "value": "peter@example.com", + }, + ], + "timestamp": 1671371213632, + "type": "tracking", + "url": "https://segment.com/academy/", + "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1", + "values": Object { + "email": "peter@example.com", + }, +} +`; diff --git a/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/__tests__/index.test.ts b/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/__tests__/index.test.ts new file mode 100644 index 0000000000..e77259a25b --- /dev/null +++ b/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/__tests__/index.test.ts @@ -0,0 +1,135 @@ +import { createTestEvent } from '@segment/actions-core' +import { expectedTs, testAction, testBatchAction, userAgent } from '../../testing' + +const actionName = 'submitIdentifyEvent' + +describe('SalesWings', () => { + describe(actionName, () => { + it('should submit event on Identify event', async () => { + const event = createTestEvent({ + type: 'identify', + traits: { + name: 'Peter Gibbons', + email: 'peter@example.com', + plan: 'premium', + logins: 5 + }, + context: { + userAgent, + page: { + url: 'https://example.com', + referrer: 'https://example.com/other' + } + } + }) + const request = await testAction(actionName, event) + expect(request).toMatchObject({ + type: 'tracking', + leadRefs: [ + { type: 'client-id', value: event.userId }, + { type: 'client-id', value: event.anonymousId }, + { type: 'email', value: event.traits?.email } + ], + kind: 'Identify', + data: 'peter@example.com', + url: 'https://example.com', + referrerUrl: 'https://example.com/other', + userAgent, + timestamp: expectedTs(event.timestamp), + values: { + name: 'Peter Gibbons', + plan: 'premium', + logins: 5 + } + }) + }) + + it('should submit event on Identify event with all optional fields omitted', async () => { + const event = createTestEvent({ + type: 'identify', + traits: { + email: 'peter@example.com' + } + }) + const request = await testAction(actionName, event) + expect(request).toMatchObject({ + type: 'tracking', + leadRefs: [ + { type: 'client-id', value: event.userId }, + { type: 'client-id', value: event.anonymousId }, + { type: 'email', value: event.traits?.email } + ], + kind: 'Identify', + data: 'peter@example.com', + timestamp: expectedTs(event.timestamp), + values: {} + }) + }) + + it('should not skip an event without ids', async () => { + const event = createTestEvent({ + type: 'identify', + traits: { + email: 'peter@example.com' + }, + userId: undefined, + anonymousId: undefined + }) + const request = await testAction(actionName, event) + expect(request).toMatchObject({ + type: 'tracking', + leadRefs: [{ type: 'email', value: event.traits?.email }], + kind: 'Identify', + data: 'peter@example.com', + timestamp: expectedTs(event.timestamp), + values: {} + }) + }) + + it('should submit event batch', async () => { + const events = [ + createTestEvent({ + type: 'identify', + traits: { + email: 'peter@example.com' + } + }), + createTestEvent({ + type: 'identify', + traits: { + email: 'frank@example.com' + } + }) + ] + const request = await testBatchAction(actionName, events) + expect(request).toMatchObject({ + events: [ + { + type: 'tracking', + leadRefs: [ + { type: 'client-id', value: events[0].userId }, + { type: 'client-id', value: events[0].anonymousId }, + { type: 'email', value: events[0].traits?.email } + ], + kind: 'Identify', + data: 'peter@example.com', + timestamp: expectedTs(events[0].timestamp), + values: {} + }, + { + type: 'tracking', + leadRefs: [ + { type: 'client-id', value: events[1].userId }, + { type: 'client-id', value: events[1].anonymousId }, + { type: 'email', value: events[1].traits?.email } + ], + kind: 'Identify', + data: 'frank@example.com', + timestamp: expectedTs(events[1].timestamp), + values: {} + } + ] + }) + }) + }) +}) diff --git a/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/__tests__/snapshot.test.ts new file mode 100644 index 0000000000..362ef386b8 --- /dev/null +++ b/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/__tests__/snapshot.test.ts @@ -0,0 +1,83 @@ +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import { generateTestData } from '../../../../lib/test-data' +import destination from '../../index' +import nock from 'nock' + +const testDestination = createTestIntegration(destination) +const actionSlug = 'submitIdentifyEvent' +const destinationSlug = '' +const seedName = `${destinationSlug}#${actionSlug}` + +describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination action:`, () => { + it('required fields', async () => { + const timestamp = new Date(1671371213632).toISOString() + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, true) + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + properties: eventData, + timestamp, + traits: { + email: 'peter@example.com' + } + }) + + const responses = await testDestination.testAction(actionSlug, { + event: event, + settings: settingsData, + useDefaultMappings: true + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + + expect(request.headers).toMatchSnapshot() + }) + + it('all fields', async () => { + const timestamp = new Date(1671371213652).toISOString() + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, false) + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + properties: eventData, + timestamp, + traits: { + email: 'peter@example.com' + } + }) + + const responses = await testDestination.testAction(actionSlug, { + event: event, + settings: settingsData, + useDefaultMappings: true + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + }) +}) diff --git a/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/generated-types.ts b/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/generated-types.ts new file mode 100644 index 0000000000..822f72a4f6 --- /dev/null +++ b/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/generated-types.ts @@ -0,0 +1,46 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * Type of the SalesWings custom event (a custom event is visualized in SalesWings cockpit and SalesForce Lead Intent View as "[[Kind]] Data"). + */ + kind: string + /** + * String description of the SalesWings custom event payload (a custom event is visualized in SalesWings cockpit and SalesForce Lead Intent View as "[[Kind]] Data"). + */ + data: string + /** + * Permanent identifier of a Segment user the event is attributed to. + */ + userId?: string + /** + * A pseudo-unique substitute for a Segment user ID the event is attributed to. + */ + anonymousId?: string + /** + * Identified email of the Segment User. + */ + email: string + /** + * URL associated with the event. + */ + url?: string + /** + * Referrer URL associated with the event. + */ + referrerUrl?: string + /** + * User Agent associated with the event. + */ + userAgent?: string + /** + * When the event was sent. + */ + timestamp?: string | number + /** + * Custom attribute values associated with the SalesWings custom event. + */ + values?: { + [k: string]: unknown + } +} diff --git a/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/index.ts b/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/index.ts new file mode 100644 index 0000000000..cbbe40bff2 --- /dev/null +++ b/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/index.ts @@ -0,0 +1,43 @@ +import type { ActionDefinition } from '@segment/actions-core' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' +import { Event, TrackingEvent } from '../api' +import { userId, anonymousId, email, url, referrerUrl, userAgent, timestamp, kind, data, values } from '../fields' +import { convertLeadRefs, convertValues, convertTimestamp } from '../converter' +import { perform, performBatch } from '../common' + +const convertEvent = (payload: Payload): Event | undefined => { + return new TrackingEvent({ + leadRefs: convertLeadRefs(payload), + kind: payload.kind, + data: payload.data, + url: payload.url, + referrerUrl: payload.referrerUrl, + userAgent: payload.userAgent, + timestamp: convertTimestamp(payload.timestamp), + values: convertValues(payload.values) + }) +} + +const action: ActionDefinition = { + title: 'Submit Identify Event', + description: + 'Send your Segment Identify events to SalesWings to use them for tagging, scoring and prioritising your leads.', + defaultSubscription: 'type = "identify"', + fields: { + kind: kind('Identify'), + data: data({ '@path': '$.traits.email' }), + userId, + anonymousId, + email: { ...email, required: true }, + url, + referrerUrl, + userAgent, + timestamp, + values: values({ '@path': '$.traits' }) + }, + perform: perform(convertEvent), + performBatch: performBatch(convertEvent) +} + +export default action diff --git a/packages/destination-actions/src/destinations/saleswings/submitPageEvent/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/saleswings/submitPageEvent/__tests__/__snapshots__/snapshot.test.ts.snap new file mode 100644 index 0000000000..cfd485649b --- /dev/null +++ b/packages/destination-actions/src/destinations/saleswings/submitPageEvent/__tests__/__snapshots__/snapshot.test.ts.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Testing snapshot for 's submitPageEvent destination action: all fields 1`] = ` +Object { + "leadRefs": Array [ + Object { + "type": "client-id", + "value": "user1234", + }, + Object { + "type": "client-id", + "value": "anonId1234", + }, + ], + "timestamp": 1671371213652, + "type": "page-visit", + "url": "[isj*b()C", + "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1", +} +`; + +exports[`Testing snapshot for 's submitPageEvent destination action: required fields 1`] = ` +Object { + "leadRefs": Array [ + Object { + "type": "client-id", + "value": "user1234", + }, + Object { + "type": "client-id", + "value": "anonId1234", + }, + ], + "timestamp": 1671371213632, + "type": "page-visit", + "url": "[isj*b()C", + "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1", +} +`; diff --git a/packages/destination-actions/src/destinations/saleswings/submitPageEvent/__tests__/index.test.ts b/packages/destination-actions/src/destinations/saleswings/submitPageEvent/__tests__/index.test.ts new file mode 100644 index 0000000000..e045b3d1c3 --- /dev/null +++ b/packages/destination-actions/src/destinations/saleswings/submitPageEvent/__tests__/index.test.ts @@ -0,0 +1,138 @@ +import { createTestEvent } from '@segment/actions-core' +import { expectedTs, testAction, testActionWithSkippedEvent, testBatchAction, userAgent } from '../../testing' + +const actionName = 'submitPageEvent' + +describe('SalesWings', () => { + describe(actionName, () => { + it('should submit event on Page event', async () => { + const event = createTestEvent({ + type: 'page', + properties: { + url: 'https://example.com', + referrer: 'https://example.com/other' + }, + context: { + userAgent + } + }) + const request = await testAction(actionName, event) + expect(request).toMatchObject({ + type: 'page-visit', + leadRefs: [ + { type: 'client-id', value: event.userId }, + { type: 'client-id', value: event.anonymousId } + ], + url: 'https://example.com', + referrerUrl: 'https://example.com/other', + userAgent, + timestamp: expectedTs(event.timestamp) + }) + }) + + it('should submit event on Page event with all optional fields omitted', async () => { + const event = createTestEvent({ + type: 'page', + properties: { + url: 'https://example.com' + } + }) + const request = await testAction(actionName, event) + expect(request).toMatchObject({ + type: 'page-visit', + leadRefs: [ + { type: 'client-id', value: event.userId }, + { type: 'client-id', value: event.anonymousId } + ], + url: 'https://example.com', + timestamp: expectedTs(event.timestamp) + }) + }) + + it('should not skip an event with userId only', async () => { + const event = createTestEvent({ + type: 'page', + properties: { + url: 'https://example.com' + }, + anonymousId: undefined + }) + const request = await testAction(actionName, event) + expect(request).toMatchObject({ + type: 'page-visit', + leadRefs: [{ type: 'client-id', value: event.userId }], + url: 'https://example.com', + timestamp: expectedTs(event.timestamp) + }) + }) + + it('should not skip an event with anonymousId only', async () => { + const event = createTestEvent({ + type: 'page', + properties: { + url: 'https://example.com' + }, + userId: undefined + }) + const request = await testAction(actionName, event) + expect(request).toMatchObject({ + type: 'page-visit', + leadRefs: [{ type: 'client-id', value: event.anonymousId }], + url: 'https://example.com', + timestamp: expectedTs(event.timestamp) + }) + }) + + it('should skip an event without any ids', async () => { + const event = createTestEvent({ + type: 'page', + properties: { + url: 'https://example.com' + }, + anonymousId: undefined, + userId: undefined + }) + await testActionWithSkippedEvent(actionName, event) + }) + + it('should submit event batch', async () => { + const events = [ + createTestEvent({ + type: 'page', + context: { + page: { url: 'https://example.com/01' } + } + }), + createTestEvent({ + type: 'page', + context: { + page: { url: 'https://example.com/02' } + } + }) + ] + const request = await testBatchAction(actionName, events) + expect(request).toMatchObject({ + events: [ + { + type: 'page-visit', + leadRefs: [ + { type: 'client-id', value: events[0].userId }, + { type: 'client-id', value: events[0].anonymousId } + ], + url: 'https://example.com/01', + timestamp: expectedTs(events[0].timestamp) + }, + { + type: 'page-visit', + leadRefs: [ + { type: 'client-id', value: events[0].userId }, + { type: 'client-id', value: events[0].anonymousId } + ], + url: 'https://example.com/02', + timestamp: expectedTs(events[1].timestamp) + } + ] + }) + }) + }) +}) diff --git a/packages/destination-actions/src/destinations/saleswings/submitPageEvent/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/saleswings/submitPageEvent/__tests__/snapshot.test.ts new file mode 100644 index 0000000000..372728cbba --- /dev/null +++ b/packages/destination-actions/src/destinations/saleswings/submitPageEvent/__tests__/snapshot.test.ts @@ -0,0 +1,77 @@ +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import { generateTestData } from '../../../../lib/test-data' +import destination from '../../index' +import nock from 'nock' + +const testDestination = createTestIntegration(destination) +const actionSlug = 'submitPageEvent' +const destinationSlug = '' +const seedName = `${destinationSlug}#${actionSlug}` + +describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination action:`, () => { + it('required fields', async () => { + const timestamp = new Date(1671371213632).toISOString() + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, true) + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + properties: eventData, + timestamp + }) + + const responses = await testDestination.testAction(actionSlug, { + event: event, + settings: settingsData, + useDefaultMappings: true + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + + expect(request.headers).toMatchSnapshot() + }) + + it('all fields', async () => { + const timestamp = new Date(1671371213652).toISOString() + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, false) + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + properties: eventData, + timestamp + }) + + const responses = await testDestination.testAction(actionSlug, { + event: event, + settings: settingsData, + useDefaultMappings: true + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + }) +}) diff --git a/packages/destination-actions/src/destinations/saleswings/submitPageEvent/generated-types.ts b/packages/destination-actions/src/destinations/saleswings/submitPageEvent/generated-types.ts new file mode 100644 index 0000000000..8584bdebe2 --- /dev/null +++ b/packages/destination-actions/src/destinations/saleswings/submitPageEvent/generated-types.ts @@ -0,0 +1,28 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * Permanent identifier of a Segment user the event is attributed to. + */ + userId?: string + /** + * A pseudo-unique substitute for a Segment user ID the event is attributed to. + */ + anonymousId?: string + /** + * URL associated with the event. + */ + url: string + /** + * Referrer URL associated with the event. + */ + referrerUrl?: string + /** + * User Agent associated with the event. + */ + userAgent?: string + /** + * When the event was sent. + */ + timestamp?: string | number +} diff --git a/packages/destination-actions/src/destinations/saleswings/submitPageEvent/index.ts b/packages/destination-actions/src/destinations/saleswings/submitPageEvent/index.ts new file mode 100644 index 0000000000..3b9d0c134a --- /dev/null +++ b/packages/destination-actions/src/destinations/saleswings/submitPageEvent/index.ts @@ -0,0 +1,38 @@ +import type { ActionDefinition } from '@segment/actions-core' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' +import { Event, PageVisitEvent } from '../api' +import { userId, anonymousId, url, referrerUrl, userAgent, timestamp } from '../fields' +import { convertLeadRefs, convertTimestamp } from '../converter' +import { perform, performBatch } from '../common' + +const convertEvent = (payload: Payload): Event | undefined => { + const leadRefs = convertLeadRefs(payload) + if (leadRefs.length == 0) return undefined + return new PageVisitEvent({ + leadRefs, + url: payload.url, + referrerUrl: payload.referrerUrl, + userAgent: payload.userAgent, + timestamp: convertTimestamp(payload.timestamp) + }) +} + +const action: ActionDefinition = { + title: 'Submit Page Event', + description: + 'Send your Segment Page events to SalesWings to use them for tagging, scoring and prioritising your leads.', + defaultSubscription: 'type = "page"', + fields: { + userId, + anonymousId, + url: { ...url, required: true }, + referrerUrl, + userAgent, + timestamp + }, + perform: perform(convertEvent), + performBatch: performBatch(convertEvent) +} + +export default action diff --git a/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/__tests__/__snapshots__/snapshot.test.ts.snap new file mode 100644 index 0000000000..9b92654bcd --- /dev/null +++ b/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/__tests__/__snapshots__/snapshot.test.ts.snap @@ -0,0 +1,62 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Testing snapshot for 's submitScreenEvent destination action: all fields 1`] = ` +Object { + "data": "Home", + "kind": "Screen", + "leadRefs": Array [ + Object { + "type": "client-id", + "value": "user1234", + }, + Object { + "type": "client-id", + "value": "anonId1234", + }, + Object { + "type": "email", + "value": "sajvoji@monarwi.pk", + }, + ], + "timestamp": 1671371213652, + "type": "tracking", + "url": "kwF&9l73A", + "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1", + "values": Object { + "anonymousId": "kwF&9l73A", + "data": "kwF&9l73A", + "email": "sajvoji@monarwi.pk", + "kind": "kwF&9l73A", + "referrerUrl": "kwF&9l73A", + "timestamp": "2021-02-01T00:00:00.000Z", + "url": "kwF&9l73A", + "userAgent": "kwF&9l73A", + "userId": "kwF&9l73A", + }, +} +`; + +exports[`Testing snapshot for 's submitScreenEvent destination action: required fields 1`] = ` +Object { + "data": "Home", + "kind": "Screen", + "leadRefs": Array [ + Object { + "type": "client-id", + "value": "user1234", + }, + Object { + "type": "client-id", + "value": "anonId1234", + }, + ], + "timestamp": 1671371213632, + "type": "tracking", + "url": "https://segment.com/academy/", + "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1", + "values": Object { + "data": "kwF&9l73A", + "kind": "kwF&9l73A", + }, +} +`; diff --git a/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/__tests__/index.test.ts b/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/__tests__/index.test.ts new file mode 100644 index 0000000000..2d440e8465 --- /dev/null +++ b/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/__tests__/index.test.ts @@ -0,0 +1,135 @@ +import { createTestEvent } from '@segment/actions-core' +import { expectedTs, testAction, testBatchAction, userAgent } from '../../testing' + +const actionName = 'submitScreenEvent' + +describe('SalesWings', () => { + describe(actionName, () => { + it('should submit event on Screen event', async () => { + const event = createTestEvent({ + type: 'screen', + name: 'Home', + properties: { + 'Feed Type': 'private' + }, + context: { + userAgent, + page: { + url: 'https://example.com', + referrer: 'https://example.com/other' + } + } + }) + const request = await testAction(actionName, event) + expect(request).toMatchObject({ + type: 'tracking', + leadRefs: [ + { type: 'client-id', value: event.userId }, + { type: 'client-id', value: event.anonymousId } + ], + kind: 'Screen', + data: 'Home', + url: 'https://example.com', + referrerUrl: 'https://example.com/other', + userAgent, + timestamp: expectedTs(event.timestamp), + values: { + 'Feed Type': 'private' + } + }) + }) + + it('should submit event on Screen event with all optional fields omitted', async () => { + const event = createTestEvent({ + type: 'screen', + name: 'Home' + }) + const request = await testAction(actionName, event) + expect(request).toMatchObject({ + type: 'tracking', + leadRefs: [ + { type: 'client-id', value: event.userId }, + { type: 'client-id', value: event.anonymousId } + ], + kind: 'Screen', + data: 'Home', + timestamp: expectedTs(event.timestamp), + values: {} + }) + }) + + it('should not skip an event with userId only', async () => { + const event = createTestEvent({ + type: 'screen', + name: 'Home', + anonymousId: undefined + }) + const request = await testAction(actionName, event) + expect(request).toMatchObject({ + type: 'tracking', + leadRefs: [{ type: 'client-id', value: event.userId }], + kind: 'Screen', + data: 'Home', + timestamp: expectedTs(event.timestamp), + values: {} + }) + }) + + it('should not skip an event with anonymousId only', async () => { + const event = createTestEvent({ + type: 'screen', + name: 'Home', + userId: undefined + }) + const request = await testAction(actionName, event) + expect(request).toMatchObject({ + type: 'tracking', + leadRefs: [{ type: 'client-id', value: event.anonymousId }], + kind: 'Screen', + data: 'Home', + timestamp: expectedTs(event.timestamp), + values: {} + }) + }) + + it('should submit event batch', async () => { + const events = [ + createTestEvent({ + type: 'screen', + name: 'Home' + }), + createTestEvent({ + type: 'screen', + name: 'Orders' + }) + ] + const request = await testBatchAction(actionName, events) + expect(request).toMatchObject({ + events: [ + { + type: 'tracking', + leadRefs: [ + { type: 'client-id', value: events[0].userId }, + { type: 'client-id', value: events[0].anonymousId } + ], + kind: 'Screen', + data: 'Home', + timestamp: expectedTs(events[0].timestamp), + values: {} + }, + { + type: 'tracking', + leadRefs: [ + { type: 'client-id', value: events[1].userId }, + { type: 'client-id', value: events[1].anonymousId } + ], + kind: 'Screen', + data: 'Orders', + timestamp: expectedTs(events[1].timestamp), + values: {} + } + ] + }) + }) + }) +}) diff --git a/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/__tests__/snapshot.test.ts new file mode 100644 index 0000000000..dd59035a05 --- /dev/null +++ b/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/__tests__/snapshot.test.ts @@ -0,0 +1,79 @@ +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import { generateTestData } from '../../../../lib/test-data' +import destination from '../../index' +import nock from 'nock' + +const testDestination = createTestIntegration(destination) +const actionSlug = 'submitScreenEvent' +const destinationSlug = '' +const seedName = `${destinationSlug}#${actionSlug}` + +describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination action:`, () => { + it('required fields', async () => { + const timestamp = new Date(1671371213632).toISOString() + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, true) + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + properties: eventData, + timestamp, + name: 'Home' + }) + + const responses = await testDestination.testAction(actionSlug, { + event: event, + settings: settingsData, + useDefaultMappings: true + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + + expect(request.headers).toMatchSnapshot() + }) + + it('all fields', async () => { + const timestamp = new Date(1671371213652).toISOString() + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, false) + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + properties: eventData, + timestamp, + name: 'Home' + }) + + const responses = await testDestination.testAction(actionSlug, { + event: event, + settings: settingsData, + useDefaultMappings: true + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + }) +}) diff --git a/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/generated-types.ts b/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/generated-types.ts new file mode 100644 index 0000000000..c06058783e --- /dev/null +++ b/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/generated-types.ts @@ -0,0 +1,46 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * Type of the SalesWings custom event (a custom event is visualized in SalesWings cockpit and SalesForce Lead Intent View as "[[Kind]] Data"). + */ + kind: string + /** + * String description of the SalesWings custom event payload (a custom event is visualized in SalesWings cockpit and SalesForce Lead Intent View as "[[Kind]] Data"). + */ + data: string + /** + * Permanent identifier of a Segment user the event is attributed to. + */ + userId?: string + /** + * A pseudo-unique substitute for a Segment user ID the event is attributed to. + */ + anonymousId?: string + /** + * Identified email of the Segment User. + */ + email?: string + /** + * URL associated with the event. + */ + url?: string + /** + * Referrer URL associated with the event. + */ + referrerUrl?: string + /** + * User Agent associated with the event. + */ + userAgent?: string + /** + * When the event was sent. + */ + timestamp?: string | number + /** + * Custom attribute values associated with the SalesWings custom event. + */ + values?: { + [k: string]: unknown + } +} diff --git a/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/index.ts b/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/index.ts new file mode 100644 index 0000000000..d51d109d66 --- /dev/null +++ b/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/index.ts @@ -0,0 +1,45 @@ +import type { ActionDefinition } from '@segment/actions-core' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' +import { Event, TrackingEvent } from '../api' +import { userId, anonymousId, email, url, referrerUrl, userAgent, timestamp, kind, data, values } from '../fields' +import { convertLeadRefs, convertValues, convertTimestamp } from '../converter' +import { perform, performBatch } from '../common' + +const convertEvent = (payload: Payload): Event | undefined => { + const leadRefs = convertLeadRefs(payload) + if (leadRefs.length == 0) return undefined + return new TrackingEvent({ + leadRefs, + kind: payload.kind, + data: payload.data, + url: payload.url, + referrerUrl: payload.referrerUrl, + userAgent: payload.userAgent, + timestamp: convertTimestamp(payload.timestamp), + values: convertValues(payload.values) + }) +} + +const action: ActionDefinition = { + title: 'Submit Screen Event', + description: + 'Send your Segment Screen events to SalesWings to use them for tagging, scoring and prioritising your leads.', + defaultSubscription: 'type = "screen"', + fields: { + kind: kind('Screen'), + data: data({ '@path': '$.name' }), + userId, + anonymousId, + email, + url, + referrerUrl, + userAgent, + timestamp, + values: values({ '@path': '$.properties' }) + }, + perform: perform(convertEvent), + performBatch: performBatch(convertEvent) +} + +export default action diff --git a/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/__tests__/__snapshots__/snapshot.test.ts.snap new file mode 100644 index 0000000000..e891e21a75 --- /dev/null +++ b/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/__tests__/__snapshots__/snapshot.test.ts.snap @@ -0,0 +1,62 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Testing snapshot for 's submitTrackEvent destination action: all fields 1`] = ` +Object { + "data": "Test Event", + "kind": "Track", + "leadRefs": Array [ + Object { + "type": "client-id", + "value": "user1234", + }, + Object { + "type": "client-id", + "value": "anonId1234", + }, + Object { + "type": "email", + "value": "tukifvus@domemci.li", + }, + ], + "timestamp": 1671371213652, + "type": "tracking", + "url": "K[uAM", + "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1", + "values": Object { + "anonymousId": "K[uAM", + "data": "K[uAM", + "email": "tukifvus@domemci.li", + "kind": "K[uAM", + "referrerUrl": "K[uAM", + "timestamp": "2021-02-01T00:00:00.000Z", + "url": "K[uAM", + "userAgent": "K[uAM", + "userId": "K[uAM", + }, +} +`; + +exports[`Testing snapshot for 's submitTrackEvent destination action: required fields 1`] = ` +Object { + "data": "Test Event", + "kind": "Track", + "leadRefs": Array [ + Object { + "type": "client-id", + "value": "user1234", + }, + Object { + "type": "client-id", + "value": "anonId1234", + }, + ], + "timestamp": 1671371213632, + "type": "tracking", + "url": "https://segment.com/academy/", + "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1", + "values": Object { + "data": "K[uAM", + "kind": "K[uAM", + }, +} +`; diff --git a/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/__tests__/index.test.ts b/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/__tests__/index.test.ts new file mode 100644 index 0000000000..02a7b6dc1d --- /dev/null +++ b/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/__tests__/index.test.ts @@ -0,0 +1,268 @@ +import { createTestEvent } from '@segment/actions-core' +import { + expectedTs, + testAction, + testActionWithSkippedEvent, + testBatchAction, + testBatchActionSkippedEvents, + userAgent +} from '../../testing' + +const actionName = 'submitTrackEvent' + +describe('SalesWings', () => { + describe(actionName, () => { + it('should submit event on Track event', async () => { + const event = createTestEvent({ + type: 'track', + event: 'User Registered', + properties: { + plan: 'Pro Annual', + accountType: 'Facebook' + }, + context: { + userAgent, + page: { + url: 'https://example.com', + referrer: 'https://example.com/other' + } + } + }) + const request = await testAction(actionName, event) + expect(request).toMatchObject({ + type: 'tracking', + leadRefs: [ + { type: 'client-id', value: event.userId }, + { type: 'client-id', value: event.anonymousId } + ], + kind: 'Track', + data: 'User Registered', + url: 'https://example.com', + referrerUrl: 'https://example.com/other', + userAgent, + timestamp: expectedTs(event.timestamp), + values: { + plan: 'Pro Annual', + accountType: 'Facebook' + } + }) + }) + + it('should submit event on Track event with all optional fields omitted', async () => { + const event = createTestEvent({ + type: 'track', + event: 'User Registered' + }) + const request = await testAction(actionName, event) + expect(request).toMatchObject({ + type: 'tracking', + leadRefs: [ + { type: 'client-id', value: event.userId }, + { type: 'client-id', value: event.anonymousId } + ], + kind: 'Track', + data: 'User Registered', + timestamp: expectedTs(event.timestamp), + values: {} + }) + }) + + it('should skip event without any id', async () => { + const event = createTestEvent({ + type: 'track', + event: 'User Registered', + userId: undefined, + anonymousId: undefined + }) + await testActionWithSkippedEvent(actionName, event) + }) + + it('should submit event on Track event with email in properties', async () => { + const event = createTestEvent({ + type: 'track', + event: 'User Registered', + properties: { + email: 'peter@example.com' + } + }) + const request = await testAction(actionName, event) + expect(request).toMatchObject({ + type: 'tracking', + leadRefs: [ + { type: 'client-id', value: event.userId }, + { type: 'client-id', value: event.anonymousId }, + { type: 'email', value: 'peter@example.com' } + ], + kind: 'Track', + data: 'User Registered', + timestamp: expectedTs(event.timestamp), + values: {} + }) + }) + + it('should submit event on Track event with email in properties and without ids', async () => { + const event = createTestEvent({ + type: 'track', + event: 'User Registered', + properties: { + email: 'peter@example.com' + }, + userId: undefined, + anonymousId: undefined + }) + const request = await testAction(actionName, event) + expect(request).toMatchObject({ + type: 'tracking', + leadRefs: [{ type: 'email', value: 'peter@example.com' }], + kind: 'Track', + data: 'User Registered', + timestamp: expectedTs(event.timestamp), + values: {} + }) + }) + + it('should not skip an event with userId only', async () => { + const event = createTestEvent({ + type: 'track', + event: 'User Registered', + anonymousId: undefined + }) + const request = await testAction(actionName, event) + expect(request).toMatchObject({ + type: 'tracking', + leadRefs: [{ type: 'client-id', value: event.userId }], + kind: 'Track', + data: 'User Registered', + timestamp: expectedTs(event.timestamp), + values: {} + }) + }) + + it('should not skip an event with anonymousId only', async () => { + const event = createTestEvent({ + type: 'track', + event: 'User Registered', + userId: undefined + }) + const request = await testAction(actionName, event) + expect(request).toMatchObject({ + type: 'tracking', + leadRefs: [{ type: 'client-id', value: event.anonymousId }], + kind: 'Track', + data: 'User Registered', + timestamp: expectedTs(event.timestamp), + values: {} + }) + }) + + it('should submit event batch', async () => { + const events = [ + createTestEvent({ + type: 'track', + event: 'User Registered' + }), + createTestEvent({ + type: 'track', + event: 'Order Completed' + }) + ] + const request = await testBatchAction(actionName, events) + expect(request).toMatchObject({ + events: [ + { + type: 'tracking', + leadRefs: [ + { type: 'client-id', value: events[0].userId }, + { type: 'client-id', value: events[0].anonymousId } + ], + kind: 'Track', + data: 'User Registered', + timestamp: expectedTs(events[0].timestamp), + values: {} + }, + { + type: 'tracking', + leadRefs: [ + { type: 'client-id', value: events[1].userId }, + { type: 'client-id', value: events[1].anonymousId } + ], + kind: 'Track', + data: 'Order Completed', + timestamp: expectedTs(events[0].timestamp), + values: {} + } + ] + }) + }) + + it('should not include skippable events into a batch', async () => { + const events = [ + createTestEvent({ + type: 'track', + event: 'User Registered' + }), + createTestEvent({ + type: 'track', + event: 'Order Purchased' + }), + createTestEvent({ + type: 'track', + event: 'Cart Abandonned', + userId: undefined, + anonymousId: undefined + }) + ] + const request = await testBatchAction(actionName, events) + expect(request).toMatchObject({ + events: [ + { + type: 'tracking', + leadRefs: [ + { type: 'client-id', value: events[0].userId }, + { type: 'client-id', value: events[0].anonymousId } + ], + kind: 'Track', + data: 'User Registered', + timestamp: expectedTs(events[0].timestamp), + values: {} + }, + { + type: 'tracking', + leadRefs: [ + { type: 'client-id', value: events[1].userId }, + { type: 'client-id', value: events[1].anonymousId } + ], + kind: 'Track', + data: 'Order Purchased', + timestamp: expectedTs(events[0].timestamp), + values: {} + } + ] + }) + }) + + it('should not submit a batch if all the events are skippable', async () => { + const events = [ + createTestEvent({ + type: 'track', + event: 'User Registered', + userId: undefined, + anonymousId: undefined + }), + createTestEvent({ + type: 'track', + event: 'Order Purchased', + userId: undefined, + anonymousId: undefined + }), + createTestEvent({ + type: 'track', + event: 'Cart Abandonned', + userId: undefined, + anonymousId: undefined + }) + ] + await testBatchActionSkippedEvents(actionName, events) + }) + }) +}) diff --git a/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/__tests__/snapshot.test.ts new file mode 100644 index 0000000000..11acbfc86b --- /dev/null +++ b/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/__tests__/snapshot.test.ts @@ -0,0 +1,77 @@ +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import { generateTestData } from '../../../../lib/test-data' +import destination from '../../index' +import nock from 'nock' + +const testDestination = createTestIntegration(destination) +const actionSlug = 'submitTrackEvent' +const destinationSlug = '' +const seedName = `${destinationSlug}#${actionSlug}` + +describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination action:`, () => { + it('required fields', async () => { + const timestamp = new Date(1671371213632).toISOString() + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, true) + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + properties: eventData, + timestamp + }) + + const responses = await testDestination.testAction(actionSlug, { + event: event, + settings: settingsData, + useDefaultMappings: true + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + + expect(request.headers).toMatchSnapshot() + }) + + it('all fields', async () => { + const timestamp = new Date(1671371213652).toISOString() + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, false) + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + properties: eventData, + timestamp + }) + + const responses = await testDestination.testAction(actionSlug, { + event: event, + settings: settingsData, + useDefaultMappings: true + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + }) +}) diff --git a/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/generated-types.ts b/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/generated-types.ts new file mode 100644 index 0000000000..c06058783e --- /dev/null +++ b/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/generated-types.ts @@ -0,0 +1,46 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * Type of the SalesWings custom event (a custom event is visualized in SalesWings cockpit and SalesForce Lead Intent View as "[[Kind]] Data"). + */ + kind: string + /** + * String description of the SalesWings custom event payload (a custom event is visualized in SalesWings cockpit and SalesForce Lead Intent View as "[[Kind]] Data"). + */ + data: string + /** + * Permanent identifier of a Segment user the event is attributed to. + */ + userId?: string + /** + * A pseudo-unique substitute for a Segment user ID the event is attributed to. + */ + anonymousId?: string + /** + * Identified email of the Segment User. + */ + email?: string + /** + * URL associated with the event. + */ + url?: string + /** + * Referrer URL associated with the event. + */ + referrerUrl?: string + /** + * User Agent associated with the event. + */ + userAgent?: string + /** + * When the event was sent. + */ + timestamp?: string | number + /** + * Custom attribute values associated with the SalesWings custom event. + */ + values?: { + [k: string]: unknown + } +} diff --git a/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/index.ts b/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/index.ts new file mode 100644 index 0000000000..6779715421 --- /dev/null +++ b/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/index.ts @@ -0,0 +1,45 @@ +import type { ActionDefinition } from '@segment/actions-core' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' +import { Event, TrackingEvent } from '../api' +import { userId, anonymousId, email, url, referrerUrl, userAgent, timestamp, kind, data, values } from '../fields' +import { convertLeadRefs, convertValues, convertTimestamp } from '../converter' +import { perform, performBatch } from '../common' + +const convertEvent = (payload: Payload): Event | undefined => { + const leadRefs = convertLeadRefs(payload) + if (leadRefs.length == 0) return undefined + return new TrackingEvent({ + leadRefs, + kind: payload.kind, + data: payload.data, + url: payload.url, + referrerUrl: payload.referrerUrl, + userAgent: payload.userAgent, + timestamp: convertTimestamp(payload.timestamp), + values: convertValues(payload.values) + }) +} + +const action: ActionDefinition = { + title: 'Submit Track Event', + description: + 'Send your Segment Track events to SalesWings to use them for tagging, scoring and prioritising your leads.', + defaultSubscription: 'type = "track"', + fields: { + kind: kind('Track'), + data: data({ '@path': '$.event' }), + userId, + anonymousId, + email, + url, + referrerUrl, + userAgent, + timestamp, + values: values({ '@path': '$.properties' }) + }, + perform: perform(convertEvent), + performBatch: performBatch(convertEvent) +} + +export default action diff --git a/packages/destination-actions/src/destinations/saleswings/testing.ts b/packages/destination-actions/src/destinations/saleswings/testing.ts new file mode 100644 index 0000000000..d6e1991f9b --- /dev/null +++ b/packages/destination-actions/src/destinations/saleswings/testing.ts @@ -0,0 +1,49 @@ +import { createTestIntegration, SegmentEvent } from '@segment/actions-core' +import nock from 'nock' +import { apiBaseUrl } from './api' +import destination from './index' + +const testDestination = createTestIntegration(destination) + +export const settings = { apiKey: 'TEST_API_KEY' } + +export const testAction = async (actionName: string, event: SegmentEvent): Promise => { + nock(apiBaseUrl).post('/events').reply(200, {}) + const input = { event, settings, useDefaultMappings: true } + const responses = await testDestination.testAction(actionName, input) + expect(responses.length).toBe(1) + const request = responses[0].request + expect(request.headers.get('Authorization')).toBe(`Bearer ${settings.apiKey}`) + const rawBody = await request.text() + return JSON.parse(rawBody) +} + +export const testBatchAction = async (actionName: string, events: SegmentEvent[]): Promise => { + nock(apiBaseUrl).post('/events/batches').reply(200, {}) + const input = { events, settings, useDefaultMappings: true } + const responses = await testDestination.testBatchAction(actionName, input) + expect(responses.length).toBe(1) + const request = responses[0].request + expect(request.headers.get('Authorization')).toBe(`Bearer ${settings.apiKey}`) + const rawBody = await request.text() + return JSON.parse(rawBody) +} + +export const testActionWithSkippedEvent = async (actionName: string, event: SegmentEvent): Promise => { + const responses = await testDestination.testAction(actionName, { event, settings, useDefaultMappings: true }) + expect(responses.length).toBe(0) +} + +export const testBatchActionSkippedEvents = async (actionName: string, events: SegmentEvent[]): Promise => { + const responses = await testDestination.testBatchAction(actionName, { events, settings, useDefaultMappings: true }) + expect(responses.length).toBe(0) +} + +export const expectedTs = (segmentEventTs: string | Date | undefined): number => { + if (segmentEventTs === undefined) throw new Error('Unexpected state: test event created without a timestamp') + else if (typeof segmentEventTs === 'string') return Date.parse(segmentEventTs) + else return segmentEventTs.valueOf() +} + +export const userAgent = + '"Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1' diff --git a/packages/destination-actions/src/destinations/twilio-studio/__tests__/triggerStudioFlow.test.ts b/packages/destination-actions/src/destinations/twilio-studio/__tests__/triggerStudioFlow.test.ts new file mode 100644 index 0000000000..9493e38448 --- /dev/null +++ b/packages/destination-actions/src/destinations/twilio-studio/__tests__/triggerStudioFlow.test.ts @@ -0,0 +1,204 @@ +import nock from 'nock' +import { createTestIntegration, createTestEvent } from '@segment/actions-core' +import Studio from '../index' + +const studio = createTestIntegration(Studio) + +describe('Twilio Studio', () => { + const settings = { + accountSid: 'a', + authToken: 'b', + spaceId: 'c', + profileApiAccessToken: 'd' + } + + const defaultMapping = { + flowSid: 'FW76e00a0d69a30e38e5cd25fdf887f62c', + from: '+17707624774', + coolingOffPeriod: 60, + userId: 'jane', + eventType: 'identify' + } + + beforeEach(() => { + nock(`https://profiles.segment.com/v1/spaces/c/collections/users/profiles/user_id:jane`) + .get('/external_ids?include=phone') + .reply(200, { + data: [ + { + id: '+918384907416', + type: 'phone', + source_id: 'pA3bgWXL8RC2s89TqqZtPW', + collection: 'users', + created_at: '2023-01-05T15:01:00.367201348Z', + encoding: 'none' + }, + { + id: '+918384907415', + type: 'phone', + source_id: 'pA3bgWXL8RC2s89TqqZtPW', + collection: 'users', + created_at: '2023-01-05T15:00:00.984939139Z', + encoding: 'none' + }, + { + id: '+918384907414', + type: 'phone', + source_id: 'pA3bgWXL8RC2s89TqqZtPW', + collection: 'users', + created_at: '2023-01-04T06:26:39.586351454Z', + encoding: 'none' + } + ] + }) + }) + + afterEach(() => { + studio.responses = [] + nock.cleanAll() + }) + + it('should abort when there is a Profile API fetch failure', async () => { + nock('https://studio.twilio.com').post(`/v2/Flows/${defaultMapping.flowSid}/Executions`).reply(200, {}) + nock(`https://profiles.segment.com/v1/spaces/c/collections/users/profiles/user_id:jane`) + .get('/external_ids?include=phone') + .reply(400, {}) + const event = createTestEvent({ + type: 'identify' + }) + try { + await studio.testAction('triggerStudioFlow', { + event, + settings, + mapping: defaultMapping + }) + } catch (err) { + expect(err).toHaveProperty('code', 'Profile fetch API failure') + } + }) + + it('should abort when there is no `phone` external ID returned from Profile API', async () => { + nock('https://studio.twilio.com').post(`/v2/Flows/${defaultMapping.flowSid}/Executions`).reply(200, {}) + nock(`https://profiles.segment.com/v1/spaces/c/collections/users/profiles/user_id:jane`) + .get('/external_ids?include=phone') + .reply(200, { data: [] }) + const event = createTestEvent({ + type: 'identify' + }) + try { + await studio.testAction('triggerStudioFlow', { + event, + settings, + mapping: defaultMapping + }) + } catch (err) { + expect(err).toHaveProperty('code', 'Trigger Studio Flow no contact address found failure') + } + }) + + it('should abort when there is no `userId` in the event payload', async () => { + nock('https://studio.twilio.com').post(`/v2/Flows/${defaultMapping.flowSid}/Executions`).reply(200, {}) + nock(`https://profiles.segment.com/v1/spaces/c/collections/users/profiles/user_id:jane`) + .get('/external_ids?include=phone') + .reply(200, { data: [] }) + const event = createTestEvent({ + type: 'identify' + }) + try { + await studio.testAction('triggerStudioFlow', { + event, + settings, + mapping: { ...defaultMapping, userId: '' } + }) + } catch (err) { + expect(err).toHaveProperty('code', 'No userId found in the Segment Event') + } + }) + + it('should abort when cooldown period is going on', async () => { + nock('https://studio.twilio.com').post(`/v2/Flows/${defaultMapping.flowSid}/Executions`).reply(200, {}) + const event = createTestEvent({ + type: 'identify' + }) + try { + await studio.testAction('triggerStudioFlow', { + event, + settings, + mapping: defaultMapping, + stateContext: { + getRequestContext: (_key: string): any => 'FW76e00a0d69a30e38e5cd25fdf887f62cjane', + setResponseContext: ( + _key: string, + _value: string, + _ttl: { hour?: number; minute?: number; second?: number } + ): void => {} + } + }) + } catch (err) { + expect(err).toHaveProperty('code', 'Cooling off Period') + } + }) + + it('should trigger a flow', async () => { + const studioRequest = nock('https://studio.twilio.com') + .post(`/v2/Flows/${defaultMapping.flowSid}/Executions`) + .reply(200, {}) + const event = createTestEvent({ + type: 'identify' + }) + const actionInputData = { + event, + settings, + mapping: defaultMapping, + stateContext: { + getRequestContext: (_key: string): any => '', + setResponseContext: ( + _key: string, + _value: string, + _ttl: { hour?: number; minute?: number; second?: number } + ): void => {} + } + } + + const responses = await studio.testAction('triggerStudioFlow', actionInputData) + expect(responses[1].status).toBe(200) + expect(responses[1].url).toBe('https://studio.twilio.com/v2/Flows/FW76e00a0d69a30e38e5cd25fdf887f62c/Executions') + expect(studioRequest.isDone()).toEqual(true) + }) + + it('should fail to trigger a flow when Studio execution API fails', async () => { + nock('https://studio.twilio.com').post(`/v2/Flows/${defaultMapping.flowSid}/Executions`).reply(400, { + code: 20001, + message: 'Missing required parameter To in the post body', + more_info: 'https://www.twilio.com/docs/errors/20001', + status: 400 + }) + const event = createTestEvent({ + type: 'identify' + }) + const actionInputData = { + event, + settings, + mapping: defaultMapping, + stateContext: { + getRequestContext: (_key: string): any => '', + setResponseContext: ( + _key: string, + _value: string, + _ttl: { hour?: number; minute?: number; second?: number } + ): void => {} + } + } + + try { + await studio.testAction('triggerStudioFlow', actionInputData) + } catch (err) { + expect(err).toHaveProperty('status', 400) + expect(err).toHaveProperty('code', 'Twilio Error Code: 20001') + expect(err).toHaveProperty( + 'message', + 'Unable to trigger Studio Flow. Missing required parameter To in the post body' + ) + } + }) +}) diff --git a/packages/destination-actions/src/destinations/twilio-studio/generated-types.ts b/packages/destination-actions/src/destinations/twilio-studio/generated-types.ts new file mode 100644 index 0000000000..7d36b7e27e --- /dev/null +++ b/packages/destination-actions/src/destinations/twilio-studio/generated-types.ts @@ -0,0 +1,20 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Settings { + /** + * Your Twilio Account SID, starting with AC. You can find this in the Account Info section of your dashboard in the [Twilio Console](https://www.twilio.com/console). + */ + accountSid: string + /** + * Your Twilio Auth Token. You can find this in the Account Info section of your dashboard in the [Twilio Console](https://www.twilio.com/console). + */ + authToken: string + /** + * Your Segment Space ID. + */ + spaceId: string + /** + * Your Segment Profile API Access Token. + */ + profileApiAccessToken: string +} diff --git a/packages/destination-actions/src/destinations/twilio-studio/index.ts b/packages/destination-actions/src/destinations/twilio-studio/index.ts new file mode 100644 index 0000000000..cfc0557556 --- /dev/null +++ b/packages/destination-actions/src/destinations/twilio-studio/index.ts @@ -0,0 +1,59 @@ +import type { DestinationDefinition } from '@segment/actions-core' +import type { Settings } from './generated-types' + +import triggerStudioFlow from './triggerStudioFlow' + +const destination: DestinationDefinition = { + name: 'Twilio Studio', + slug: 'actions-twilio-studio', + mode: 'cloud', + + authentication: { + scheme: 'basic', + fields: { + accountSid: { + label: 'Account SID', + description: + 'Your Twilio Account SID, starting with AC. You can find this in the Account Info section of your dashboard in the [Twilio Console](https://www.twilio.com/console).', + type: 'string', + required: true + }, + authToken: { + label: 'Auth Token', + description: + 'Your Twilio Auth Token. You can find this in the Account Info section of your dashboard in the [Twilio Console](https://www.twilio.com/console).', + type: 'password', + required: true + }, + spaceId: { + label: 'Space ID', + description: 'Your Segment Space ID.', + type: 'string', + required: true + }, + profileApiAccessToken: { + label: 'Profile API Access Token', + description: 'Your Segment Profile API Access Token.', + type: 'password', + required: true + } + }, + testAuthentication: (request) => { + return request('https://api.twilio.com/2010-04-01/Accounts') + } + }, + + extendRequest({ settings }) { + return { + headers: { + authorization: `Basic ${Buffer.from(`${settings.accountSid}:${settings.authToken}`).toString('base64')}` + } + } + }, + + actions: { + triggerStudioFlow + } +} + +export default destination diff --git a/packages/destination-actions/src/destinations/twilio-studio/triggerStudioFlow/generated-types.ts b/packages/destination-actions/src/destinations/twilio-studio/triggerStudioFlow/generated-types.ts new file mode 100644 index 0000000000..bae8bdd561 --- /dev/null +++ b/packages/destination-actions/src/destinations/twilio-studio/triggerStudioFlow/generated-types.ts @@ -0,0 +1,28 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * The Flow SID, starting with FW, for the Studio Flow to trigger. + */ + flowSid: string + /** + * The Twilio phone number to initiate calls or messages from during the Flow Execution. Use [E.164](https://www.twilio.com/docs/glossary/what-e164) format (+1xxxxxxxxxx). + */ + from: string + /** + * The amount of time during which the Flow can only be triggered once per Flow SID - User ID combination. Default is 60 seconds. + */ + coolingOffPeriod?: number + /** + * A Distinct User ID + */ + userId?: string + /** + * A Distinct External ID + */ + anonymousId?: string + /** + * The type of the event being performed. + */ + eventType: string +} diff --git a/packages/destination-actions/src/destinations/twilio-studio/triggerStudioFlow/index.ts b/packages/destination-actions/src/destinations/twilio-studio/triggerStudioFlow/index.ts new file mode 100644 index 0000000000..e62cc0311d --- /dev/null +++ b/packages/destination-actions/src/destinations/twilio-studio/triggerStudioFlow/index.ts @@ -0,0 +1,121 @@ +import { ActionDefinition, IntegrationError } from '@segment/actions-core' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' +import { + shouldSendToStudio, + getToAddressField, + STUDIO_BASE_URL, + DEFAULT_COOLING_OFF_PERIOD, + TwilioError +} from '../utils' + +const action: ActionDefinition = { + title: 'Trigger Studio Flow', + description: + 'Trigger a Flow in Twilio Studio to initiate an outbound call or message. The Flow will execute via the REST API trigger.', + fields: { + flowSid: { + label: 'Flow SID', + description: 'The Flow SID, starting with FW, for the Studio Flow to trigger.', + type: 'string', + required: true + }, + from: { + label: 'From Phone Number', + description: + 'The Twilio phone number to initiate calls or messages from during the Flow Execution. Use [E.164](https://www.twilio.com/docs/glossary/what-e164) format (+1xxxxxxxxxx).', + type: 'string', + required: true + }, + coolingOffPeriod: { + label: 'Cooling-off Period (in seconds)', + description: + 'The amount of time during which the Flow can only be triggered once per Flow SID - User ID combination. Default is 60 seconds.', + type: 'number', + default: 60 + }, + userId: { + label: 'User ID', + description: 'A Distinct User ID', + type: 'hidden', + default: { '@path': '$.userId' } + }, + anonymousId: { + label: 'Anonymous ID', + description: 'A Distinct External ID', + type: 'hidden', + default: { '@path': '$.anonymousId' } + }, + eventType: { + label: 'Event type', + type: 'hidden', + description: 'The type of the event being performed.', + required: true, + default: { + '@path': '$.type' + } + } + }, + perform: async (request, { settings, payload, stateContext }) => { + if (payload.eventType !== 'identify') { + throw new IntegrationError( + 'Unable to trigger Studio Flow. Only Identify Events are allowed!', + `Incompatible Event Type: ${payload.eventType}`, + 400 + ) + } + if (!payload.userId && !payload.anonymousId) { + throw new IntegrationError( + 'Unable to trigger Studio Flow. No User identifier found for this Segment profile!', + `No userId found in the Segment Event`, + 400 + ) + } + if ( + stateContext && + !shouldSendToStudio( + `${payload.flowSid}_${payload.userId || payload.anonymousId}`, + stateContext, + payload.coolingOffPeriod! + ) + ) { + throw new IntegrationError( + `Unable to trigger Studio Flow. Only 1 request allowed per Flow SID - User ID combination in ${ + payload.coolingOffPeriod || DEFAULT_COOLING_OFF_PERIOD + } seconds`, + 'Cooling off Period', + 400 + ) + } + const toAddress = await getToAddressField(request, settings, payload) + if (!toAddress) { + throw new IntegrationError( + 'Unable to trigger Studio Flow. No Contact Address Found!', + 'Trigger Studio Flow no contact address found failure', + 400 + ) + } + const url = `${STUDIO_BASE_URL}/v2/Flows/${payload.flowSid}/Executions` + const parametersMap = JSON.stringify({ source: 'studio_segment_destination' }) + try { + await request(url, { + method: 'post', + body: new URLSearchParams({ + From: payload.from, + To: toAddress, + Parameters: parametersMap + }) + }) + } catch (err) { + const error = err as TwilioError + const errData = error.response.data + throw new IntegrationError( + `Unable to trigger Studio Flow. ${errData?.message}`, + `Twilio Error Code: ${errData?.code}`, + errData?.status + ) + } + } +} + +export default action diff --git a/packages/destination-actions/src/destinations/twilio-studio/utils.ts b/packages/destination-actions/src/destinations/twilio-studio/utils.ts new file mode 100644 index 0000000000..8942b00f8b --- /dev/null +++ b/packages/destination-actions/src/destinations/twilio-studio/utils.ts @@ -0,0 +1,81 @@ +import { RequestOptions, IntegrationError, HTTPError } from '@segment/actions-core' +import { StateContext } from '@segment/actions-core/src/destination-kit' +import type { Settings } from './generated-types' +import type { Payload } from './triggerStudioFlow/generated-types' +interface ExternalId { + id: string + type: string + source_id: string + collection: string + created_at: string + encoding: string +} + +type RequestFn = (url: string, options?: RequestOptions) => Promise +export interface TwilioError extends HTTPError { + response: Response & { + data: { + status: number + message: string + code: string + } + } +} + +export const STUDIO_BASE_URL = 'https://studio.twilio.com' +export const PROFILE_API_BASE_URL = 'https://profiles.segment.com' + +// Default cooldown period in seconds +export const DEFAULT_COOLING_OFF_PERIOD = 60 + +// Extracts the latest external Id based on the fetched list of external Ids +export const getToAddressField = async (request: RequestFn, settings: Settings, payload: Payload) => { + const externalIds = await fetchProfileExternalIds(request, settings, payload.userId!, payload.anonymousId!) + if (externalIds.length <= 0) { + return false + } + const latestId = externalIds.reduce((acc, current) => { + return new Date(acc.created_at) > new Date(current.created_at) ? acc : current + }) + return latestId?.id +} + +// Fetches the external Ids for a Segment profile +export const fetchProfileExternalIds = async ( + request: RequestFn, + settings: Settings, + userId: string, + anonymousId: string +): Promise => { + try { + // NOTE: Not falling back to the anonymousId for now as this action is going to use ID Sync Feature in future to + // directly get the phone external ID in the payload. This API call won't be required anymore. + // For the private Beta, there is no requirement to fallback to the anonymousId. + // Not using the region based Profile API call as well for the above mentioned reason. + const identifier = userId ? `user_id:${userId}` : `anonymous_id:${anonymousId}` + const response = await request( + `${PROFILE_API_BASE_URL}/v1/spaces/${settings.spaceId}/collections/users/profiles/${identifier}/external_ids?include=phone`, + { + headers: { + authorization: `Basic ${Buffer.from(settings.profileApiAccessToken + ':').toString('base64')}`, + 'content-type': 'application/json' + } + } + ) + const body = await response.json() + return body.data + } catch (error: unknown) { + throw new IntegrationError( + 'Unable to trigger Studio Flow. Fetching phone external Id failed.', + 'Profile fetch API failure', + 500 + ) + } +} + +// Decides whether to trigger the flow based on the cache. +export const shouldSendToStudio = (cacheKey: string, stateContext: StateContext, coolingOffPeriod: number): boolean => { + if (stateContext?.getRequestContext?.(cacheKey)) return false + stateContext?.setResponseContext?.(cacheKey, cacheKey, { second: coolingOffPeriod || DEFAULT_COOLING_OFF_PERIOD }) + return true +} diff --git a/packages/destination-actions/tsconfig.json b/packages/destination-actions/tsconfig.json index 41cc6831b3..6ee75d955c 100644 --- a/packages/destination-actions/tsconfig.json +++ b/packages/destination-actions/tsconfig.json @@ -9,6 +9,6 @@ "@segment/actions-shared": ["../actions-shared/src"] } }, - "exclude": [], + "exclude": ["dist"], "include": ["src", "test"] } diff --git a/packages/destination-subscriptions/package.json b/packages/destination-subscriptions/package.json index cd1cab15bd..65b340ef6c 100644 --- a/packages/destination-subscriptions/package.json +++ b/packages/destination-subscriptions/package.json @@ -1,6 +1,6 @@ { "name": "@segment/destination-subscriptions", - "version": "3.14.0", + "version": "3.15.0", "description": "Validate event payload using subscription AST", "license": "MIT", "repository": { diff --git a/packages/destination-subscriptions/src/__tests__/fql.test.ts b/packages/destination-subscriptions/src/__tests__/fql.test.ts index 1ae1c723a0..d215caafd7 100644 --- a/packages/destination-subscriptions/src/__tests__/fql.test.ts +++ b/packages/destination-subscriptions/src/__tests__/fql.test.ts @@ -5,7 +5,7 @@ const testFql = (fql: string, ast: any): void => { expect(generateFql(ast)).toEqual(fql) } -const expectedTypeError = new Error("Cannot read property 'type' of undefined") +const expectedTypeError = new Error("Cannot read properties of undefined (reading 'type')") expectedTypeError.name = 'TypeError' test('should handle invalid payloads', () => { diff --git a/packages/destination-subscriptions/tsconfig.json b/packages/destination-subscriptions/tsconfig.json index ab86887833..acd61576ef 100644 --- a/packages/destination-subscriptions/tsconfig.json +++ b/packages/destination-subscriptions/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "./tsconfig.build.json", - "exclude": [], - "include": ["src"] + "extends": "./tsconfig.build.json", + "include": ["src"], + "exclude": ["dist"] } diff --git a/scripts/clean.sh b/scripts/clean.sh new file mode 100644 index 0000000000..b425a99073 --- /dev/null +++ b/scripts/clean.sh @@ -0,0 +1,8 @@ +#!/bin/sh +# Nuke all caches. +# Can be used in CI, or # just to get your project out of a weird state. + +rm -rf node_modules/.cache .eslintcache +find . \( -name "dist" -o -iname "*.tsbuildinfo" \) ! -path "*/node_modules/*" -print0 | xargs -0 rm -rf + +echo "Build files and cache deleted." diff --git a/tsconfig.json b/tsconfig.json index 0b6911667b..38a5caf050 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,7 @@ "experimentalDecorators": true, "skipLibCheck": true }, - "exclude": ["node_modules", "dist", "templates"], + "exclude": ["dist", "templates"], "references": [ { "path": "./packages/ajv-human-errors/tsconfig.build.json" }, { "path": "./packages/browser-destinations/tsconfig.build.json" }, diff --git a/yarn.lock b/yarn.lock index 6314eb8704..40a9e8aaab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1164,6 +1164,20 @@ "@types/node" "*" jest-mock "^27.3.0" +"@jest/expect-utils@^28.1.3": + version "28.1.3" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-28.1.3.tgz#58561ce5db7cd253a7edddbc051fb39dda50f525" + integrity sha512-wvbi9LUrHJLn3NlDW6wF2hvIMtd4JUl2QNVrjq+IBSHirgfrR3o9RnVtxzdEGO2n9JyIWwHnLfby5KzqBGg2YA== + dependencies: + jest-get-type "^28.0.2" + +"@jest/expect-utils@^29.4.1": + version "29.4.1" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.4.1.tgz#105b9f3e2c48101f09cae2f0a4d79a1b3a419cbb" + integrity sha512-w6YJMn5DlzmxjO00i9wu2YSozUYRBhIoJ6nQwpMYcBMtiqMGJm1QBzOf6DDgRao8dbtpDoaqLg6iiQTvv0UHhQ== + dependencies: + jest-get-type "^29.2.0" + "@jest/fake-timers@^27.3.1": version "27.3.1" resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-27.3.1.tgz#1fad860ee9b13034762cdb94266e95609dfce641" @@ -1216,6 +1230,13 @@ terminal-link "^2.0.0" v8-to-istanbul "^8.1.0" +"@jest/schemas@^28.1.3": + version "28.1.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-28.1.3.tgz#ad8b86a66f11f33619e3d7e1dcddd7f2d40ff905" + integrity sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg== + dependencies: + "@sinclair/typebox" "^0.24.1" + "@jest/schemas@^29.0.0": version "29.0.0" resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.0.0.tgz#5f47f5994dd4ef067fb7b4188ceac45f77fe952a" @@ -1223,6 +1244,13 @@ dependencies: "@sinclair/typebox" "^0.24.1" +"@jest/schemas@^29.4.0": + version "29.4.0" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.4.0.tgz#0d6ad358f295cc1deca0b643e6b4c86ebd539f17" + integrity sha512-0E01f/gOZeNTG76i5eWWSupvSHaIINrTie7vCyjiYFKgzNdyEGd12BUv4oNBFHOqlHDbtoJi3HrQ38KCC90NsQ== + dependencies: + "@sinclair/typebox" "^0.25.16" + "@jest/source-map@^27.0.6": version "27.0.6" resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-27.0.6.tgz#be9e9b93565d49b0548b86e232092491fb60551f" @@ -1284,6 +1312,18 @@ "@types/yargs" "^16.0.0" chalk "^4.0.0" +"@jest/types@^28.1.3": + version "28.1.3" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-28.1.3.tgz#b05de80996ff12512bc5ceb1d208285a7d11748b" + integrity sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ== + dependencies: + "@jest/schemas" "^28.1.3" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + "@jest/types@^29.3.1": version "29.3.1" resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.3.1.tgz#7c5a80777cb13e703aeec6788d044150341147e3" @@ -1296,6 +1336,18 @@ "@types/yargs" "^17.0.8" chalk "^4.0.0" +"@jest/types@^29.4.1": + version "29.4.1" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.4.1.tgz#f9f83d0916f50696661da72766132729dcb82ecb" + integrity sha512-zbrAXDUOnpJ+FMST2rV7QZOgec8rskg2zv8g2ajeqitp4tvZiyqTCYXANrKsM+ryj5o+LI+ZN2EgU9drrkiwSA== + dependencies: + "@jest/schemas" "^29.4.0" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + "@jsdevtools/ono@^7.1.3": version "7.1.3" resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" @@ -2568,11 +2620,26 @@ resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.51.tgz#645f33fe4e02defe26f2f5c0410e1c094eac7f5f" integrity sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA== +"@sinclair/typebox@^0.25.16": + version "0.25.21" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.21.tgz#763b05a4b472c93a8db29b2c3e359d55b29ce272" + integrity sha512-gFukHN4t8K4+wVC+ECqeqwzBDeFeTzBXroBTqE6vcWrQGbEUpHO7LYdG0f4xnvYq4VOEwITSlHlp0JBAIFMS/g== + "@sindresorhus/is@^0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== +"@sindresorhus/is@^0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" + integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow== + +"@sindresorhus/is@^4.0.0": + version "4.6.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f" + integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw== + "@sinonjs/commons@^1.7.0": version "1.8.3" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" @@ -2770,6 +2837,13 @@ dependencies: defer-to-connect "^1.0.1" +"@szmarczak/http-timer@^4.0.5": + version "4.0.6" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.6.tgz#b4a914bb62e7c272d4e5989fe4440f812ab1d807" + integrity sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w== + dependencies: + defer-to-connect "^2.0.0" + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" @@ -2795,6 +2869,11 @@ resolved "https://registry.yarnpkg.com/@types/archy/-/archy-0.0.32.tgz#8b572741dad9172dfbf289397af1bb41296d3e40" integrity sha512-5ZZ5+YGmUE01yejiXsKnTcvhakMZ2UllZlMsQni53Doc1JWhe21ia8VntRoRD6fAEWw08JBh/z9qQHJ+//MrIg== +"@types/aria-query@^5.0.0": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.1.tgz#3286741fb8f1e1580ac28784add4c7a1d49bdfbc" + integrity sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q== + "@types/asn1js@^2.0.2": version "2.0.2" resolved "https://registry.yarnpkg.com/@types/asn1js/-/asn1js-2.0.2.tgz#bb1992291381b5f06e22a829f2ae009267cdf8c5" @@ -2846,6 +2925,16 @@ resolved "https://registry.yarnpkg.com/@types/btoa-lite/-/btoa-lite-1.0.0.tgz#e190a5a548e0b348adb0df9ac7fa5f1151c7cca4" integrity sha512-wJsiX1tosQ+J5+bY5LrSahHxr2wT+uME5UDwdN1kg4frt40euqA+wzECkmq4t5QbveHiJepfdThgQrPw6KiSlg== +"@types/cacheable-request@^6.0.1": + version "6.0.3" + resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.3.tgz#a430b3260466ca7b5ca5bfd735693b36e7a9d183" + integrity sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw== + dependencies: + "@types/http-cache-semantics" "*" + "@types/keyv" "^3.1.4" + "@types/node" "*" + "@types/responselike" "^1.0.0" + "@types/chai@*": version "4.2.21" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.21.tgz#9f35a5643129df132cf3b5c1ec64046ea1af0650" @@ -2873,6 +2962,23 @@ resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080" integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw== +"@types/diff@^5.0.0": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@types/diff/-/diff-5.0.2.tgz#dd565e0086ccf8bc6522c6ebafd8a3125c91c12b" + integrity sha512-uw8eYMIReOwstQ0QKF0sICefSy8cNO/v7gOTiIy9SbwuHyEecJUm7qlgueOO5S1udZ5I/irVydHVwMchgzbKTg== + +"@types/easy-table@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@types/easy-table/-/easy-table-1.2.0.tgz#d7153551a2c3f6571dddff974b05aa2fb1a4a948" + integrity sha512-gVQkR2G/q6UK3wQT+waY9tCrbFauzMoBfJpMxHSuemHLQ8HpHdUIQ9YyRwYMfNX4CfoAoj/eJATyECGkFr65Pg== + dependencies: + easy-table "*" + +"@types/ejs@^3.0.5": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@types/ejs/-/ejs-3.1.1.tgz#29c539826376a65e7f7d672d51301f37ed718f6d" + integrity sha512-RQul5wEfY7BjWm0sYY86cmUN/pcXWGyVxWX93DFFJvcrxax5zKlieLwA3T77xJGwNcZW0YW6CYG70p1m8xPFmA== + "@types/eslint-scope@^3.7.0": version "3.7.1" resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.1.tgz#8dc390a7b4f9dd9f1284629efce982e41612116e" @@ -2894,22 +3000,22 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83" integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw== -"@types/express-serve-static-core@^4.17.18": - version "4.17.24" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz#ea41f93bf7e0d59cd5a76665068ed6aab6815c07" - integrity sha512-3UJuW+Qxhzwjq3xhwXm2onQcFHn76frIYVbTu+kn24LFxI+dEhdfISDFovPB8VpEgW8oQCTpRuCe+0zJxB7NEA== +"@types/express-serve-static-core@^4.17.31": + version "4.17.33" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz#de35d30a9d637dc1450ad18dd583d75d5733d543" + integrity sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA== dependencies: "@types/node" "*" "@types/qs" "*" "@types/range-parser" "*" -"@types/express@^4.17.11": - version "4.17.13" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034" - integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA== +"@types/express@^4.17.16": + version "4.17.16" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.16.tgz#986caf0b4b850611254505355daa24e1b8323de8" + integrity sha512-LkKpqRZ7zqXJuvoELakaFYuETHjZkSol8EV6cNnyishutDBCCdv6+dsKPbKkCcIk57qRphOLY5sEgClw1bO3gA== dependencies: "@types/body-parser" "*" - "@types/express-serve-static-core" "^4.17.18" + "@types/express-serve-static-core" "^4.17.31" "@types/qs" "*" "@types/serve-static" "*" @@ -2920,6 +3026,13 @@ dependencies: "@types/node" "*" +"@types/fs-extra@^9.0.4": + version "9.0.13" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.13.tgz#7594fbae04fe7f1918ce8b3d213f74ff44ac1f45" + integrity sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA== + dependencies: + "@types/node" "*" + "@types/glob@*", "@types/glob@^7.1.1": version "7.1.4" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.4.tgz#ea59e21d2ee5c517914cb4bc8e4153b99e566672" @@ -2928,6 +3041,11 @@ "@types/minimatch" "*" "@types/node" "*" +"@types/google-libphonenumber@^7.4.23": + version "7.4.23" + resolved "https://registry.yarnpkg.com/@types/google-libphonenumber/-/google-libphonenumber-7.4.23.tgz#c44c9125d45f042943694d605fd8d8d796cafc3b" + integrity sha512-C3ydakLTQa8HxtYf9ge4q6uT9krDX8smSIxmmW3oACFi5g5vv6T068PRExF7UyWbWpuYiDG8Nm24q2X5XhcZWw== + "@types/graceful-fs@^4.1.2": version "4.1.5" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" @@ -2935,6 +3053,11 @@ dependencies: "@types/node" "*" +"@types/http-cache-semantics@*": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz#0ea7b61496902b95890dc4c3a116b60cb8dae812" + integrity sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ== + "@types/http-proxy@^1.17.5": version "1.17.7" resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.7.tgz#30ea85cc2c868368352a37f0d0d3581e24834c6f" @@ -2942,6 +3065,13 @@ dependencies: "@types/node" "*" +"@types/inquirer@^8.1.2": + version "8.2.5" + resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-8.2.5.tgz#c508423bcc11126db278170ab07347783ac2300c" + integrity sha512-QXlzybid60YtAwfgG3cpykptRYUx2KomzNutMlWsQC64J/WG/gQSl+P4w7A21sGN0VIxRVava4rgnT7FQmFCdg== + dependencies: + "@types/through" "*" + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762" @@ -2992,6 +3122,34 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= +"@types/keyv@^3.1.4": + version "3.1.4" + resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.4.tgz#3ccdb1c6751b0c7e52300bcdacd5bcbf8faa75b6" + integrity sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg== + dependencies: + "@types/node" "*" + +"@types/lodash.flattendeep@^4.4.6": + version "4.4.7" + resolved "https://registry.yarnpkg.com/@types/lodash.flattendeep/-/lodash.flattendeep-4.4.7.tgz#0ce3dccbe006826d58e9824b27df4b00ed3e90e6" + integrity sha512-1h6GW/AeZw/Wej6uxrqgmdTDZX1yFS39lRsXYkg+3kWvOWWrlGCI6H7lXxlUHOzxDT4QeYGmgPpQ3BX9XevzOg== + dependencies: + "@types/lodash" "*" + +"@types/lodash.pickby@^4.6.6": + version "4.6.7" + resolved "https://registry.yarnpkg.com/@types/lodash.pickby/-/lodash.pickby-4.6.7.tgz#fd089a5a7f8cbe7294ae5c90ea5ecd9f4cae4d2c" + integrity sha512-4ebXRusuLflfscbD0PUX4eVknDHD9Yf+uMtBIvA/hrnTqeAzbuHuDjvnYriLjUrI9YrhCPVKUf4wkRSXJQ6gig== + dependencies: + "@types/lodash" "*" + +"@types/lodash.union@^4.6.6": + version "4.6.7" + resolved "https://registry.yarnpkg.com/@types/lodash.union/-/lodash.union-4.6.7.tgz#ceace5ed9f3610652ba4a72e0e0afb2a0eec7a4d" + integrity sha512-6HXM6tsnHJzKgJE0gA/LhTGf/7AbjUk759WZ1MziVm+OBNAATHhdgj+a3KVE8g76GCLAnN4ZEQQG1EGgtBIABA== + dependencies: + "@types/lodash" "*" + "@types/lodash@*", "@types/lodash@^4.14.168", "@types/lodash@^4.14.175": version "4.14.175" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.175.tgz#b78dfa959192b01fae0ad90e166478769b215f45" @@ -3019,6 +3177,11 @@ resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== +"@types/mocha@^10.0.0": + version "10.0.1" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.1.tgz#2f4f65bb08bc368ac39c96da7b2f09140b26851b" + integrity sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q== + "@types/mustache@^4.1.0": version "4.1.2" resolved "https://registry.yarnpkg.com/@types/mustache/-/mustache-4.1.2.tgz#d0e158013c81674a5b6d8780bc3fe234e1804eaf" @@ -3029,7 +3192,7 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.1.tgz#2e50a649a50fc403433a14f829eface1a3443e97" integrity sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA== -"@types/node@>=10.0.0": +"@types/node@>=10.0.0", "@types/node@^18.0.0": version "18.11.18" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f" integrity sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA== @@ -3039,16 +3202,21 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.33.tgz#24927446e8b7669d10abacedd16077359678f436" integrity sha512-5XmYX2GECSa+CxMYaFsr2mrql71Q4EvHjKS+ox/SiwSdaASMoBIWE6UmZqFO+VX1jIcsYLStI4FFoB6V7FeIYw== -"@types/node@^14.0.0": - version "14.17.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.9.tgz#b97c057e6138adb7b720df2bd0264b03c9f504fd" - integrity sha512-CMjgRNsks27IDwI785YMY0KLt3co/c0cQ5foxHYv/shC2w8oOnVwz5Ubq1QG5KzrcW+AXk6gzdnxIkDnTvzu3g== +"@types/node@^18.11.15": + version "18.11.15" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.15.tgz#de0e1fbd2b22b962d45971431e2ae696643d3f5d" + integrity sha512-VkhBbVo2+2oozlkdHXLrb3zjsRkpdnaU2bXmX8Wgle3PUi569eLRaHGlgETQHR7lLL1w7GiG3h9SnePhxNDecw== "@types/normalize-package-data@^2.4.0": version "2.4.1" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== +"@types/object-inspect@^1.8.0": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@types/object-inspect/-/object-inspect-1.8.1.tgz#7c08197ad05cc0e513f529b1f3919cc99f720e1f" + integrity sha512-0JTdf3CGV0oWzE6Wa40Ayv2e2GhpP3pEJMcrlM74vBSJPuuNkVwfDnl0SZxyFCXETcB4oKA/MpTVfuYSMOelBg== + "@types/parse-json@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" @@ -3076,6 +3244,20 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== +"@types/recursive-readdir@^2.2.0": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@types/recursive-readdir/-/recursive-readdir-2.2.1.tgz#330f5ec0b73e8aeaf267a6e056884e393f3543a3" + integrity sha512-Xd+Ptc4/F2ueInqy5yK2FI5FxtwwbX2+VZpcg+9oYsFJVen8qQKGapCr+Bi5wQtHU1cTXT8s+07lo/nKPgu8Gg== + dependencies: + "@types/node" "*" + +"@types/responselike@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" + integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA== + dependencies: + "@types/node" "*" + "@types/retry@^0.12.0": version "0.12.1" resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.1.tgz#d8f1c0d0dc23afad6dc16a9e993a0865774b4065" @@ -3114,11 +3296,40 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/stream-buffers@^3.0.3": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/stream-buffers/-/stream-buffers-3.0.4.tgz#bf128182da7bc62722ca0ddf5458a9c65f76e648" + integrity sha512-qU/K1tb2yUdhXkLIATzsIPwbtX6BpZk0l3dPW6xqWyhfzzM1ECaQ/8faEnu3CNraLiQ9LHyQQPBGp7N9Fbs25w== + dependencies: + "@types/node" "*" + +"@types/supports-color@^8.1.0": + version "8.1.1" + resolved "https://registry.yarnpkg.com/@types/supports-color/-/supports-color-8.1.1.tgz#1b44b1b096479273adf7f93c75fc4ecc40a61ee4" + integrity sha512-dPWnWsf+kzIG140B8z2w3fr5D03TLWbOAFQl45xUpI3vcizeXriNR5VYkWZ+WTMsUHqZ9Xlt3hrxGNANFyNQfw== + +"@types/through@*": + version "0.0.30" + resolved "https://registry.yarnpkg.com/@types/through/-/through-0.0.30.tgz#e0e42ce77e897bd6aead6f6ea62aeb135b8a3895" + integrity sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg== + dependencies: + "@types/node" "*" + +"@types/tmp@^0.2.0": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.2.3.tgz#908bfb113419fd6a42273674c00994d40902c165" + integrity sha512-dDZH/tXzwjutnuk4UacGgFRwV+JSLaXL1ikvidfJprkb7L9Nx1njcRHHmi3Dsvt7pgqqTEeucQuOrWHPFgzVHA== + "@types/to-title-case@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/to-title-case/-/to-title-case-1.0.0.tgz#37257e5293a54a92f29efc45b7ebc5f20807f32d" integrity sha512-064+ER3yeChxTQwM0LTRTlG7GK6/HXM9ekX1y/KqJi0pvBBIrvUnYWZ2JQGivCAuw29CuLq0b6zUPL9FY/JMnQ== +"@types/ua-parser-js@^0.7.33": + version "0.7.36" + resolved "https://registry.yarnpkg.com/@types/ua-parser-js/-/ua-parser-js-0.7.36.tgz#9bd0b47f26b5a3151be21ba4ce9f5fa457c5f190" + integrity sha512-N1rW+njavs70y2cApeIw1vLMYXRwfBy+7trgavGuuTfOd7j1Yh7QTRc/yqsPl6ncokt72ZXuxEU0PiCp9bSwNQ== + "@types/webpack@^5.0.0": version "5.28.0" resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-5.28.0.tgz#78dde06212f038d77e54116cfe69e88ae9ed2c03" @@ -3128,6 +3339,11 @@ tapable "^2.2.0" webpack "^5" +"@types/which@^1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@types/which/-/which-1.3.2.tgz#9c246fc0c93ded311c8512df2891fb41f6227fdf" + integrity sha512-8oDqyLC7eD4HM307boe2QWKyuzdzWBj56xI/imSl2cpL+U3tCMaTAkMJ4ee5JBZ/FsOJlvRGeIShiZDAl1qERA== + "@types/ws@^8.5.1": version "8.5.1" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.1.tgz#79136958b48bc73d5165f286707ceb9f04471599" @@ -3154,6 +3370,13 @@ dependencies: "@types/yargs-parser" "*" +"@types/yauzl@^2.9.1": + version "2.10.0" + resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.0.tgz#b3248295276cf8c6f153ebe6a9aba0c988cb2599" + integrity sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw== + dependencies: + "@types/node" "*" + "@typescript-eslint/eslint-plugin@^4.14.0": version "4.29.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.29.1.tgz#808d206e2278e809292b5de752a91105da85860b" @@ -3269,6 +3492,170 @@ "@typescript-eslint/types" "5.1.0" eslint-visitor-keys "^3.0.0" +"@wdio/cli@^7.26.0": + version "7.30.0" + resolved "https://registry.yarnpkg.com/@wdio/cli/-/cli-7.30.0.tgz#974a1d0763c077902786c71934cf72f3b0bc4804" + integrity sha512-QG7BXhmoZCJ3u7snQtsBcHamZwMhBPEttYyZyJwQSAddiUgc356Elq/io+EEXuknorP1ya017yLehzuJzXiNdQ== + dependencies: + "@types/ejs" "^3.0.5" + "@types/fs-extra" "^9.0.4" + "@types/inquirer" "^8.1.2" + "@types/lodash.flattendeep" "^4.4.6" + "@types/lodash.pickby" "^4.6.6" + "@types/lodash.union" "^4.6.6" + "@types/node" "^18.0.0" + "@types/recursive-readdir" "^2.2.0" + "@wdio/config" "7.30.0" + "@wdio/logger" "7.26.0" + "@wdio/protocols" "7.27.0" + "@wdio/types" "7.26.0" + "@wdio/utils" "7.26.0" + async-exit-hook "^2.0.1" + chalk "^4.0.0" + chokidar "^3.0.0" + cli-spinners "^2.1.0" + ejs "^3.0.1" + fs-extra "^10.0.0" + inquirer "8.2.4" + lodash.flattendeep "^4.4.0" + lodash.pickby "^4.6.0" + lodash.union "^4.6.0" + mkdirp "^1.0.4" + recursive-readdir "^2.2.2" + webdriverio "7.30.0" + yargs "^17.0.0" + yarn-install "^1.0.0" + +"@wdio/config@7.30.0": + version "7.30.0" + resolved "https://registry.yarnpkg.com/@wdio/config/-/config-7.30.0.tgz#5e20a2097b5cdf08046359739caf06272d23d857" + integrity sha512-/38rol9WCfFTMtXyd/C856/aexxIZnfVvXg7Fw2WXpqZ9qadLA+R4N35S2703n/RByjK/5XAYtHoljtvh3727w== + dependencies: + "@wdio/logger" "7.26.0" + "@wdio/types" "7.26.0" + "@wdio/utils" "7.26.0" + deepmerge "^4.0.0" + glob "^8.0.3" + +"@wdio/local-runner@^7.26.0": + version "7.30.0" + resolved "https://registry.yarnpkg.com/@wdio/local-runner/-/local-runner-7.30.0.tgz#abcaba93351dd2425d97319fffc529aa81d134d9" + integrity sha512-C/MbKY1wc3R3be9IauDiOIDt5HioD/A83RFd0O8G/ABgqsgEnEHyT2a5MaoO/wRnIw/XSlzqPosFESIIGSbSuQ== + dependencies: + "@types/stream-buffers" "^3.0.3" + "@wdio/logger" "7.26.0" + "@wdio/repl" "7.26.0" + "@wdio/runner" "7.30.0" + "@wdio/types" "7.26.0" + async-exit-hook "^2.0.1" + split2 "^4.0.0" + stream-buffers "^3.0.2" + +"@wdio/logger@7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@wdio/logger/-/logger-7.26.0.tgz#2c105a00f63a81d52de969fef5a54a9035146b2d" + integrity sha512-kQj9s5JudAG9qB+zAAcYGPHVfATl2oqKgqj47yjehOQ1zzG33xmtL1ArFbQKWhDG32y1A8sN6b0pIqBEIwgg8Q== + dependencies: + chalk "^4.0.0" + loglevel "^1.6.0" + loglevel-plugin-prefix "^0.8.4" + strip-ansi "^6.0.0" + +"@wdio/mocha-framework@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@wdio/mocha-framework/-/mocha-framework-7.26.0.tgz#757be1648c662d5637b1d4522f826613c5647e95" + integrity sha512-iqAVRDc5ECeRUV2sk6AfCH0eryR3GtdDbDHUr4IxPjb9avtuYhHBR20hCV5c6PAyX33h7ZnHCIIej4bUZcCI6g== + dependencies: + "@types/mocha" "^10.0.0" + "@wdio/logger" "7.26.0" + "@wdio/types" "7.26.0" + "@wdio/utils" "7.26.0" + expect-webdriverio "^3.0.0" + mocha "^10.0.0" + +"@wdio/protocols@7.27.0": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@wdio/protocols/-/protocols-7.27.0.tgz#8e2663ec877dce7a5f76b021209c18dd0132e853" + integrity sha512-hT/U22R5i3HhwPjkaKAG0yd59eaOaZB0eibRj2+esCImkb5Y6rg8FirrlYRxIGFVBl0+xZV0jKHzR5+o097nvg== + +"@wdio/repl@7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@wdio/repl/-/repl-7.26.0.tgz#bf0703f46ad379107b9cfc254c3eccbd5cd6d848" + integrity sha512-2YxbXNfYVGVLrffUJzl/l5s8FziDPl917eLP62gkEH/H5IV27Pnwx3Iyu0KOEaBzgntnURANlwhCZFXQ4OPq8Q== + dependencies: + "@wdio/utils" "7.26.0" + +"@wdio/reporter@7.29.1": + version "7.29.1" + resolved "https://registry.yarnpkg.com/@wdio/reporter/-/reporter-7.29.1.tgz#7fc2e3b7aa3843172dcd97221c44257384cbbd27" + integrity sha512-mpusCpbw7RxnJSDu9qa1qv5IfEMCh7377y1Typ4J2TlMy+78CQzGZ8coEXjBxLcqijTUwcyyoLNI5yRSvbDExw== + dependencies: + "@types/diff" "^5.0.0" + "@types/node" "^18.0.0" + "@types/object-inspect" "^1.8.0" + "@types/supports-color" "^8.1.0" + "@types/tmp" "^0.2.0" + "@wdio/types" "7.26.0" + diff "^5.0.0" + fs-extra "^10.0.0" + object-inspect "^1.10.3" + supports-color "8.1.1" + +"@wdio/runner@7.30.0": + version "7.30.0" + resolved "https://registry.yarnpkg.com/@wdio/runner/-/runner-7.30.0.tgz#cd298a3f75fa342ba9ab534872de613eef2e80ac" + integrity sha512-oawZtHXXY/6+e/0pIKMuPo4LlX8U7t0pfg6SkK06dm9NPppcZH2FvOtzgw1MHAmm9YYxNJ/LOJz0Ne8kXy/y7w== + dependencies: + "@wdio/config" "7.30.0" + "@wdio/logger" "7.26.0" + "@wdio/types" "7.26.0" + "@wdio/utils" "7.26.0" + deepmerge "^4.0.0" + gaze "^1.1.2" + webdriver "7.30.0" + webdriverio "7.30.0" + +"@wdio/sauce-service@^7.26.0": + version "7.30.0" + resolved "https://registry.yarnpkg.com/@wdio/sauce-service/-/sauce-service-7.30.0.tgz#26a921c3f0212279808e730369a2418fe0957bd1" + integrity sha512-xLTrDrdThqIJKSRlqAph8MbcwDq4TjJcBmhqBUBbADw9h0wtVM4Bbwp4qgY46ufjrrBuCWZEsK1+WpX4cuQ5gw== + dependencies: + "@types/node" "^18.0.0" + "@wdio/logger" "7.26.0" + "@wdio/types" "7.26.0" + "@wdio/utils" "7.26.0" + saucelabs "^7.1.3" + webdriverio "7.30.0" + +"@wdio/spec-reporter@^7.26.0": + version "7.29.1" + resolved "https://registry.yarnpkg.com/@wdio/spec-reporter/-/spec-reporter-7.29.1.tgz#08e13c02ea0876672226d5a2c326dda7e1a66c8e" + integrity sha512-bwSGM72QrDedqacY7Wq9Gn86VgRwIGPYzZtcaD7aDnvppCuV8Z/31Wpdfen+CzUk2+whXjXKe66ohPyl9TG5+w== + dependencies: + "@types/easy-table" "^1.2.0" + "@wdio/reporter" "7.29.1" + "@wdio/types" "7.26.0" + chalk "^4.0.0" + easy-table "^1.1.1" + pretty-ms "^7.0.0" + +"@wdio/types@7", "@wdio/types@7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@wdio/types/-/types-7.26.0.tgz#70bc879c5dbe316a0eebbac4a46f0f66430b1d84" + integrity sha512-mOTfWAGQ+iT58iaZhJMwlUkdEn3XEWE4jthysMLXFnSuZ2eaODVAiK31SmlS/eUqgSIaupeGqYUrtCuSNbLefg== + dependencies: + "@types/node" "^18.0.0" + got "^11.8.1" + +"@wdio/utils@7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@wdio/utils/-/utils-7.26.0.tgz#e282d072ccbacbe583f6d1b192c0320cede170c1" + integrity sha512-pVq2MPXZAYLkKGKIIHktHejnHqg4TYKoNYSi2EDv+I3GlT8VZKXHazKhci82ov0tD+GdF27+s4DWNDCfGYfBdQ== + dependencies: + "@wdio/logger" "7.26.0" + "@wdio/types" "7.26.0" + p-iteration "^1.1.8" + "@webassemblyjs/ast@1.11.1": version "1.11.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" @@ -3475,6 +3862,14 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: mime-types "~2.1.24" negotiator "0.6.2" +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + acorn-globals@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45" @@ -3586,7 +3981,7 @@ ansi-align@^2.0.0: dependencies: string-width "^2.0.0" -ansi-colors@^4.1.1: +ansi-colors@4.1.1, ansi-colors@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== @@ -3610,7 +4005,7 @@ ansi-html-community@^0.0.8: ansi-regex@5.0.1, ansi-regex@^2.0.0, ansi-regex@^2.1.1, ansi-regex@^3.0.0, ansi-regex@^5.0.0, ansi-regex@^5.0.1, ansi-regex@^6.0.1: version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-styles@^2.2.1: @@ -3665,6 +4060,42 @@ arch@^2.1.1: resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== +archive-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/archive-type/-/archive-type-4.0.0.tgz#f92e72233056dfc6969472749c267bdb046b1d70" + integrity sha512-zV4Ky0v1F8dBrdYElwTvQhweQ0P7Kwc1aluqJsYtOBP01jXcWCyW2IEfI1YiqsG+Iy7ZR+o5LF1N+PGECBxHWA== + dependencies: + file-type "^4.2.0" + +archiver-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-2.1.0.tgz#e8a460e94b693c3e3da182a098ca6285ba9249e2" + integrity sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw== + dependencies: + glob "^7.1.4" + graceful-fs "^4.2.0" + lazystream "^1.0.0" + lodash.defaults "^4.2.0" + lodash.difference "^4.5.0" + lodash.flatten "^4.4.0" + lodash.isplainobject "^4.0.6" + lodash.union "^4.6.0" + normalize-path "^3.0.0" + readable-stream "^2.0.0" + +archiver@^5.0.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/archiver/-/archiver-5.3.1.tgz#21e92811d6f09ecfce649fbefefe8c79e57cbbb6" + integrity sha512-8KyabkmbYrH+9ibcTScQ1xCJC/CGcugdVIwB+53f5sZziXgwUh3iXlAlANMxcZyDEfTHMe6+Z5FofV8nopXP7w== + dependencies: + archiver-utils "^2.1.0" + async "^3.2.3" + buffer-crc32 "^0.2.1" + readable-stream "^3.6.0" + readdir-glob "^1.0.0" + tar-stream "^2.2.0" + zip-stream "^4.1.0" + archy@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" @@ -3700,6 +4131,13 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +aria-query@^5.0.0: + version "5.1.3" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e" + integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ== + dependencies: + deep-equal "^2.0.5" + arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" @@ -3789,6 +4227,11 @@ astral-regex@^2.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== +async-exit-hook@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/async-exit-hook/-/async-exit-hook-2.0.1.tgz#8bd8b024b0ec9b1c01cccb9af9db29bd717dfaf3" + integrity sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw== + async@^2.6.2: version "2.6.4" resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" @@ -3816,6 +4259,11 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +available-typed-arrays@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" + integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== + aws4@^1.11.0: version "1.11.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" @@ -4004,6 +4452,14 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== +bl@^1.0.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.3.tgz#1e8dd80142eac80d7158c9dccc047fb620e035e7" + integrity sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww== + dependencies: + readable-stream "^2.3.5" + safe-buffer "^5.1.1" + bl@^4.0.3, bl@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" @@ -4029,7 +4485,7 @@ body-parser@1.19.0, body-parser@^1.18.2: raw-body "2.4.0" type-is "~1.6.17" -body-parser@^1.19.0: +body-parser@1.20.1, body-parser@^1.19.0: version "1.20.1" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== @@ -4132,6 +4588,11 @@ browser-process-hrtime@^1.0.0: resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.16.0, browserslist@^4.16.6, browserslist@^4.16.7: version "4.17.4" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.17.4.tgz#72e2508af2a403aec0a49847ef31bd823c57ead4" @@ -4162,6 +4623,29 @@ btoa-lite@^1.0.0: resolved "https://registry.yarnpkg.com/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337" integrity sha1-M3dm2hWAEhD92VbCLpxokaudAzc= +buffer-alloc-unsafe@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" + integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg== + +buffer-alloc@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec" + integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow== + dependencies: + buffer-alloc-unsafe "^1.1.0" + buffer-fill "^1.0.0" + +buffer-crc32@^0.2.1, buffer-crc32@^0.2.13, buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== + +buffer-fill@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" + integrity sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ== + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" @@ -4172,7 +4656,7 @@ buffer-indexof@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g== -buffer@^5.5.0: +buffer@^5.2.1, buffer@^5.5.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== @@ -4217,6 +4701,19 @@ bytes@3.1.2: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== +cac@^3.0.3: + version "3.0.4" + resolved "https://registry.yarnpkg.com/cac/-/cac-3.0.4.tgz#6d24ceec372efe5c9b798808bc7f49b47242a4ef" + integrity sha512-hq4rxE3NT5PlaEiVV39Z45d6MoFcQZG5dsgJqtAUeOz3408LEQAElToDkf9i5IYSCOmK0If/81dLg7nKxqPR0w== + dependencies: + camelcase-keys "^3.0.0" + chalk "^1.1.3" + indent-string "^3.0.0" + minimist "^1.2.0" + read-pkg-up "^1.0.1" + suffix "^0.1.0" + text-table "^0.2.0" + cacache@^16.0.0, cacache@^16.0.6, cacache@^16.1.0: version "16.1.3" resolved "https://registry.yarnpkg.com/cacache/-/cacache-16.1.3.tgz#a02b9f34ecfaf9a78c9f4bc16fceb94d5d67a38e" @@ -4256,6 +4753,24 @@ cache-base@^1.0.1: union-value "^1.0.0" unset-value "^1.0.0" +cacheable-lookup@^5.0.3: + version "5.0.4" + resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz#5a6b865b2c44357be3d5ebc2a467b032719a7005" + integrity sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA== + +cacheable-request@^2.1.1: + version "2.1.4" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-2.1.4.tgz#0d808801b6342ad33c91df9d0b44dc09b91e5c3d" + integrity sha512-vag0O2LKZ/najSoUwDbVlnlCFvhBE/7mGTY2B5FgCBDcRD+oVV1HYTOwM6JZfMg/hIcM6IwnTZ1uQQL5/X3xIQ== + dependencies: + clone-response "1.0.2" + get-stream "3.0.0" + http-cache-semantics "3.8.1" + keyv "3.0.0" + lowercase-keys "1.0.0" + normalize-url "2.0.1" + responselike "1.0.2" + cacheable-request@^6.0.0: version "6.1.0" resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" @@ -4269,6 +4784,19 @@ cacheable-request@^6.0.0: normalize-url "^4.1.0" responselike "^1.0.2" +cacheable-request@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.2.tgz#ea0d0b889364a25854757301ca12b2da77f91d27" + integrity sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew== + dependencies: + clone-response "^1.0.2" + get-stream "^5.1.0" + http-cache-semantics "^4.0.0" + keyv "^4.0.0" + lowercase-keys "^2.0.0" + normalize-url "^6.0.1" + responselike "^2.0.0" + call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" @@ -4287,6 +4815,22 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== +camel-case@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a" + integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw== + dependencies: + pascal-case "^3.1.2" + tslib "^2.0.3" + +camelcase-keys@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-3.0.0.tgz#fc0c6c360363f7377e3793b9a16bccf1070c1ca4" + integrity sha512-U4E6A6aFyYnNW+tDt5/yIUKQURKXe3WMFPfX4FxrQFcwZ/R08AUk1xWcUtlr7oq6CV07Ji+aa69V2g7BSpblnQ== + dependencies: + camelcase "^3.0.0" + map-obj "^1.0.0" + camelcase-keys@^6.2.2: version "6.2.2" resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0" @@ -4296,6 +4840,11 @@ camelcase-keys@^6.2.2: map-obj "^4.0.0" quick-lru "^4.0.1" +camelcase@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" + integrity sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg== + camelcase@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" @@ -4306,6 +4855,11 @@ camelcase@^5.0.0, camelcase@^5.3.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== +camelcase@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + camelcase@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" @@ -4326,6 +4880,15 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001265: resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001431.tgz" integrity sha512-zBUoFU0ZcxpvSt9IU66dXVT/3ctO1cy4y9cscs1szkPlcWb6pasYM144GqrUygUbT+k7cmUCW61cvskjcv0enQ== +capital-case@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/capital-case/-/capital-case-1.0.4.tgz#9d130292353c9249f6b00fa5852bee38a717e669" + integrity sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + upper-case-first "^2.0.2" + capture-stack-trace@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz#a6c0bbe1f38f3aa0b92238ecb6ff42c344d4135d" @@ -4368,7 +4931,7 @@ chalk@4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^1.0.0, chalk@^1.1.1: +chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= @@ -4409,6 +4972,24 @@ chance@^1.1.8: resolved "https://registry.yarnpkg.com/chance/-/chance-1.1.8.tgz#5d6c2b78c9170bf6eb9df7acdda04363085be909" integrity sha512-v7fi5Hj2VbR6dJEGRWLmJBA83LJMS47pkAbmROFxHWd9qmE1esHRZW8Clf1Fhzr3rjxnNZVCjOEv/ivFxeIMtg== +change-case@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/change-case/-/change-case-4.1.2.tgz#fedfc5f136045e2398c0410ee441f95704641e12" + integrity sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A== + dependencies: + camel-case "^4.1.2" + capital-case "^1.0.4" + constant-case "^3.0.4" + dot-case "^3.0.4" + header-case "^2.0.4" + no-case "^3.0.4" + param-case "^3.0.4" + pascal-case "^3.1.2" + path-case "^3.0.4" + sentence-case "^3.0.4" + snake-case "^3.0.4" + tslib "^2.0.3" + char-regex@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" @@ -4453,6 +5034,21 @@ cheerio@^1.0.0-rc.10: parse5-htmlparser2-tree-adapter "^6.0.1" tslib "^2.2.0" +chokidar@3.5.3, chokidar@^3.0.0: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + chokidar@^3.5.1, chokidar@^3.5.2: version "3.5.2" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75" @@ -4478,6 +5074,16 @@ chownr@^2.0.0: resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== +chrome-launcher@^0.15.0: + version "0.15.1" + resolved "https://registry.yarnpkg.com/chrome-launcher/-/chrome-launcher-0.15.1.tgz#0a0208037063641e2b3613b7e42b0fcb3fa2d399" + integrity sha512-UugC8u59/w2AyX5sHLZUHoxBAiSiunUhZa3zZwMH6zPVis0C3dDKiRWyUGIo14tTbZHGVviWxv3PQWZ7taZ4fg== + dependencies: + "@types/node" "*" + escape-string-regexp "^4.0.0" + is-wsl "^2.2.0" + lighthouse-logger "^1.0.0" + chrome-trace-event@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" @@ -4569,6 +5175,11 @@ cli-spinners@2.6.1: resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.1.tgz#adc954ebe281c37a6319bfa401e6dd2488ffb70d" integrity sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g== +cli-spinners@^2.1.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.7.0.tgz#f815fd30b5f9eaac02db604c7a231ed7cb2f797a" + integrity sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw== + cli-spinners@^2.5.0: version "2.6.0" resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.0.tgz#36c7dc98fb6a9a76bd6238ec3f77e2425627e939" @@ -4655,7 +5266,7 @@ clone-deep@^4.0.1: kind-of "^6.0.2" shallow-clone "^3.0.0" -clone-response@^1.0.2: +clone-response@1.0.2, clone-response@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= @@ -4761,7 +5372,7 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" -commander@^2.20.0: +commander@^2.20.0, commander@^2.8.1: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -4799,6 +5410,16 @@ component-emitter@^1.2.1: resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== +compress-commons@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-4.1.1.tgz#df2a09a7ed17447642bad10a85cc9a19e5c42a7d" + integrity sha512-QLdDLCKNV2dtoTorqgxngQCMA+gWXkM/Nwu7FpeBhk/RdkzimqC3jueb/FDmaZeXh+uby1jkBqE3xArsLBE5wQ== + dependencies: + buffer-crc32 "^0.2.13" + crc32-stream "^4.0.2" + normalize-path "^3.0.0" + readable-stream "^3.6.0" + compressible@~2.0.14, compressible@~2.0.16: version "2.0.18" resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" @@ -4929,6 +5550,15 @@ const-smallest-float64@^1.0.0: dependencies: utils-define-read-only-property "^1.0.0" +constant-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/constant-case/-/constant-case-3.0.4.tgz#3b84a9aeaf4cf31ec45e6bf5de91bdfb0589faf1" + integrity sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + upper-case "^2.0.2" + content-disposition@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" @@ -4941,6 +5571,13 @@ content-disposition@0.5.3: dependencies: safe-buffer "5.1.2" +content-disposition@0.5.4, content-disposition@^0.5.2: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + content-type@^1.0.4, content-type@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" @@ -5046,6 +5683,11 @@ cookie@0.4.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== +cookie@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + cookie@~0.4.1: version "0.4.2" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" @@ -5093,6 +5735,19 @@ cosmiconfig@^7.0.0: path-type "^4.0.0" yaml "^1.10.0" +crc-32@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff" + integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ== + +crc32-stream@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-4.0.2.tgz#c922ad22b38395abe9d3870f02fa8134ed709007" + integrity sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w== + dependencies: + crc-32 "^1.2.0" + readable-stream "^3.4.0" + create-cert@^1.0.2: version "1.0.6" resolved "https://registry.yarnpkg.com/create-cert/-/create-cert-1.0.6.tgz#7ed01fff7f9f0cea500aba5eff119af4c8de84f6" @@ -5123,6 +5778,13 @@ create-test-server@^3.0.1: express "^4.15.3" pify "^3.0.0" +cross-fetch@3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" + integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== + dependencies: + node-fetch "2.6.7" + cross-fetch@^3.1.4: version "3.1.4" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.4.tgz#9723f3a3a247bf8b89039f3a380a9244e8fa2f39" @@ -5130,6 +5792,14 @@ cross-fetch@^3.1.4: dependencies: node-fetch "2.6.1" +cross-spawn@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41" + integrity sha512-yAXz/pA1tD8Gtg2S98Ekf/sewp3Lcp3YoFKJ4Hkp5h5yLWnKVTDU0kwjKJ8NDCYcfTLfyGkzTikst+jWypT1iA== + dependencies: + lru-cache "^4.0.1" + which "^1.2.9" + cross-spawn@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" @@ -5214,6 +5884,11 @@ css-select@^4.1.3: domutils "^2.6.0" nth-check "^2.0.0" +css-shorthand-properties@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/css-shorthand-properties/-/css-shorthand-properties-1.1.1.tgz#1c808e63553c283f289f2dd56fcee8f3337bd935" + integrity sha512-Md+Juc7M3uOdbAFwOYlTrccIZ7oCFuzrhKYQjdeUEW/sE1hv17Jp/Bws+ReOPpGVBTYCBoYo+G17V5Qo8QQ75A== + css-tree@^1.1.2: version "1.1.3" resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" @@ -5222,6 +5897,11 @@ css-tree@^1.1.2: mdn-data "2.0.14" source-map "^0.6.1" +css-value@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/css-value/-/css-value-0.0.1.tgz#5efd6c2eea5ea1fd6b6ac57ec0427b18452424ea" + integrity sha512-FUV3xaJ63buRLgHrLQVlVgQnQdR4yqdLGaDu7g8CQcWjInDfM9plBTPI9FRfpahju1UBSaMckeb2/46ApS/V1Q== + css-what@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.0.1.tgz#3efa820131f4669a8ac2408f9c32e7c7de9f4cad" @@ -5372,7 +6052,7 @@ dayjs@^1.10.7: resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.7.tgz#2cf5f91add28116748440866a0a1d26f3a6ce468" integrity sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig== -debug@2.6.9, debug@^2.2.0, debug@^2.3.3: +debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -5386,6 +6066,13 @@ debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1, d dependencies: ms "2.1.2" +debug@4.3.4, debug@^4.3.3, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + debug@^3.1.1: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -5393,13 +6080,6 @@ debug@^3.1.1: dependencies: ms "^2.1.1" -debug@^4.3.3, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -5425,6 +6105,11 @@ decamelize@^3.2.0: dependencies: xregexp "^4.2.4" +decamelize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" + integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== + decimal.js@^10.2.1, decimal.js@^10.3.1: version "10.3.1" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" @@ -5442,14 +6127,74 @@ decompress-response@^3.3.0: dependencies: mimic-response "^1.0.0" -dedent@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" - integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== + dependencies: + mimic-response "^3.1.0" -deep-eql@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df" +decompress-tar@^4.0.0, decompress-tar@^4.1.0, decompress-tar@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/decompress-tar/-/decompress-tar-4.1.1.tgz#718cbd3fcb16209716e70a26b84e7ba4592e5af1" + integrity sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ== + dependencies: + file-type "^5.2.0" + is-stream "^1.1.0" + tar-stream "^1.5.2" + +decompress-tarbz2@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz#3082a5b880ea4043816349f378b56c516be1a39b" + integrity sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A== + dependencies: + decompress-tar "^4.1.0" + file-type "^6.1.0" + is-stream "^1.1.0" + seek-bzip "^1.0.5" + unbzip2-stream "^1.0.9" + +decompress-targz@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/decompress-targz/-/decompress-targz-4.1.1.tgz#c09bc35c4d11f3de09f2d2da53e9de23e7ce1eee" + integrity sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w== + dependencies: + decompress-tar "^4.1.1" + file-type "^5.2.0" + is-stream "^1.1.0" + +decompress-unzip@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/decompress-unzip/-/decompress-unzip-4.0.1.tgz#deaaccdfd14aeaf85578f733ae8210f9b4848f69" + integrity sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw== + dependencies: + file-type "^3.8.0" + get-stream "^2.2.0" + pify "^2.3.0" + yauzl "^2.4.2" + +decompress@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/decompress/-/decompress-4.2.1.tgz#007f55cc6a62c055afa37c07eb6a4ee1b773f118" + integrity sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ== + dependencies: + decompress-tar "^4.0.0" + decompress-tarbz2 "^4.0.0" + decompress-targz "^4.0.0" + decompress-unzip "^4.0.1" + graceful-fs "^4.1.10" + make-dir "^1.0.0" + pify "^2.3.0" + strip-dirs "^2.0.0" + +dedent@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" + integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= + +deep-eql@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df" integrity sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw== dependencies: type-detect "^4.0.0" @@ -5466,6 +6211,29 @@ deep-equal@^1.0.1: object-keys "^1.1.1" regexp.prototype.flags "^1.2.0" +deep-equal@^2.0.5: + version "2.2.0" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.0.tgz#5caeace9c781028b9ff459f33b779346637c43e6" + integrity sha512-RdpzE0Hv4lhowpIUKKMJfeH6C1pXdtT1/it80ubgWqwI3qpuxUBpC1S4hnHg+zjnuOoDkzUtUCEEkG+XG5l3Mw== + dependencies: + call-bind "^1.0.2" + es-get-iterator "^1.1.2" + get-intrinsic "^1.1.3" + is-arguments "^1.1.1" + is-array-buffer "^3.0.1" + is-date-object "^1.0.5" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + isarray "^2.0.5" + object-is "^1.1.5" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.4.3" + side-channel "^1.0.4" + which-boxed-primitive "^1.0.2" + which-collection "^1.0.1" + which-typed-array "^1.1.9" + deep-extend@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" @@ -5476,6 +6244,11 @@ deep-is@^0.1.3, deep-is@~0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= +deepmerge@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.0.tgz#65491893ec47756d44719ae520e0e2609233b59b" + integrity sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og== + deepmerge@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" @@ -5500,6 +6273,11 @@ defer-to-connect@^1.0.1: resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== +defer-to-connect@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" + integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== + define-lazy-prop@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" @@ -5512,6 +6290,14 @@ define-properties@^1.1.3: dependencies: object-keys "^1.0.12" +define-properties@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" + integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA== + dependencies: + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + define-property@^0.2.5: version "0.2.5" resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" @@ -5603,6 +6389,35 @@ detect-node@^2.0.4: resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== +devtools-protocol@0.0.981744: + version "0.0.981744" + resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.981744.tgz#9960da0370284577d46c28979a0b32651022bacf" + integrity sha512-0cuGS8+jhR67Fy7qG3i3Pc7Aw494sb9yG9QgpG97SFVWwolgYjlhJg7n+UaHxOQT30d1TYu/EYe9k01ivLErIg== + +devtools-protocol@^0.0.1092731: + version "0.0.1092731" + resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1092731.tgz#ba3593dd915dadc821f60fbbeac0585a9fbfec59" + integrity sha512-T2LOqvUUDLB24Nr0krEjltTREnlPYSh2FeWuYRH1S8Y4KeUDvW30djvs4H7rNB1xtNAbYeRaF5J3G1LNnPQh7A== + +devtools@7.30.0: + version "7.30.0" + resolved "https://registry.yarnpkg.com/devtools/-/devtools-7.30.0.tgz#39146fcd0a3986dc5b347acf31b96ae0f33d3aa0" + integrity sha512-liC2nLMt/pEFTGwF+sCpciNPzQHsIfS+cQMBIBDWZBADBXLceftJxz1rBVrWsD3lM2t8dvpM4ZU7PiMScFDx8Q== + dependencies: + "@types/node" "^18.0.0" + "@types/ua-parser-js" "^0.7.33" + "@wdio/config" "7.30.0" + "@wdio/logger" "7.26.0" + "@wdio/protocols" "7.27.0" + "@wdio/types" "7.26.0" + "@wdio/utils" "7.26.0" + chrome-launcher "^0.15.0" + edge-paths "^2.1.0" + puppeteer-core "^13.1.3" + query-selector-shadow-dom "^1.0.0" + ua-parser-js "^1.0.1" + uuid "^9.0.0" + dezalgo@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" @@ -5621,11 +6436,31 @@ diff-sequences@^27.0.6: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.0.6.tgz#3305cb2e55a033924054695cc66019fd7f8e5723" integrity sha512-ag6wfpBFyNXZ0p8pcuIDS//D8H062ZQJ3fzYxjpmeKjnz8W4pekL3AI8VohmyZmsWW2PWaHgjsmqR6L13101VQ== +diff-sequences@^28.1.1: + version "28.1.1" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-28.1.1.tgz#9989dc731266dc2903457a70e996f3a041913ac6" + integrity sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw== + +diff-sequences@^29.3.1: + version "29.3.1" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.3.1.tgz#104b5b95fe725932421a9c6e5b4bef84c3f2249e" + integrity sha512-hlM3QR272NXCi4pq+N4Kok4kOp6EsgOM3ZSpJI7Da3UAs+Ttsi8MRmB6trM/lhyzUxGfOgnpkHtgqm5Q/CTcfQ== + +diff@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== +diff@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40" + integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw== + difflib@~0.2.1: version "0.2.4" resolved "https://registry.yarnpkg.com/difflib/-/difflib-0.2.4.tgz#b5e30361a6db023176d562892db85940a718f47e" @@ -5742,9 +6577,17 @@ domutils@^2.6.0: domelementtype "^2.2.0" domhandler "^4.2.0" +dot-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" + integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + dot-prop@^3.0.0, dot-prop@^4.2.1: version "4.2.1" - resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.1.tgz#45884194a71fc2cda71cbb4bceb3a4dd2f433ba4" + resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz#45884194a71fc2cda71cbb4bceb3a4dd2f433ba4" integrity sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ== dependencies: is-obj "^1.0.0" @@ -5768,6 +6611,23 @@ dotenv@^10.0.0, dotenv@~10.0.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== +download@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/download/-/download-8.0.0.tgz#afc0b309730811731aae9f5371c9f46be73e51b1" + integrity sha512-ASRY5QhDk7FK+XrQtQyvhpDKanLluEEQtWl/J7Lxuf/b+i8RYh997QeXvL85xitrmRKVlx9c7eTrcRdq2GS4eA== + dependencies: + archive-type "^4.0.0" + content-disposition "^0.5.2" + decompress "^4.2.1" + ext-name "^5.0.0" + file-type "^11.1.0" + filenamify "^3.0.0" + get-stream "^4.1.0" + got "^8.3.1" + make-dir "^2.1.0" + p-event "^2.1.0" + pify "^4.0.1" + dreamopt@~0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/dreamopt/-/dreamopt-0.6.0.tgz#d813ccdac8d39d8ad526775514a13dda664d6b4b" @@ -5797,6 +6657,15 @@ duplexer@^0.1.1, duplexer@^0.1.2: resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== +easy-table@*, easy-table@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/easy-table/-/easy-table-1.2.0.tgz#ba9225d7138fee307bfd4f0b5bc3c04bdc7c54eb" + integrity sha512-OFzVOv03YpvtcWGe5AayU5G2hgybsg3iqA6drU8UaoZyB9jLGMTrz9+asnLp/E+6qPh88yEI1gvyZFZ41dmgww== + dependencies: + ansi-regex "^5.0.1" + optionalDependencies: + wcwidth "^1.0.1" + ecs-logs-js@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/ecs-logs-js/-/ecs-logs-js-1.0.0.tgz#ff198bb3840483a2d7cbf49c08d091b471ea6258" @@ -5809,12 +6678,20 @@ ecs-logs-js@^1.0.0: replace-string "^3.0.0" serialize-error "^6.0.0" +edge-paths@^2.1.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/edge-paths/-/edge-paths-2.2.1.tgz#d2d91513225c06514aeac9843bfce546abbf4391" + integrity sha512-AI5fC7dfDmCdKo3m5y7PkYE8m6bMqR6pvVpgtrZkkhcJXFLelUgkjrhk3kXXx8Kbw2cRaTT4LkOR7hqf39KJdw== + dependencies: + "@types/which" "^1.3.2" + which "^2.0.2" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -ejs@^3.1.7: +ejs@^3.0.1, ejs@^3.1.7: version "3.1.8" resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.8.tgz#758d32910c78047585c7ef1f92f9ee041c1c190b" integrity sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ== @@ -5853,7 +6730,7 @@ encoding@^0.1.13: dependencies: iconv-lite "^0.6.2" -end-of-stream@^1.1.0, end-of-stream@^1.4.1: +end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== @@ -5928,6 +6805,21 @@ error-ex@^1.2.0, error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" +es-get-iterator@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6" + integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + has-symbols "^1.0.3" + is-arguments "^1.1.1" + is-map "^2.0.2" + is-set "^2.0.2" + is-string "^1.0.7" + isarray "^2.0.5" + stop-iteration-iterator "^1.0.0" + es-module-lexer@^0.9.0: version "0.9.3" resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" @@ -6288,6 +7180,14 @@ expand-brackets@^2.1.4: snapdragon "^0.8.1" to-regex "^3.0.1" +expect-webdriverio@^3.0.0: + version "3.5.3" + resolved "https://registry.yarnpkg.com/expect-webdriverio/-/expect-webdriverio-3.5.3.tgz#1f233de6f8abd76e1315f1e34d0a8cc5d34f28fc" + integrity sha512-pMecdn7JOde9ko0F6+v/DOAPTYg7FqmpXEx7dn0AST0+Z/RYj9DHqg40PrUBdHi4gsCHEUE6/ryalf0Nfo7bVQ== + dependencies: + expect "^28.1.0" + jest-matcher-utils "^28.1.0" + expect@^27.3.1: version "27.3.1" resolved "https://registry.yarnpkg.com/expect/-/expect-27.3.1.tgz#d0f170b1f5c8a2009bab0beffd4bb94f043e38e7" @@ -6300,6 +7200,28 @@ expect@^27.3.1: jest-message-util "^27.3.1" jest-regex-util "^27.0.6" +expect@^28.1.0: + version "28.1.3" + resolved "https://registry.yarnpkg.com/expect/-/expect-28.1.3.tgz#90a7c1a124f1824133dd4533cce2d2bdcb6603ec" + integrity sha512-eEh0xn8HlsuOBxFgIss+2mX85VAS4Qy3OSkjV7rlBWljtA4oWH37glVGyOZSZvErDT/yBywZdPGwCXuTvSG85g== + dependencies: + "@jest/expect-utils" "^28.1.3" + jest-get-type "^28.0.2" + jest-matcher-utils "^28.1.3" + jest-message-util "^28.1.3" + jest-util "^28.1.3" + +expect@^29.4.1: + version "29.4.1" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.4.1.tgz#58cfeea9cbf479b64ed081fd1e074ac8beb5a1fe" + integrity sha512-OKrGESHOaMxK3b6zxIq9SOW8kEXztKff/Dvg88j4xIJxur1hspEbedVkR3GpHe5LO+WB2Qw7OWN0RMTdp6as5A== + dependencies: + "@jest/expect-utils" "^29.4.1" + jest-get-type "^29.2.0" + jest-matcher-utils "^29.4.1" + jest-message-util "^29.4.1" + jest-util "^29.4.1" + express@^4.15.3, express@^4.17.1: version "4.17.1" resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" @@ -6336,6 +7258,58 @@ express@^4.15.3, express@^4.17.1: utils-merge "1.0.1" vary "~1.1.2" +express@^4.18.2: + version "4.18.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" + integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.1" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.5.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +ext-list@^2.0.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/ext-list/-/ext-list-2.2.2.tgz#0b98e64ed82f5acf0f2931babf69212ef52ddd37" + integrity sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA== + dependencies: + mime-db "^1.28.0" + +ext-name@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ext-name/-/ext-name-5.0.0.tgz#70781981d183ee15d13993c8822045c506c8f0a6" + integrity sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ== + dependencies: + ext-list "^2.0.0" + sort-keys-length "^1.0.0" + ext@^1.1.2: version "1.4.0" resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244" @@ -6391,6 +7365,17 @@ extract-stack@^2.0.0: resolved "https://registry.yarnpkg.com/extract-stack/-/extract-stack-2.0.0.tgz#11367bc865bfcd9bc0db3123e5edb57786f11f9b" integrity sha512-AEo4zm+TenK7zQorGK1f9mJ8L14hnTDi2ZQPR+Mub1NX8zimka1mXpV5LpH8x9HoUmFSHZCfLHqWvp0Y4FxxzQ== +extract-zip@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" + integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== + dependencies: + debug "^4.1.1" + get-stream "^5.1.0" + yauzl "^2.10.0" + optionalDependencies: + "@types/yauzl" "^2.9.1" + fancy-test@^1.4.3: version "1.4.10" resolved "https://registry.yarnpkg.com/fancy-test/-/fancy-test-1.4.10.tgz#310be93d4aa45d788bce56a573ae4d1b92b2e1a0" @@ -6405,6 +7390,11 @@ fancy-test@^1.4.3: nock "^13.0.0" stdout-stderr "^0.1.9" +fast-deep-equal@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" + integrity sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -6469,6 +7459,13 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g== + dependencies: + pend "~1.2.0" + figures@3.2.0, figures@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" @@ -6483,6 +7480,31 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" +file-type@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-11.1.0.tgz#93780f3fed98b599755d846b99a1617a2ad063b8" + integrity sha512-rM0UO7Qm9K7TWTtA6AShI/t7H5BPjDeGVDaNyg9BjHAj3PysKy7+8C8D137R88jnR3rFJZQB/tFgydl5sN5m7g== + +file-type@^3.8.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9" + integrity sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA== + +file-type@^4.2.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-4.4.0.tgz#1b600e5fca1fbdc6e80c0a70c71c8dba5f7906c5" + integrity sha512-f2UbFQEk7LXgWpi5ntcO86OeA/cC80fuDDDaX/fZ2ZGel+AF7leRQqBBW1eJNiiQkrZlAoM6P+VYP5P6bOlDEQ== + +file-type@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-5.2.0.tgz#2ddbea7c73ffe36368dfae49dc338c058c2b8ad6" + integrity sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ== + +file-type@^6.1.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-6.2.0.tgz#e50cd75d356ffed4e306dc4f5bcf52a79903a919" + integrity sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg== + filelist@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" @@ -6490,6 +7512,20 @@ filelist@^1.0.1: dependencies: minimatch "^5.0.1" +filename-reserved-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz#abf73dfab735d045440abfea2d91f389ebbfa229" + integrity sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ== + +filenamify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/filenamify/-/filenamify-3.0.0.tgz#9603eb688179f8c5d40d828626dcbb92c3a4672c" + integrity sha512-5EFZ//MsvJgXjBAFJ+Bh2YaCTRF/VP1YOmGrgt+KJ4SFRLjI87EIdwLLuT6wQX0I4F9W41xutobzczjsOKlI/g== + dependencies: + filename-reserved-regex "^2.0.0" + strip-outer "^1.0.0" + trim-repeated "^1.0.0" + fill-range@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" @@ -6530,6 +7566,19 @@ finalhandler@1.1.2, finalhandler@~1.1.2: statuses "~1.5.0" unpipe "~1.0.0" +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + find-cache-dir@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" @@ -6539,6 +7588,22 @@ find-cache-dir@^2.0.0: make-dir "^2.0.0" pkg-dir "^3.0.0" +find-up@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +find-up@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" + integrity sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA== + dependencies: + path-exists "^2.0.0" + pinkie-promise "^2.0.0" + find-up@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" @@ -6606,6 +7671,13 @@ follow-redirects@^1.15.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -6646,6 +7718,14 @@ fresh@0.5.2: resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= +from2@^2.1.1: + version "2.3.0" + resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" + integrity sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g== + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.0" + fs-constants@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" @@ -6729,6 +7809,11 @@ functional-red-black-tree@^1.0.1: resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +functions-have-names@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + gauge@^4.0.3: version "4.0.4" resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.4.tgz#52ff0652f2bbf607a989793d53b751bef2328dce" @@ -6743,6 +7828,13 @@ gauge@^4.0.3: strip-ansi "^6.0.1" wide-align "^1.1.5" +gaze@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.3.tgz#c441733e13b927ac8c0ff0b4c3b033f28812924a" + integrity sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g== + dependencies: + globule "^1.0.0" + gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -6767,6 +7859,15 @@ get-intrinsic@^1.0.2: has "^1.0.3" has-symbols "^1.0.1" +get-intrinsic@^1.1.1, get-intrinsic@^1.1.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f" + integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.3" + get-own-enumerable-property-symbols@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" @@ -6802,11 +7903,19 @@ get-stdin@^8.0.0: resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53" integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg== -get-stream@^3.0.0: +get-stream@3.0.0, get-stream@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= +get-stream@^2.2.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-2.3.1.tgz#5f38f93f346009666ee0150a054167f91bdd95de" + integrity sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA== + dependencies: + object-assign "^4.0.1" + pinkie-promise "^2.0.0" + get-stream@^4.0.0, get-stream@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" @@ -6896,7 +8005,7 @@ glob-parent@^5.1.1, glob-parent@^5.1.2, glob-parent@~5.1.2: glob-parent@^6.0.1: version "6.0.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== dependencies: is-glob "^4.0.3" @@ -6925,7 +8034,7 @@ glob@7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: +glob@7.2.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.2.0" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== @@ -6960,6 +8069,29 @@ glob@^8.0.1: minimatch "^5.0.1" once "^1.3.0" +glob@^8.0.3: + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + +glob@~7.1.1: + version "7.1.7" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" + integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" @@ -6998,6 +8130,44 @@ globby@^11.0.1, globby@^11.0.2, globby@^11.0.3, globby@^11.0.4: merge2 "^1.3.0" slash "^3.0.0" +globule@^1.0.0: + version "1.3.4" + resolved "https://registry.yarnpkg.com/globule/-/globule-1.3.4.tgz#7c11c43056055a75a6e68294453c17f2796170fb" + integrity sha512-OPTIfhMBh7JbBYDpa5b+Q5ptmMWKwcNcFSR/0c6t8V4f3ZAVBEsKNY37QdVqmLRYSMhOUGYrY0QhSoEpzGr/Eg== + dependencies: + glob "~7.1.1" + lodash "^4.17.21" + minimatch "~3.0.2" + +google-libphonenumber@^3.2.31: + version "3.2.31" + resolved "https://registry.yarnpkg.com/google-libphonenumber/-/google-libphonenumber-3.2.31.tgz#d2c4d4c8d7385be70b515086e4d28dd20da50600" + integrity sha512-l3bzAkfN4ITICKvuqEiY7JN06RxDAviOoKMtD2KfGYjGK3btPO8Xav7k0fgmf1Ud/pEm523yBh1/s/xDtKEvnw== + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +got@^11.0.2, got@^11.8.1, got@^11.8.2: + version "11.8.6" + resolved "https://registry.yarnpkg.com/got/-/got-11.8.6.tgz#276e827ead8772eddbcfc97170590b841823233a" + integrity sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g== + dependencies: + "@sindresorhus/is" "^4.0.0" + "@szmarczak/http-timer" "^4.0.5" + "@types/cacheable-request" "^6.0.1" + "@types/responselike" "^1.0.0" + cacheable-lookup "^5.0.3" + cacheable-request "^7.0.2" + decompress-response "^6.0.0" + http2-wrapper "^1.0.0-beta.5.2" + lowercase-keys "^2.0.0" + p-cancelable "^2.0.0" + responselike "^2.0.0" + got@^5.0.0: version "5.7.1" resolved "https://registry.yarnpkg.com/got/-/got-5.7.1.tgz#5f81635a61e4a6589f180569ea4e381680a51f35" @@ -7019,6 +8189,29 @@ got@^5.0.0: unzip-response "^1.0.2" url-parse-lax "^1.0.0" +got@^8.3.1: + version "8.3.2" + resolved "https://registry.yarnpkg.com/got/-/got-8.3.2.tgz#1d23f64390e97f776cac52e5b936e5f514d2e937" + integrity sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw== + dependencies: + "@sindresorhus/is" "^0.7.0" + cacheable-request "^2.1.1" + decompress-response "^3.3.0" + duplexer3 "^0.1.4" + get-stream "^3.0.0" + into-stream "^3.1.0" + is-retry-allowed "^1.1.0" + isurl "^1.0.0-alpha5" + lowercase-keys "^1.0.0" + mimic-response "^1.0.0" + p-cancelable "^0.4.0" + p-timeout "^2.0.1" + pify "^3.0.0" + safe-buffer "^5.1.1" + timed-out "^4.0.1" + url-parse-lax "^3.0.0" + url-to-options "^1.0.1" + got@^9.0.0: version "9.6.0" resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" @@ -7036,15 +8229,20 @@ got@^9.0.0: to-readable-stream "^1.0.0" url-parse-lax "^3.0.0" +graceful-fs@^4.1.10, graceful-fs@^4.2.9: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6: version "4.2.8" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== -graceful-fs@^4.2.9: - version "4.2.10" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" - integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== +grapheme-splitter@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" + integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== gzip-size@^6.0.0: version "6.0.0" @@ -7082,6 +8280,11 @@ has-ansi@^2.0.0: dependencies: ansi-regex "^2.0.0" +has-bigints@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -7092,11 +8295,35 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-property-descriptors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" + integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== + dependencies: + get-intrinsic "^1.1.1" + +has-symbol-support-x@^1.4.1: + version "1.4.2" + resolved "https://registry.yarnpkg.com/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz#1409f98bc00247da45da67cee0a36f282ff26455" + integrity sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw== + has-symbols@^1.0.1, has-symbols@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-to-string-tag-x@^1.2.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz#a045ab383d7b4b2012a00148ab0aa5f290044d4d" + integrity sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw== + dependencies: + has-symbol-support-x "^1.4.1" + has-tostringtag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" @@ -7147,6 +8374,27 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hash.js@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" + integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.1" + +he@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +header-case@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/header-case/-/header-case-2.0.4.tgz#5a42e63b55177349cf405beb8d775acabb92c063" + integrity sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q== + dependencies: + capital-case "^1.0.4" + tslib "^2.0.3" + "heap@>= 0.2.0": version "0.2.6" resolved "https://registry.yarnpkg.com/heap/-/heap-0.2.6.tgz#087e1f10b046932fc8594dd9e6d378afc9d1e5ac" @@ -7227,6 +8475,11 @@ htmlparser2@^6.1.0: domutils "^2.5.2" entities "^2.0.0" +http-cache-semantics@3.8.1: + version "3.8.1" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" + integrity sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w== + http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" @@ -7335,6 +8588,22 @@ http-proxy@^1.18.1: follow-redirects "^1.0.0" requires-port "^1.0.0" +http2-wrapper@^1.0.0-beta.5.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d" + integrity sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg== + dependencies: + quick-lru "^5.1.1" + resolve-alpn "^1.0.0" + +https-proxy-agent@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + https-proxy-agent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" @@ -7437,6 +8706,11 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= +indent-string@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" + integrity sha512-BYqTHXTGUIvg7t1r4sJNKcbDZkL92nkXA8YtRpbjFHRHGDL/NtUeiBJMeE60kIFN/Mg8ESaWQvftaYMGJzQZCQ== + indent-string@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" @@ -7483,6 +8757,27 @@ init-package-json@^3.0.2: validate-npm-package-license "^3.0.4" validate-npm-package-name "^4.0.0" +inquirer@8.2.4: + version "8.2.4" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.4.tgz#ddbfe86ca2f67649a67daa6f1051c128f684f0b4" + integrity sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg== + dependencies: + ansi-escapes "^4.2.1" + chalk "^4.1.1" + cli-cursor "^3.1.0" + cli-width "^3.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.21" + mute-stream "0.0.8" + ora "^5.4.1" + run-async "^2.4.0" + rxjs "^7.5.5" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + wrap-ansi "^7.0.0" + inquirer@^8.2.4: version "8.2.5" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.5.tgz#d8654a7542c35a9b9e069d27e2df4858784d54f8" @@ -7514,11 +8809,28 @@ internal-ip@^6.2.0: is-ip "^3.1.0" p-event "^4.2.0" +internal-slot@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.4.tgz#8551e7baf74a7a6ba5f749cfb16aa60722f0d6f3" + integrity sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ== + dependencies: + get-intrinsic "^1.1.3" + has "^1.0.3" + side-channel "^1.0.4" + interpret@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== +into-stream@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-3.1.0.tgz#96fb0a936c12babd6ff1752a17d05616abd094c6" + integrity sha512-TcdjPibTksa1NQximqep2r17ISRiNE9fwlfbg3F8ANdvP5/yrFTew86VcO//jk4QTaMlbjypPBq76HN2zaKfZQ== + dependencies: + from2 "^2.1.1" + p-is-promise "^1.1.0" + ip-regex@^4.0.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5" @@ -7563,7 +8875,7 @@ is-accessor-descriptor@^1.0.0: dependencies: kind-of "^6.0.0" -is-arguments@^1.0.4: +is-arguments@^1.0.4, is-arguments@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== @@ -7571,11 +8883,27 @@ is-arguments@^1.0.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-array-buffer@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.1.tgz#deb1db4fcae48308d54ef2442706c0393997052a" + integrity sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + is-typed-array "^1.1.10" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" @@ -7583,11 +8911,24 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + is-buffer@^1.1.5, is-buffer@~1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== +is-callable@^1.1.3: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + is-ci@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" @@ -7630,7 +8971,7 @@ is-data-descriptor@^1.0.0: dependencies: kind-of "^6.0.0" -is-date-object@^1.0.1: +is-date-object@^1.0.1, is-date-object@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== @@ -7728,11 +9069,28 @@ is-lambda@^1.0.1: resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" integrity sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU= +is-map@^2.0.1, is-map@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127" + integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg== + +is-natural-number@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-natural-number/-/is-natural-number-4.0.1.tgz#ab9d76e1db4ced51e35de0c72ebecf09f734cde8" + integrity sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ== + is-npm@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4" integrity sha1-8vtjpl5JBbQGyGBydloaTceTufQ= +is-number-object@^1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== + dependencies: + has-tostringtag "^1.0.0" + is-number@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" @@ -7755,6 +9113,11 @@ is-obj@^2.0.0: resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== +is-object@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.2.tgz#a56552e1c665c9e950b4a025461da87e72f86fcf" + integrity sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA== + is-path-cwd@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" @@ -7770,7 +9133,7 @@ is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= -is-plain-obj@^2.0.0: +is-plain-obj@^2.0.0, is-plain-obj@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== @@ -7807,7 +9170,7 @@ is-redirect@^1.0.0: resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" integrity sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ= -is-regex@^1.0.4: +is-regex@^1.0.4, is-regex@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== @@ -7830,6 +9193,18 @@ is-retry-allowed@^1.0.0, is-retry-allowed@^1.1.0, is-retry-allowed@^1.2.0: resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4" integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg== +is-set@^2.0.1, is-set@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec" + integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g== + +is-shared-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" + integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== + dependencies: + call-bind "^1.0.2" + is-ssh@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/is-ssh/-/is-ssh-1.4.0.tgz#4f8220601d2839d8fa624b3106f8e8884f01b8b2" @@ -7847,6 +9222,20 @@ is-stream@^2.0.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + is-text-path@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-text-path/-/is-text-path-1.0.1.tgz#4e1aa0fb51bfbcb3e92688001397202c1775b66e" @@ -7854,6 +9243,17 @@ is-text-path@^1.0.1: dependencies: text-extensions "^1.0.0" +is-typed-array@^1.1.10: + version "1.1.10" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.10.tgz#36a5b5cb4189b575d1a3e4b08536bfb485801e3f" + integrity sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.0" + is-typedarray@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -7864,6 +9264,24 @@ is-unicode-supported@^0.1.0: resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== +is-utf8@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + integrity sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q== + +is-weakmap@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2" + integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA== + +is-weakset@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.2.tgz#4569d67a747a1ce5a994dfd4ef6dcea76e7c0a1d" + integrity sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -7881,6 +9299,11 @@ isarray@1.0.0, isarray@~1.0.0: resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + isbinaryfile@^4.0.8: version "4.0.10" resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.10.tgz#0c5b5e30c2557a2f06febd37b7322946aaee42b3" @@ -7944,6 +9367,14 @@ istanbul-reports@^3.0.2: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" +isurl@^1.0.0-alpha5: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isurl/-/isurl-1.0.0.tgz#b27f4f49f3cdaa3ea44a0a5b7f3462e6edc39d67" + integrity sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w== + dependencies: + has-to-string-tag-x "^1.2.0" + is-object "^1.0.1" + jake@^10.8.5: version "10.8.5" resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46" @@ -8053,6 +9484,26 @@ jest-diff@^27.0.0, jest-diff@^27.3.1: jest-get-type "^27.3.1" pretty-format "^27.3.1" +jest-diff@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-28.1.3.tgz#948a192d86f4e7a64c5264ad4da4877133d8792f" + integrity sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw== + dependencies: + chalk "^4.0.0" + diff-sequences "^28.1.1" + jest-get-type "^28.0.2" + pretty-format "^28.1.3" + +jest-diff@^29.4.1: + version "29.4.1" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.4.1.tgz#9a6dc715037e1fa7a8a44554e7d272088c4029bd" + integrity sha512-uazdl2g331iY56CEyfbNA0Ut7Mn2ulAG5vUaEHXycf1L6IPyuImIxSz4F0VYBKi7LYIuxOwTZzK3wh5jHzASMw== + dependencies: + chalk "^4.0.0" + diff-sequences "^29.3.1" + jest-get-type "^29.2.0" + pretty-format "^29.4.1" + jest-docblock@^27.0.6: version "27.0.6" resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-27.0.6.tgz#cc78266acf7fe693ca462cbbda0ea4e639e4e5f3" @@ -8101,6 +9552,16 @@ jest-get-type@^27.3.1: resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.3.1.tgz#a8a2b0a12b50169773099eee60a0e6dd11423eff" integrity sha512-+Ilqi8hgHSAdhlQ3s12CAVNd8H96ZkQBfYoXmArzZnOfAtVAJEiPDBirjByEblvG/4LPJmkL+nBqPO3A1YJAEg== +jest-get-type@^28.0.2: + version "28.0.2" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-28.0.2.tgz#34622e628e4fdcd793d46db8a242227901fcf203" + integrity sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA== + +jest-get-type@^29.2.0: + version "29.2.0" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.2.0.tgz#726646f927ef61d583a3b3adb1ab13f3a5036408" + integrity sha512-uXNJlg8hKFEnDgFsrCjznB+sTxdkuqiCL6zMgA75qEbAJjJYTs9XPrvDctrEig2GDow22T/LvHgO57iJhXB/UA== + jest-haste-map@^27.3.1: version "27.3.1" resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.3.1.tgz#7656fbd64bf48bda904e759fc9d93e2c807353ee" @@ -8163,6 +9624,26 @@ jest-matcher-utils@^27.3.1: jest-get-type "^27.3.1" pretty-format "^27.3.1" +jest-matcher-utils@^28.1.0, jest-matcher-utils@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-28.1.3.tgz#5a77f1c129dd5ba3b4d7fc20728806c78893146e" + integrity sha512-kQeJ7qHemKfbzKoGjHHrRKH6atgxMk8Enkk2iPQ3XwO6oE/KYD8lMYOziCkeSB9G4adPM4nR1DE8Tf5JeWH6Bw== + dependencies: + chalk "^4.0.0" + jest-diff "^28.1.3" + jest-get-type "^28.0.2" + pretty-format "^28.1.3" + +jest-matcher-utils@^29.4.1: + version "29.4.1" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.4.1.tgz#73d834e305909c3b43285fbc76f78bf0ad7e1954" + integrity sha512-k5h0u8V4nAEy6lSACepxL/rw78FLDkBnXhZVgFneVpnJONhb2DhZj/Gv4eNe+1XqQ5IhgUcqj745UwH0HJmMnA== + dependencies: + chalk "^4.0.0" + jest-diff "^29.4.1" + jest-get-type "^29.2.0" + pretty-format "^29.4.1" + jest-message-util@^27.3.1: version "27.3.1" resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-27.3.1.tgz#f7c25688ad3410ab10bcb862bcfe3152345c6436" @@ -8178,6 +9659,36 @@ jest-message-util@^27.3.1: slash "^3.0.0" stack-utils "^2.0.3" +jest-message-util@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-28.1.3.tgz#232def7f2e333f1eecc90649b5b94b0055e7c43d" + integrity sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^28.1.3" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^28.1.3" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-message-util@^29.4.1: + version "29.4.1" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.4.1.tgz#522623aa1df9a36ebfdffb06495c7d9d19e8a845" + integrity sha512-H4/I0cXUaLeCw6FM+i4AwCnOwHRgitdaUFOdm49022YD5nfyr8C/DrbXOBEyJaj+w/y0gGJ57klssOaUiLLQGQ== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^29.4.1" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^29.4.1" + slash "^3.0.0" + stack-utils "^2.0.3" + jest-mock@^27.3.0: version "27.3.0" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.3.0.tgz#ddf0ec3cc3e68c8ccd489bef4d1f525571a1b867" @@ -8340,6 +9851,18 @@ jest-util@^27.0.0, jest-util@^27.3.1: graceful-fs "^4.2.4" picomatch "^2.2.3" +jest-util@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-28.1.3.tgz#f4f932aa0074f0679943220ff9cbba7e497028b0" + integrity sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ== + dependencies: + "@jest/types" "^28.1.3" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + jest-util@^29.3.1: version "29.3.1" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.3.1.tgz#1dda51e378bbcb7e3bc9d8ab651445591ed373e1" @@ -8352,6 +9875,18 @@ jest-util@^29.3.1: graceful-fs "^4.2.9" picomatch "^2.2.3" +jest-util@^29.4.1: + version "29.4.1" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.4.1.tgz#2eeed98ff4563b441b5a656ed1a786e3abc3e4c4" + integrity sha512-bQy9FPGxVutgpN4VRc0hk6w7Hx/m6L53QxpDreTZgJd9gfx/AV2MjyPde9tGyZRINAUrSv57p2inGBu2dRLmkQ== + dependencies: + "@jest/types" "^29.4.1" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + jest-validate@^27.3.1: version "27.3.1" resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-27.3.1.tgz#3a395d61a19cd13ae9054af8cdaf299116ef8a24" @@ -8555,6 +10090,11 @@ json-buffer@3.0.0: resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + json-diff@^0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/json-diff/-/json-diff-0.5.4.tgz#7bc8198c441756632aab66c7d9189d365a7a035a" @@ -8747,6 +10287,13 @@ karma@^6.4.1: ua-parser-js "^0.7.30" yargs "^16.1.1" +keyv@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.0.0.tgz#44923ba39e68b12a7cec7df6c3268c031f2ef373" + integrity sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA== + dependencies: + json-buffer "3.0.0" + keyv@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" @@ -8754,6 +10301,13 @@ keyv@^3.0.0: dependencies: json-buffer "3.0.0" +keyv@^4.0.0: + version "4.5.2" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.2.tgz#0e310ce73bf7851ec702f2eaf46ec4e3805cce56" + integrity sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g== + dependencies: + json-buffer "3.0.1" + kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" @@ -8793,6 +10347,11 @@ klona@^2.0.4: resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0" integrity sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA== +ky@0.30.0: + version "0.30.0" + resolved "https://registry.yarnpkg.com/ky/-/ky-0.30.0.tgz#a3d293e4f6c4604a9a4694eceb6ce30e73d27d64" + integrity sha512-X/u76z4JtDVq10u1JA5UQfatPxgPaVDMYTrgHyiTpGN2z4TMEJkIHsoSBBSg9SWZEIXTKsi9kHgiQ9o3Y/4yog== + latest-version@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-2.0.0.tgz#56f8d6139620847b8017f8f1f4d78e211324168b" @@ -8800,6 +10359,13 @@ latest-version@^2.0.0: dependencies: package-json "^2.0.0" +lazystream@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.1.tgz#494c831062f1f9408251ec44db1cba29242a2638" + integrity sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw== + dependencies: + readable-stream "^2.0.5" + lerna@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/lerna/-/lerna-6.0.3.tgz#0364eadeedbdf5ade375d8a6f0a87bb9003f6019" @@ -8871,6 +10437,14 @@ libnpmpublish@^6.0.4: semver "^7.3.7" ssri "^9.0.0" +lighthouse-logger@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/lighthouse-logger/-/lighthouse-logger-1.3.0.tgz#ba6303e739307c4eee18f08249524e7dafd510db" + integrity sha512-BbqAKApLb9ywUli+0a+PcV04SyJ/N1q/8qgCNe6U97KbPCS1BTksEuHFLYdvc8DltuhfxIUBqDZsC0bBGtl3lA== + dependencies: + debug "^2.6.9" + marky "^1.2.2" + lilconfig@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.3.tgz#68f3005e921dafbd2a2afb48379986aa6d2579fd" @@ -8920,6 +10494,17 @@ listr2@^3.2.2: through "^2.3.8" wrap-ansi "^7.0.0" +load-json-file@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" + integrity sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A== + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + pinkie-promise "^2.0.0" + strip-bom "^2.0.0" + load-json-file@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" @@ -8968,6 +10553,13 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + lodash._reinterpolate@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" @@ -8983,6 +10575,26 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ== + +lodash.difference@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c" + integrity sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA== + +lodash.flatten@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" + integrity sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g== + +lodash.flattendeep@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" + integrity sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ== + lodash.get@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" @@ -8993,16 +10605,31 @@ lodash.ismatch@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37" integrity sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc= +lodash.isobject@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d" + integrity sha512-3/Qptq2vr7WeJbB4KHUSKlq8Pl7ASXi3UG6CMbBm8WRtXi8+GHm7mKaU3urfpSEzWe2wCIChs6/sdocUsTKJiA== + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + lodash.memoize@4.x, lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= -lodash.merge@^4.6.2: +lodash.merge@^4.6.1, lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash.pickby@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.pickby/-/lodash.pickby-4.6.0.tgz#7dea21d8c18d7703a27c704c15d3b84a67e33aff" + integrity sha512-AZV+GsS/6ckvPOVQPXSiFFacKvKB4kOQu6ynt9wz0F3LO4R9Ij4K1ddYsIytDpSgLz88JHd9P+oaLeej5/Sl7Q== + lodash.set@^4.3.2: version "4.3.2" resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" @@ -9028,17 +10655,27 @@ lodash.truncate@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= +lodash.union@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" + integrity sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw== + lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= +lodash.zip@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.zip/-/lodash.zip-4.2.0.tgz#ec6662e4896408ed4ab6c542a3990b72cc080020" + integrity sha512-C7IOaBBK/0gMORRBd8OETNx3kmOkgIWIPvyDpZSCTwUrpYmgZwJkjZeOD8ww4xbOUOs4/attY+pciKvadNfFbg== + lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -log-symbols@^4.0.0, log-symbols@^4.1.0: +log-symbols@4.1.0, log-symbols@^4.0.0, log-symbols@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== @@ -9067,11 +10704,33 @@ log4js@^6.4.1: rfdc "^1.3.0" streamroller "^3.1.3" +loglevel-plugin-prefix@^0.8.4: + version "0.8.4" + resolved "https://registry.yarnpkg.com/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.4.tgz#2fe0e05f1a820317d98d8c123e634c1bd84ff644" + integrity sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g== + +loglevel@^1.6.0: + version "1.8.1" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.1.tgz#5c621f83d5b48c54ae93b6156353f555963377b4" + integrity sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg== + logrocket@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/logrocket/-/logrocket-3.0.1.tgz#c746e8df3d5fee999b152975e51db1908357a6f0" integrity sha512-jOWG+jEzobKVxGytzZ+4KGm2kiMQMRTHab2uDWQyVZcHfEF38BlCH1yjQVY4LCmuQUwZitP9biMzJZnyUQ0dtQ== +lower-case@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" + integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== + dependencies: + tslib "^2.0.3" + +lowercase-keys@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" + integrity sha512-RPlX0+PHuvxVDZ7xX+EBVAp4RsVxP/TdDSN2mJYdiq1Lc4Hz7EUSjUI7RZrKKlmrIzVhf6Jo2stj7++gVarS0A== + lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" @@ -9109,6 +10768,13 @@ lru-queue@^0.1.0: dependencies: es5-ext "~0.10.2" +make-dir@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" + integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== + dependencies: + pify "^3.0.0" + make-dir@^2.0.0, make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -9180,6 +10846,11 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +marky@^1.2.2: + version "1.2.5" + resolved "https://registry.yarnpkg.com/marky/-/marky-1.2.5.tgz#55796b688cbd72390d2d399eaaf1832c9413e3c0" + integrity sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q== + math-abs@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/math-abs/-/math-abs-1.0.2.tgz#8fb2675d969327a61a629821fc23e41faac5c4d3" @@ -9370,6 +11041,11 @@ mime-db@1.50.0, "mime-db@>= 1.43.0 < 2": resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.50.0.tgz#abd4ac94e98d3c0e185016c67ab45d5fde40c11f" integrity sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A== +mime-db@1.52.0, mime-db@^1.28.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + mime-db@~1.33.0: version "1.33.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" @@ -9389,6 +11065,13 @@ mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, dependencies: mime-db "1.50.0" +mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + mime@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" @@ -9414,12 +11097,17 @@ mimic-response@^1.0.0, mimic-response@^1.0.1: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== + min-indent@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== -minimalistic-assert@^1.0.0: +minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== @@ -9438,7 +11126,14 @@ minimatch@3.0.5: dependencies: brace-expansion "^1.1.7" -minimatch@^3.1.1: +minimatch@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" + integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^3.0.5, minimatch@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -9452,6 +11147,27 @@ minimatch@^5.0.1: dependencies: brace-expansion "^2.0.1" +minimatch@^5.1.0: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^6.0.4: + version "6.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-6.1.6.tgz#5384bb324be5b5dae12a567c03d22908febd0ddd" + integrity sha512-6bR3UIeh/DF8+p6A9Spyuy67ShOq42rOkHWi7eUe3Ua99Zo5lZfGC6lJJWkeoK4k9jQFT3Pl7czhTXimG2XheA== + dependencies: + brace-expansion "^2.0.1" + +minimatch@~3.0.2: + version "3.0.8" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.8.tgz#5e6a59bd11e2ab0de1cfb843eb2d82e546c321c1" + integrity sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q== + dependencies: + brace-expansion "^1.1.7" + minimist-options@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" @@ -9569,6 +11285,33 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +mocha@^10.0.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.2.0.tgz#1fd4a7c32ba5ac372e03a17eef435bd00e5c68b8" + integrity sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg== + dependencies: + ansi-colors "4.1.1" + browser-stdout "1.3.1" + chokidar "3.5.3" + debug "4.3.4" + diff "5.0.0" + escape-string-regexp "4.0.0" + find-up "5.0.0" + glob "7.2.0" + he "1.2.0" + js-yaml "4.1.0" + log-symbols "4.1.0" + minimatch "5.0.1" + ms "2.1.3" + nanoid "3.3.3" + serialize-javascript "6.0.0" + strip-json-comments "3.1.1" + supports-color "8.1.1" + workerpool "6.2.1" + yargs "16.2.0" + yargs-parser "20.2.4" + yargs-unparser "2.0.0" + mock-stdin@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/mock-stdin/-/mock-stdin-1.0.0.tgz#efcfaf4b18077e14541742fd758b9cae4e5365ea" @@ -9594,7 +11337,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@^2.0.0, ms@^2.1.1: +ms@2.1.3, ms@^2.0.0, ms@^2.1.1: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -9642,6 +11385,11 @@ mz@^2.7.0: object-assign "^4.0.1" thenify-all "^1.0.0" +nanoid@3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25" + integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w== + nanoid@^2.1.7: version "2.1.11" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.1.11.tgz#ec24b8a758d591561531b4176a01e3ab4f0f0280" @@ -9691,7 +11439,7 @@ negotiator@0.6.2: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== -negotiator@^0.6.3: +negotiator@0.6.3, negotiator@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== @@ -9723,6 +11471,14 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +no-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" + integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== + dependencies: + lower-case "^2.0.2" + tslib "^2.0.3" + nock@^13.0.0, nock@^13.1.4: version "13.1.4" resolved "https://registry.yarnpkg.com/nock/-/nock-13.1.4.tgz#367c917d4c532a889404b85ade92762c29e80262" @@ -9750,7 +11506,7 @@ node-fetch@2.6.1, node-fetch@^2.6.1: resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== -node-fetch@^2.6.7: +node-fetch@2.6.7, node-fetch@^2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== @@ -9852,6 +11608,15 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== +normalize-url@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-2.0.1.tgz#835a9da1551fa26f70e92329069a23aa6574d7e6" + integrity sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw== + dependencies: + prepend-http "^2.0.0" + query-string "^5.0.1" + sort-keys "^2.0.0" + normalize-url@^4.1.0: version "4.5.1" resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" @@ -10032,7 +11797,7 @@ obj-case@0.2.1: resolved "https://registry.yarnpkg.com/obj-case/-/obj-case-0.2.1.tgz#13a554d04e5ca32dfd9d566451fd2b0e11007f1a" integrity sha512-PquYBBTy+Y6Ob/O2574XHhDtHJlV1cJHMCgW+rDRc9J5hhmRelJB3k5dTK/3cVmFVtzvAKuENeuLpoyTzMzkOg== -object-assign@^4, object-assign@^4.0.1: +object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= @@ -10046,12 +11811,12 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" -object-inspect@^1.9.0: +object-inspect@^1.10.3, object-inspect@^1.9.0: version "1.12.3" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== -object-is@^1.0.1: +object-is@^1.0.1, object-is@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== @@ -10086,6 +11851,16 @@ object.assign@^4.1.0: has-symbols "^1.0.1" object-keys "^1.1.1" +object.assign@^4.1.4: + version "4.1.4" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" + integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + has-symbols "^1.0.3" + object-keys "^1.1.1" + object.pick@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" @@ -10216,11 +11991,28 @@ osenv@^0.1.0: os-homedir "^1.0.0" os-tmpdir "^1.0.0" +p-cancelable@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.4.1.tgz#35f363d67d52081c8d9585e37bcceb7e0bbcb2a0" + integrity sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ== + p-cancelable@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== +p-cancelable@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf" + integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg== + +p-event@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/p-event/-/p-event-2.3.1.tgz#596279ef169ab2c3e0cae88c1cfbb08079993ef6" + integrity sha512-NQCqOFhbpVTMX4qMe8PF8lbGtzZ+LCiN7pcNrb/413Na7+TRoe1xkKUzuWa/YEJdGQ0FvKtj35EEbDoVPO2kbA== + dependencies: + p-timeout "^2.0.1" + p-event@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/p-event/-/p-event-4.2.0.tgz#af4b049c8acd91ae81083ebd1e6f5cae2044c1b5" @@ -10233,6 +12025,16 @@ p-finally@^1.0.0: resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= +p-is-promise@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e" + integrity sha512-zL7VE4JVS2IFSkR2GQKDSPEVxkoH43/p7oEnwpdCndKYJO0HVeRB7fA8TJwuLOTBREtK0ea8eHaxdwcpob5dmg== + +p-iteration@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/p-iteration/-/p-iteration-1.1.8.tgz#14df726d55af368beba81bcc92a26bb1b48e714a" + integrity sha512-IMFBSDIYcPNnW7uWYGrBqmvTiq7W0uB0fJn6shQZs7dlF3OvrHOre+JT9ikSZ7gZS3vWqclVgoQSvToJrns7uQ== + p-limit@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" @@ -10275,6 +12077,13 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + p-map-series@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/p-map-series/-/p-map-series-2.1.0.tgz#7560d4c452d9da0c07e692fdbfe6e2c81a2a91f2" @@ -10313,6 +12122,13 @@ p-retry@^4.5.0: "@types/retry" "^0.12.0" retry "^0.13.1" +p-timeout@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-2.0.1.tgz#d8dd1979595d2dc0139e1fe46b8b646cb3cdf038" + integrity sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA== + dependencies: + p-finally "^1.0.0" + p-timeout@^3.1.0, p-timeout@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" @@ -10374,6 +12190,14 @@ pacote@^13.0.3, pacote@^13.6.1: ssri "^9.0.0" tar "^6.1.11" +param-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" + integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -10390,7 +12214,7 @@ parse-conflict-json@^2.0.1: just-diff "^5.0.1" just-diff-apply "^5.2.0" -parse-json@^2.1.0: +parse-json@^2.1.0, parse-json@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= @@ -10415,6 +12239,11 @@ parse-json@^5.0.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" +parse-ms@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-2.1.0.tgz#348565a753d4391fa524029956b172cb7753097d" + integrity sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA== + parse-path@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/parse-path/-/parse-path-7.0.0.tgz#605a2d58d0a749c8594405d8cc3a2bf76d16099b" @@ -10446,6 +12275,14 @@ parseurl@~1.3.2, parseurl@~1.3.3: resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== +pascal-case@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb" + integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + pascalcase@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" @@ -10464,6 +12301,21 @@ path-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== +path-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/path-case/-/path-case-3.0.4.tgz#9168645334eb942658375c56f80b4c0cb5f82c6f" + integrity sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" + +path-exists@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" + integrity sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ== + dependencies: + pinkie-promise "^2.0.0" + path-exists@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" @@ -10514,6 +12366,15 @@ path-to-regexp@^6.1.0: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.0.tgz#f7b3803336104c346889adece614669230645f38" integrity sha512-f66KywYG6+43afgE/8j/GoiNyygk/bnoCbps++3ErRKsIYkGGupyv07R2Ok5m9i67Iqc+T2g1eAUGUPzWhYTyg== +path-type@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" + integrity sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg== + dependencies: + graceful-fs "^4.1.2" + pify "^2.0.0" + pinkie-promise "^2.0.0" + path-type@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" @@ -10541,6 +12402,11 @@ pem@^1.9.7: os-tmpdir "^1.0.1" which "^2.0.2" +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== + picocolors@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f" @@ -10556,7 +12422,7 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== -pify@^2.3.0: +pify@^2.0.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= @@ -10595,6 +12461,13 @@ pirates@^4.0.0, pirates@^4.0.1: dependencies: node-modules-regexp "^1.0.0" +pkg-dir@4.2.0, pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + pkg-dir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" @@ -10602,13 +12475,6 @@ pkg-dir@^3.0.0: dependencies: find-up "^3.0.0" -pkg-dir@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" - integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== - dependencies: - find-up "^4.0.0" - pkginfo@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.3.1.tgz#5b29f6a81f70717142e09e765bbeab97b4f81e21" @@ -10952,6 +12818,32 @@ pretty-format@^27.0.0, pretty-format@^27.3.1: ansi-styles "^5.0.0" react-is "^17.0.1" +pretty-format@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-28.1.3.tgz#c9fba8cedf99ce50963a11b27d982a9ae90970d5" + integrity sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q== + dependencies: + "@jest/schemas" "^28.1.3" + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^18.0.0" + +pretty-format@^29.4.1: + version "29.4.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.4.1.tgz#0da99b532559097b8254298da7c75a0785b1751c" + integrity sha512-dt/Z761JUVsrIKaY215o1xQJBGlSmTx/h4cSqXqjHLnU1+Kt+mavVE7UgqJJO5ukx5HjSswHfmXz4LjS2oIJfg== + dependencies: + "@jest/schemas" "^29.4.0" + ansi-styles "^5.0.0" + react-is "^18.0.0" + +pretty-ms@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-7.0.1.tgz#7d903eaab281f7d8e03c66f867e239dc32fb73e8" + integrity sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q== + dependencies: + parse-ms "^2.1.0" + proc-log@^2.0.0, proc-log@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-2.0.1.tgz#8f3f69a1f608de27878f91f5c688b225391cb685" @@ -10967,7 +12859,7 @@ process@^0.11.10: resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== -progress@^2.0.0: +progress@2.0.3, progress@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== @@ -11025,7 +12917,7 @@ protocols@^2.0.0, protocols@^2.0.1: resolved "https://registry.yarnpkg.com/protocols/-/protocols-2.0.1.tgz#8f155da3fc0f32644e83c5782c8e8212ccf70a86" integrity sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q== -proxy-addr@~2.0.5: +proxy-addr@~2.0.5, proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== @@ -11033,7 +12925,7 @@ proxy-addr@~2.0.5: forwarded "0.2.0" ipaddr.js "1.9.1" -proxy-from-env@^1.1.0: +proxy-from-env@1.1.0, proxy-from-env@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== @@ -11071,6 +12963,24 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +puppeteer-core@^13.1.3: + version "13.7.0" + resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-13.7.0.tgz#3344bee3994163f49120a55ddcd144a40575ba5b" + integrity sha512-rXja4vcnAzFAP1OVLq/5dWNfwBGuzcOARJ6qGV7oAZhnLmVRU8G5MsdeQEAOy332ZhkIOnn9jp15R89LKHyp2Q== + dependencies: + cross-fetch "3.1.5" + debug "4.3.4" + devtools-protocol "0.0.981744" + extract-zip "2.0.1" + https-proxy-agent "5.0.1" + pkg-dir "4.2.0" + progress "2.0.3" + proxy-from-env "1.1.0" + rimraf "3.0.2" + tar-fs "2.1.1" + unbzip2-stream "1.4.3" + ws "8.5.0" + pvtsutils@^1.2.0, pvtsutils@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.2.1.tgz#8212e846ca9afb21e40cebb0691755649f9f498a" @@ -11124,6 +13034,20 @@ qs@6.7.0: resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== +query-selector-shadow-dom@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz#1c7b0058eff4881ac44f45d8f84ede32e9a2f349" + integrity sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw== + +query-string@^5.0.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb" + integrity sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw== + dependencies: + decode-uri-component "^0.2.0" + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" + query-string@^6.10.1: version "6.14.1" resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.14.1.tgz#7ac2dca46da7f309449ba0f86b1fd28255b0c86a" @@ -11154,6 +13078,11 @@ quick-lru@^4.0.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== +quick-lru@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" + integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== + randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -11206,6 +13135,11 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +react-is@^18.0.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" + integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== + read-all-stream@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/read-all-stream/-/read-all-stream-3.1.0.tgz#35c3e177f2078ef789ee4bfafa4373074eaef4fa" @@ -11237,6 +13171,14 @@ read-package-json@^5.0.0, read-package-json@^5.0.1: normalize-package-data "^4.0.0" npm-normalize-package-bin "^2.0.0" +read-pkg-up@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" + integrity sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A== + dependencies: + find-up "^1.0.0" + read-pkg "^1.0.0" + read-pkg-up@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07" @@ -11254,6 +13196,15 @@ read-pkg-up@^7.0.1: read-pkg "^5.2.0" type-fest "^0.8.1" +read-pkg@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" + integrity sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ== + dependencies: + load-json-file "^1.0.0" + normalize-package-data "^2.3.2" + path-type "^1.0.0" + read-pkg@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" @@ -11289,7 +13240,7 @@ readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stre string_decoder "^1.1.1" util-deprecate "^1.0.1" -readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@~2.3.6: +readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.3.0, readable-stream@^2.3.5, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -11302,6 +13253,13 @@ readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable string_decoder "~1.1.1" util-deprecate "~1.0.1" +readdir-glob@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.2.tgz#b185789b8e6a43491635b6953295c5c5e3fd224c" + integrity sha512-6RLVvwJtVwEDfPdn6X6Ille4/lxGl0ATOY4FN/B9nxQcgOazvvI0nodiD19ScKq0PvA/29VpaOQML36o5IzZWA== + dependencies: + minimatch "^5.1.0" + readdir-scoped-modules@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz#8d45407b4f870a0dcaebc0e28670d18e74514309" @@ -11336,6 +13294,13 @@ rechoir@^0.7.0: dependencies: resolve "^1.9.0" +recursive-readdir@^2.2.2: + version "2.2.3" + resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.3.tgz#e726f328c0d69153bcabd5c322d3195252379372" + integrity sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA== + dependencies: + minimatch "^3.0.5" + redent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" @@ -11391,6 +13356,15 @@ regexp.prototype.flags@^1.2.0: call-bind "^1.0.2" define-properties "^1.1.3" +regexp.prototype.flags@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" + integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + functions-have-names "^1.2.2" + regexpp@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" @@ -11480,6 +13454,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= +resolve-alpn@^1.0.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9" + integrity sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g== + resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" @@ -11515,13 +13494,27 @@ resolve@^1.10.0, resolve@^1.14.2, resolve@^1.20.0, resolve@^1.9.0: is-core-module "^2.2.0" path-parse "^1.0.6" -responselike@^1.0.2: +responselike@1.0.2, responselike@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec= dependencies: lowercase-keys "^1.0.0" +responselike@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.1.tgz#9a0bc8fdc252f3fb1cca68b016591059ba1422bc" + integrity sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw== + dependencies: + lowercase-keys "^2.0.0" + +resq@^1.9.1: + version "1.11.0" + resolved "https://registry.yarnpkg.com/resq/-/resq-1.11.0.tgz#edec8c58be9af800fd628118c0ca8815283de196" + integrity sha512-G10EBz+zAAy3zUd/CDoBbXRL6ia9kOo3xRHrMDsHljI0GDkhYlyjwoCx5+3eCC4swi1uCoZQhskuJkj7Gp57Bw== + dependencies: + fast-deep-equal "^2.0.1" + restore-cursor@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" @@ -11555,20 +13548,25 @@ rfdc@^1.3.0: resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== -rimraf@^2.6.3: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" +rgb2hex@0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/rgb2hex/-/rgb2hex-0.2.5.tgz#f82230cd3ab1364fa73c99be3a691ed688f8dbdc" + integrity sha512-22MOP1Rh7sAo1BZpDG6R5RFYzR2lYEgwq7HEmyW2qcsOqR2lQKmn+O//xV3YG/0rrhMC6KVX2hU+ZXuaw9a5bw== -rimraf@^3.0.0, rimraf@^3.0.2: +rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== dependencies: glob "^7.1.3" +rimraf@^2.6.3: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + rimraf@~2.6.2: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" @@ -11607,7 +13605,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -11624,6 +13622,19 @@ safe-regex@^1.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +saucelabs@^7.1.3: + version "7.2.0" + resolved "https://registry.yarnpkg.com/saucelabs/-/saucelabs-7.2.0.tgz#82394a785e4cdc97270d1cd6b71dee9995cdbf91" + integrity sha512-87NDR3J5aH+mOSO9MY49ukxd2+PpvqYghAA0Vr6CHHw/0yuKtF94henVMdnC1HxpWe9agBizV+Gh9Bi98qzQow== + dependencies: + change-case "^4.1.2" + download "^8.0.0" + form-data "^4.0.0" + got "^11.8.2" + hash.js "^1.1.7" + tunnel "^0.0.6" + yargs "^17.2.1" + saxes@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d" @@ -11640,6 +13651,13 @@ schema-utils@^3.0.0, schema-utils@^3.1.0, schema-utils@^3.1.1: ajv "^6.12.5" ajv-keywords "^3.5.2" +seek-bzip@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/seek-bzip/-/seek-bzip-1.0.6.tgz#35c4171f55a680916b52a07859ecf3b5857f21c4" + integrity sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ== + dependencies: + commander "^2.8.1" + select-hose@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" @@ -11719,6 +13737,34 @@ send@0.17.1: range-parser "~1.2.1" statuses "~1.5.0" +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +sentence-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/sentence-case/-/sentence-case-3.0.4.tgz#3645a7b8c117c787fde8702056225bb62a45131f" + integrity sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + upper-case-first "^2.0.2" + serialize-error@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-6.0.0.tgz#ccfb887a1dd1c48d6d52d7863b92544331fd752b" @@ -11726,20 +13772,27 @@ serialize-error@^6.0.0: dependencies: type-fest "^0.12.0" -serialize-javascript@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-5.0.1.tgz#7886ec848049a462467a97d3d918ebb2aaf934f4" - integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA== +serialize-error@^8.0.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-8.1.0.tgz#3a069970c712f78634942ddd50fbbc0eaebe2f67" + integrity sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ== dependencies: - randombytes "^2.1.0" + type-fest "^0.20.2" -serialize-javascript@^6.0.0: +serialize-javascript@6.0.0, serialize-javascript@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== dependencies: randombytes "^2.1.0" +serialize-javascript@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-5.0.1.tgz#7886ec848049a462467a97d3d918ebb2aaf934f4" + integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA== + dependencies: + randombytes "^2.1.0" + serve-handler@6.1.3: version "6.1.3" resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.3.tgz#1bf8c5ae138712af55c758477533b9117f6435e8" @@ -11777,6 +13830,16 @@ serve-static@1.14.1: parseurl "~1.3.3" send "0.17.1" +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + serve@^12.0.1: version "12.0.1" resolved "https://registry.yarnpkg.com/serve/-/serve-12.0.1.tgz#5b0e05849f5ed9b8aab0f30a298c3664bba052bb" @@ -11942,6 +14005,14 @@ smart-buffer@^4.2.0: resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== +snake-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" + integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -12023,6 +14094,20 @@ socks@^2.6.2: ip "^2.0.0" smart-buffer "^4.2.0" +sort-keys-length@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sort-keys-length/-/sort-keys-length-1.0.1.tgz#9cb6f4f4e9e48155a6aa0671edd336ff1479a188" + integrity sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw== + dependencies: + sort-keys "^1.0.0" + +sort-keys@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" + integrity sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg== + dependencies: + is-plain-obj "^1.0.0" + sort-keys@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128" @@ -12159,6 +14244,11 @@ split2@^3.0.0: dependencies: readable-stream "^3.0.0" +split2@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.1.0.tgz#101907a24370f85bb782f08adaabe4e281ecf809" + integrity sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ== + split@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" @@ -12216,6 +14306,18 @@ stdout-stderr@^0.1.9: debug "^4.1.1" strip-ansi "^6.0.0" +stop-iteration-iterator@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4" + integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ== + dependencies: + internal-slot "^1.0.4" + +stream-buffers@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-3.0.2.tgz#5249005a8d5c2d00b3a32e6e0a6ea209dc4f3521" + integrity sha512-DQi1h8VEBA/lURbSwFtEHnSTb9s2/pwLEaFuNhXwy1Dx3Sa0lOuYT2yNUr4/j2fs8oCAMANtrZ5OrPZtyVs3MQ== + streamroller@^3.1.3: version "3.1.4" resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.1.4.tgz#844a18e795d39c1089a8216e66a1cf1151271df0" @@ -12225,6 +14327,11 @@ streamroller@^3.1.3: debug "^4.3.4" fs-extra "^8.1.0" +strict-uri-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" + integrity sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ== + strict-uri-encode@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" @@ -12336,6 +14443,13 @@ strip-ansi@^7.0.0: dependencies: ansi-regex "^6.0.1" +strip-bom@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" + integrity sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g== + dependencies: + is-utf8 "^0.2.0" + strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" @@ -12346,6 +14460,13 @@ strip-bom@^4.0.0: resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== +strip-dirs@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/strip-dirs/-/strip-dirs-2.1.0.tgz#4987736264fc344cf20f6c34aca9d13d1d4ed6c5" + integrity sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g== + dependencies: + is-natural-number "^4.0.1" + strip-eof@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" @@ -12363,7 +14484,7 @@ strip-indent@^3.0.0: dependencies: min-indent "^1.0.0" -strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: +strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== @@ -12373,6 +14494,13 @@ strip-json-comments@~2.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= +strip-outer@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-outer/-/strip-outer-1.0.1.tgz#b2fd2abf6604b9d1e6013057195df836b8a9d631" + integrity sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg== + dependencies: + escape-string-regexp "^1.0.2" + strong-log-transformer@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz#0f5ed78d325e0421ac6f90f7f10e691d6ae3ae10" @@ -12395,6 +14523,18 @@ stylehacks@^5.0.1: browserslist "^4.16.0" postcss-selector-parser "^6.0.4" +suffix@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/suffix/-/suffix-0.1.1.tgz#cc58231646a0ef1102f79478ef3a9248fd9c842f" + integrity sha512-j5uf6MJtMCfC4vBe5LFktSe4bGyNTBk7I2Kdri0jeLrcv5B9pWfxVa5JQpoxgtR8vaVB7bVxsWgnfQbX5wkhAA== + +supports-color@8.1.1, supports-color@^8.0.0, supports-color@^8.1.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" @@ -12414,13 +14554,6 @@ supports-color@^7.0.0, supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -supports-color@^8.0.0, supports-color@^8.1.0: - version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - supports-hyperlinks@^2.0.0, supports-hyperlinks@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz#4f77b42488765891774b70c79babd87f9bd594bb" @@ -12464,7 +14597,7 @@ tapable@^2.1.1, tapable@^2.2.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== -tar-fs@^2.0.0: +tar-fs@2.1.1, tar-fs@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== @@ -12474,7 +14607,20 @@ tar-fs@^2.0.0: pump "^3.0.0" tar-stream "^2.1.4" -tar-stream@^2.1.4, tar-stream@~2.2.0: +tar-stream@^1.5.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555" + integrity sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A== + dependencies: + bl "^1.0.0" + buffer-alloc "^1.2.0" + end-of-stream "^1.0.0" + fs-constants "^1.0.0" + readable-stream "^2.3.0" + to-buffer "^1.1.1" + xtend "^4.0.0" + +tar-stream@^2.1.4, tar-stream@^2.2.0, tar-stream@~2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== @@ -12625,6 +14771,11 @@ timed-out@^3.0.0: resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-3.1.3.tgz#95860bfcc5c76c277f8f8326fd0f5b2e20eba217" integrity sha1-lYYL/MXHbCd/j4Mm/Q9bLiDrohc= +timed-out@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" + integrity sha512-G7r3AhovYtr5YKOWQkta8RKAPb+J9IsO4uVmzjl8AZwfhs8UcUwTiD6gcJYSgOtzyjvQKrKYn41syHbUWMkafA== + timers-browserify@^2.0.12: version "2.0.12" resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.12.tgz#44a45c11fbf407f34f97bccd1577c652361b00ee" @@ -12681,6 +14832,11 @@ tmpl@1.0.x: resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== +to-buffer@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80" + integrity sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg== + to-capital-case@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/to-capital-case/-/to-capital-case-1.0.0.tgz#a57c5014fd5a37217cf05099ff8a421bbf9c9b7f" @@ -12822,6 +14978,13 @@ trim-off-newlines@^1.0.0: resolved "https://registry.yarnpkg.com/trim-off-newlines/-/trim-off-newlines-1.0.2.tgz#65d52e4f4115992c0dac87220bdcc279fe58c422" integrity sha512-DAnbtY4lNoOTLw05HLuvPoBFAGV4zOKQ9d1Q45JB+bcDwYIEkCr0xNgwKtygtKFBbRlFA/8ytkAM1V09QGWksg== +trim-repeated@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trim-repeated/-/trim-repeated-1.0.0.tgz#e3646a2ea4e891312bf7eace6cfb05380bc01c21" + integrity sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg== + dependencies: + escape-string-regexp "^1.0.2" + ts-custom-error@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/ts-custom-error/-/ts-custom-error-3.2.0.tgz#ff8f80a3812bab9dc448536312da52dce1b720fb" @@ -12902,6 +15065,11 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +tunnel@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" + integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -13006,11 +15174,24 @@ ua-parser-js@^0.7.30: resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.32.tgz#cd8c639cdca949e30fa68c44b7813ef13e36d211" integrity sha512-f9BESNVhzlhEFf2CHMSj40NWOjYPl1YKYbrvIr/hFTDEmLq7SRbWvm7FcdcpCYT95zrOhC7gZSxjdnnTpBcwVw== +ua-parser-js@^1.0.1: + version "1.0.33" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.33.tgz#f21f01233e90e7ed0f059ceab46eb190ff17f8f4" + integrity sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ== + uglify-js@^3.1.4: version "3.14.1" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.14.1.tgz#e2cb9fe34db9cb4cf7e35d1d26dfea28e09a7d06" integrity sha512-JhS3hmcVaXlp/xSo3PKY5R0JqKs5M3IV+exdLHW99qKvKivPO4Z8qbej6mte17SOPqAOVMjt/XGgWacnFSzM3g== +unbzip2-stream@1.4.3, unbzip2-stream@^1.0.9: + version "1.4.3" + resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" + integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== + dependencies: + buffer "^5.2.1" + through "^2.3.8" + unfetch@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-3.1.2.tgz#dc271ef77a2800768f7b459673c5604b5101ef77" @@ -13131,6 +15312,20 @@ update-notifier@^0.6.0: latest-version "^2.0.0" semver-diff "^2.0.0" +upper-case-first@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/upper-case-first/-/upper-case-first-2.0.2.tgz#992c3273f882abd19d1e02894cc147117f844324" + integrity sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg== + dependencies: + tslib "^2.0.3" + +upper-case@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-2.0.2.tgz#d89810823faab1df1549b7d97a76f8662bae6f7a" + integrity sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg== + dependencies: + tslib "^2.0.3" + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" @@ -13157,6 +15352,11 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" +url-to-options@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9" + integrity sha512-0kQLIzG4fdk/G5NONku64rSH/x32NOA39LVQqlK8Le6lvTF6GGRJpqaQFGgU+CLwySIqBSMdwYM0sYcW9f6P4A== + url@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" @@ -13341,6 +15541,54 @@ webcrypto-core@^1.4.0: pvtsutils "^1.2.0" tslib "^2.3.1" +webdriver@7.30.0: + version "7.30.0" + resolved "https://registry.yarnpkg.com/webdriver/-/webdriver-7.30.0.tgz#8263e74df6927e1a6df57ef583335a7aafb1459e" + integrity sha512-bQE4oVgjjg5sb3VkCD+Eb8mscEvf3TioP0mnEZK0f5OJUNI045gMCJgpX8X4J8ScGyEhzlhn1KvlAn3yzxjxog== + dependencies: + "@types/node" "^18.0.0" + "@wdio/config" "7.30.0" + "@wdio/logger" "7.26.0" + "@wdio/protocols" "7.27.0" + "@wdio/types" "7.26.0" + "@wdio/utils" "7.26.0" + got "^11.0.2" + ky "0.30.0" + lodash.merge "^4.6.1" + +webdriverio@7.30.0: + version "7.30.0" + resolved "https://registry.yarnpkg.com/webdriverio/-/webdriverio-7.30.0.tgz#b693b9af2710c6751980e2955d5dee8b92b4dcdd" + integrity sha512-GRz7XKMFKDKyTP5UxPeF3KciyZyzF44eZZtGhY6tk1PQqVtnyENwJkDVIFxcYttOsnLLj4BQuLfvf1WQF/xZbw== + dependencies: + "@types/aria-query" "^5.0.0" + "@types/node" "^18.0.0" + "@wdio/config" "7.30.0" + "@wdio/logger" "7.26.0" + "@wdio/protocols" "7.27.0" + "@wdio/repl" "7.26.0" + "@wdio/types" "7.26.0" + "@wdio/utils" "7.26.0" + archiver "^5.0.0" + aria-query "^5.0.0" + css-shorthand-properties "^1.1.1" + css-value "^0.0.1" + devtools "7.30.0" + devtools-protocol "^0.0.1092731" + fs-extra "^10.0.0" + grapheme-splitter "^1.0.2" + lodash.clonedeep "^4.5.0" + lodash.isobject "^3.0.2" + lodash.isplainobject "^4.0.6" + lodash.zip "^4.2.0" + minimatch "^6.0.4" + puppeteer-core "^13.1.3" + query-selector-shadow-dom "^1.0.0" + resq "^1.9.1" + rgb2hex "0.2.5" + serialize-error "^8.0.0" + webdriver "7.30.0" + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" @@ -13549,6 +15797,39 @@ whatwg-url@^8.0.0, whatwg-url@^8.5.0: tr46 "^2.1.0" webidl-conversions "^6.1.0" +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + +which-collection@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906" + integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A== + dependencies: + is-map "^2.0.1" + is-set "^2.0.1" + is-weakmap "^2.0.1" + is-weakset "^2.0.1" + +which-typed-array@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6" + integrity sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.0" + is-typed-array "^1.1.10" + which@^1.2.1, which@^1.2.9: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" @@ -13606,6 +15887,11 @@ wordwrap@>=0.0.2, wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= +workerpool@6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" + integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== + wrap-ansi@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-4.0.0.tgz#b3570d7c70156159a2d42be5cc942e957f7b1131" @@ -13707,6 +15993,11 @@ write-pkg@^4.0.0: type-fest "^0.4.1" write-json-file "^3.2.0" +ws@8.5.0, ws@^8.5.0: + version "8.5.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f" + integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg== + ws@^7.3.1, ws@^7.4.6: version "7.5.5" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881" @@ -13717,11 +16008,6 @@ ws@^8.1.0, ws@^8.2.3, ws@~8.2.3: resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba" integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA== -ws@^8.5.0: - version "8.5.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f" - integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg== - xdg-basedir@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-2.0.0.tgz#edbc903cc385fc04523d966a335504b5504d1bd2" @@ -13751,7 +16037,7 @@ xregexp@^4.2.4: dependencies: "@babel/runtime-corejs3" "^7.12.1" -xtend@~4.0.1: +xtend@^4.0.0, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== @@ -13799,7 +16085,17 @@ yargs-parser@^18.1.3: camelcase "^5.0.0" decamelize "^1.2.0" -yargs@^16.1.1, yargs@^16.2.0: +yargs-unparser@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" + integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== + dependencies: + camelcase "^6.0.0" + decamelize "^4.0.0" + flat "^5.0.2" + is-plain-obj "^2.1.0" + +yargs@16.2.0, yargs@^16.1.1, yargs@^16.2.0: version "16.2.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== @@ -13812,7 +16108,7 @@ yargs@^16.1.1, yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" -yargs@^17.6.2: +yargs@^17.0.0, yargs@^17.2.1, yargs@^17.6.2: version "17.6.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.6.2.tgz#2e23f2944e976339a1ee00f18c77fedee8332541" integrity sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw== @@ -13825,6 +16121,23 @@ yargs@^17.6.2: y18n "^5.0.5" yargs-parser "^21.1.1" +yarn-install@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/yarn-install/-/yarn-install-1.0.0.tgz#57f45050b82efd57182b3973c54aa05cb5d25230" + integrity sha512-VO1u181msinhPcGvQTVMnHVOae8zjX/NSksR17e6eXHRveDvHCF5mGjh9hkN8mzyfnCqcBe42LdTs7bScuTaeg== + dependencies: + cac "^3.0.3" + chalk "^1.1.3" + cross-spawn "^4.0.2" + +yauzl@^2.10.0, yauzl@^2.4.2: + version "2.10.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0" + yn@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" @@ -13834,3 +16147,12 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zip-stream@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-4.1.0.tgz#51dd326571544e36aa3f756430b313576dc8fc79" + integrity sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A== + dependencies: + archiver-utils "^2.1.0" + compress-commons "^4.1.0" + readable-stream "^3.6.0" From 84a220a06127b7a5fac46099a9d5e87d273942b8 Mon Sep 17 00:00:00 2001 From: Noah Cooper Date: Thu, 16 Feb 2023 20:27:07 -0500 Subject: [PATCH 02/10] main (#7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Publish - @segment/actions-cli-internal@3.123.2 - @segment/actions-cli@3.123.2 - @segment/action-destinations@3.127.2 * Subscribe to more events (Redo without required type) (#986) * Add subscriptions to track event * Generate types and new snapshot * Add test * Improve labels and descriptions * Generated types * Change description * Generate types * Remove required * Generate types * Update Ripe web destination (#968) * Update Ripe web destination - Add new endpoint setting for testing purposes - Remove alias call - Update misleading anonymousId descriptions * Update erroneous default paths * Set anonymousId in identify call * Heap 34916 - add session_id + update segment library for tracking purposes (#787) * Fix events payload * Use the single event not the bulk * Fix tests * Fix should not override * remove console log and update SEGMENT_LIB var * update constant value * update browser tests as well * Adding Group support for customerio -Rename identifier field names (#973) * Initial commit for objects * Added Test cases * Adding Tests validation for the payload * committing generate type file * Adding group support from identify * Fixing conflicts * Adding traits to attributes property for createUpdateObject action * renaming id and type_id to object_id and object_type_id Co-authored-by: kishoredevarasettyn * SalesWings (Actions) Destination (#945) * Generated integration from scaffold * Fix action name * Implement SalesWings destination actions * Send user agent, rearrange fields * Bugfixes * Remove debug logging * First tests * Auth tests & track event tests * Page event tests * Identify event tests * Screen event tests * Event batch test * More event batch tests * Change API key description * Commit generated types * Minor cleanup * Fix square brackets in field description UI * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Hardcoding timestamps for snapshot tests * Extract email from properties of Track event * Add action description * Add default subscription to action * Add destination present * Merge URL fields * Dedicated actions per event type * Cleanup * Update field descriptions * Update geenrated types Co-authored-by: Yevgeny Terov <73266004+yevsw@users.noreply.github.com> Co-authored-by: Marín Alcaraz Co-authored-by: Yevgeny Terov * Changing default subscription to group for group call (#995) * Initial commit for objects * Added Test cases * Adding Tests validation for the payload * committing generate type file * Adding group support from identify * Fixing conflicts * Adding traits to attributes property for createUpdateObject action * setting default subscription as group for createUpdateObject and addressing other review comments * correcting merge overrides Co-authored-by: kishoredevarasettyn * Add anonymous id as a user property (#981) * Update setting description in Google Ads Conversions (#983) * HGI-237 | Updated Description for Braze Cohorts Fields (#992) * udpated description for braze cohorts * updated description for braze cohorts * updated description in mapping fields * updated description * updated event_properties to hidden * made event_properties unhidden Co-authored-by: Gaurav Kochar * Increase CI timeout to 15 minutes and 10 minutes respectively (#985) * Increase CI timeout to 15 minutes * Bump browser tests to 10 minutes Co-authored-by: Nolan Chan * Pipedrive actions PE-20 (#996) * fix for pipedrive pe-20 issue * removing default visible_to * Register Saleswing Action (#999) Co-authored-by: Nolan Chan * Publish - @segment/browser-destinations@3.72.0 - @segment/actions-cli-internal@3.124.0 - @segment/actions-cli@3.124.0 - @segment/action-destinations@3.128.0 * ACT-362 Brackets Support (#993) * add support for brackets inside js keys in get method * add double quotes * explanatory text + new link * safari support * remove invalid bracket test since it is now supported * use class based regex to avoid parseError * actually convert the regex correctly * cleanup * split tests by functionality * Refactor .get test (#1000) * Heap Fix for empty event name (#1004) * fix for pe-52 * fixing breaking tests * Publish - @segment/actions-shared@1.32.0 - @segment/browser-destinations@3.73.0 - @segment/actions-cli-internal@3.125.0 - @segment/actions-cli@3.125.0 - @segment/actions-core@3.50.0 - @segment/action-destinations@3.129.0 * fix scaffolding for oauth (#1008) * [CHANNELS-329] Add WhatsApp support for Twilio Engage (#987) * feat: added whatsapp support * fix: added missing dependencies * refactor: minor cleanup * fix: moved dependency to package level * fix: uri encoding for get traits * fix: using same auth scheme for sms & whatsapp whatsApp thankfully allows using apiKeySid & apiSecret instead of accountSid & authToken * fix: reverted changed package version * feat: allow bypassing contentVariables reconciliation * Publish - @segment/actions-cli-internal@3.126.0 - @segment/actions-cli@3.126.0 - @segment/action-destinations@3.130.0 * Add browser destination tests with saucelabs (#994) * Node 18 Upgrade (#991) * packages * ci + nvm * lock for 18 * fix webpack hashing issue * node types to 18 * Update node version for browser-tests and snyk * Fix tests * Try to fix browser tests * Fix pipedrive unit tests * Fix domain in snapshot tests * Fix yarn subscriptions * Update README --------- Co-authored-by: Dan Lasky * Use node18 for browser tests destinations (#1014) * Contribution pe 53 (#1007) * updating contributing guidelines * adding extra instructions for post deployment changes * spelling corrections * spelling corrections * Update CONTRIBUTING.md Co-authored-by: SyedWasiHaider * Update CONTRIBUTING.md Co-authored-by: SyedWasiHaider --------- Co-authored-by: SyedWasiHaider * Qualtrics upsert transaction (#963) * adding upsert contact transacion destination * fixing snapshots * Updating perform function for upsertTransaction * Adding dynamic fields for directoryId. Updating field descriptions. Update upsertTransaction defaultSubscription * updating types * Update qualtrics destination name and descriptons on actions --------- Co-authored-by: Carl Lee * fixing a couple of issues with new Ironclad destination (#1002) * fixing a couple of issues with new Ironclad destination * adding updated generated types * fixing broken test * [salesforce] - Verify the `instanceUrl` is a valid Salesforce domain (#997) * Regex and WIP unit tests * Unit tests working * Updates regex and unit tests * Updates other unit tests * Saving package.json * Adds a couple more unit tests * Removes package.json from commits * Removes package.json from commits * Imports request client using absolute path instead of relative path * Enforce https * Publish - @segment/actions-shared@1.33.0 - @segment/browser-destinations-integration-tests@0.1.0 - @segment/browser-destinations@3.74.0 - @segment/actions-cli-internal@3.127.0 - @segment/actions-cli@3.127.0 - @segment/actions-core@3.51.0 - @segment/action-destinations@3.131.0 - @segment/destination-subscriptions@3.15.0 * Fix CommandBar browser destination initialization when CommandBar has already been loaded through other means (#1009) Co-authored-by: Thomas Kainrad * remove flow that attempts to create a JIRA ticket (#1021) * Twilio Studio as a Segment Action Destination (#1023) * Twilio Studio as a Segment Action Destination * Replaced phone number with userid in the cache key * Addressed review comments * DOTORG-839: Blackbaud Raiser's Edge NXT Destination (#998) * DOTORG-839: Create or Update Individual Constituent Action (#1) * DOTORG-839 Added OAuth2 settings for Blackbaud (#2) * Move bbApiSubscriptionKey to settings * Only aggregate integrationErrors * Update Online Presence label * Update directory structure * Add types * Abstract API calls * Add dateStringToFuzzyDate * Add types * Don't retry 401s * Don't catch errors on constituent search or creation * Concatenate integrationErrors * Add throwHttpErrors * Set default for lookup_id to userId * Pass constituentId to updateConstituent * Remove try/catch * Use camelCase traits * Add filterObjectListByMatchFields * Check if primary property is defined * DOTORG-839 Added authentication test (#3) * Don't match on country * Use datetime type * Strip non-numeric characters from phone when matching * Don't match on undefined boolean fields * Update generated-types.ts * Fix linting errors * Move fixtures out of tests directory * Update constituentData * Update default lookup_id mapping * Update testAuthentication * Remove UNEXPECTED_RECORD_COUNT error * Update tests --------- Co-authored-by: twilio-hwong <91703194+twilio-hwong@users.noreply.github.com> * Google Analytics 4 Web Destination (#1012) * addPaymentInfo Action * ga4 types, properties, and functions * set config fields action poc * set config fields action poc * GA4 all action created * Remove unit test cases files * Added defaultSubscription tag in viewItemList and generateLead Action * added register code for GA4 in broweser-destinations * add customEvent action * set config action & custom event action * Delete snapshot.test.ts * Delete index.test.ts * clean up & add mappings for preset * updated types and removed picklist * Added test cases for GA4 actions * Added test cases * added custom event unit test + cleaned up merge issues and commented code * fixed typo * add back ripe and commandbar to index file * added comment on non using variable * Apply suggestions from code review Co-authored-by: Neek Sandhu * revert set configuration field actions * added updateUser function, and updated event to payload * reverted back gtag function and datalayer setup * add gtag type and remove comment * update yarn.lock file * add viewItemList & disable linter for args * remove gtag.js type dependency * update unit tests * added events to defaultSubscriptions * update index.js --------- Co-authored-by: Varadarajan V Co-authored-by: Ankit Gupta Co-authored-by: Neek Sandhu * livelike-cloud action destination (PE-41) (#1020) * created livelike-cloud action destination with one trackEvent action and three presets and added unit tests * Update packages/destination-actions/src/destinations/livelike-cloud/trackEvent/index.ts Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> --------- Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> * Qualtrics - Fixed timezone issue in unit test (#1026) * Fixed timezone issue in unit test * refactored to use dayjs from lib * generate types (#1028) * PE-47 - Merge Algolia Insights (#1027) * Algolia insights integration (#975) * feat: initial commit after scaffold * feat: clarify insights API and impliment as distinct actions * test: write test for conversion destination * test: write test for click and view destination * test: write auth schema test * create productClick presets * add presets to destination * create all event presets * remove default imports for actions * add testAuthentication method for algolia-insights --------- Co-authored-by: Beatrice Parfait * fixing snapshots and replacing userId and anonId with userToken * fixing timestamps in tests --------- Co-authored-by: Wesley Walser Co-authored-by: Beatrice Parfait * VWO Cloud Mode Description changes (#1015) * Description changes and changes in utility * Tests altered for new format * Update CODEOWNERS (#1029) * [HEAP-38485] Move to the integrations endpoint (#1016) * [HEAP-32036] First trackEvent implementation (#8) * [HEAP-32037] Send identify and add user properties requests (#9) * [HEAP-32037] Add user properties and migrate anonymous users * Hash anonymous user ID * Use message ID as idempotency key * Throw if parameters are missing * [HEAP-32884] backport filtering event properties (#10) * [HEAP-32884] backport filtering event properties - flat properties from the request payload before sending them to heap * address comments: - remove the embeded object under the util.ts, move it to flattenObj - remove unnecessary tests on identifyUser - import and use the embeded object and flatten object in trackEvent and identifyUser test not part of comments: - rename the file from flattenObj to flat * PR comments * Update packages/destination-actions/src/destinations/heap/trackEvent/index.ts Co-authored-by: Marín Alcaraz * Update packages/destination-actions/src/destinations/heap/trackEvent/index.ts Co-authored-by: Marín Alcaraz * Update packages/destination-actions/src/destinations/heap/trackEvent/index.ts Co-authored-by: Marín Alcaraz * Update packages/destination-actions/src/destinations/heap/trackEvent/index.ts Co-authored-by: Marín Alcaraz * remove unused file * save changes * save * fix library * remove accidental file * update tests * remove session id * PR comments * fix the tests * add user properties if more that one identifier is present * Revert "add user properties if more that one identifier is present" This reverts commit bb4fb94684be54c933e9375ae1fc877e876fbbd1. --------- Co-authored-by: Gediminas Rapolavicius Co-authored-by: Yiyang Li <93153941+yiyangli-heap@users.noreply.github.com> Co-authored-by: Marín Alcaraz Co-authored-by: murphpdx * Launchpad Segment Integration (#1010) * Launchpad Segment Integration We are now adding the ability to send events from segment into Launchpad.pm - the mission control for Product teams at scale. We are adding the following: * trackEvent * groupIdentifyUser * identifyUser We have added unit tests and have tested this manually. It requires the following: * apiRegion - EU by default. We have added the US as means of future extensibility. * apiSecret - given by Launchpad.pm while onboarding. * sourceName - to be added. * updating snapshots * Changes implemented following call with Joe Track() [x] Explicitly indicate which fields are required or optional [x] Look for traits and if not context.traits [x] Need description for the Action [x] Make Timestamp field required [x] Make messageId required. It will always be there. [x] getEventProperties: This line looks incorrect: id: payload.event, [x] source mapping looks incorrect: source: integration?.name == 'Iterable' ? 'Iterable' : 'segment', Identify() [x] Description should be updated. [x] identify() calls are often fired when a trait is collected, or when a userId is collected. [x] Maybe use wording: “Creates or updates a user profile, and adds or updates trait values on the user profile…” [x] Refactor the perform() function group() [x] Group Key - Amend description to better explain that group key is a way to connect multiple organizations together [x] groupId field - Should be updated [x] Consider adding anonymousId as a field [x] Handle when there are no traits. * changes * tests passing, removed the check on user_id, anonymous_id since none are required * fixing the types as well * fixing test --------- Co-authored-by: joe-ayoub-segment <45374896+joe-ayoub-segment@users.noreply.github.com> * [STRATCONN-1950] Init new destination `Pinterest Conversions API` (#1030) * Init new destination `Pinterest Conversions API` * Update index.ts * Update index.ts * Update index.ts --------- Co-authored-by: rvadera12 <89420099+rvadera12@users.noreply.github.com> * Try to only pass in NODE_OPTIONS for node18+ (#1032) * Try using conditional for scripts * Remove NODE_OPTIONS from else * Echo the nodeversion * Try setting the node major version and testing that * Use correct variable in condition * Try a different approach * Use bash instead * Try to fix script * Add other remaining scripts * Changing name of new Algolia Insights Integration (#1038) * Register new action destinations (#1036) * Register new action destinations: Launchpad, Livelike Cloud, Twilio Studio, BlackBaud Raisers Edge Nxt, Pinterest Conversions Api * Fix path * add action to pinterest conversions api * add action to pinterest conversions api * add description to pinterest action * Register algolia insights (Actions) --------- Co-authored-by: rvadera12 * Publish - @segment/actions-shared@1.34.0 - @segment/browser-destinations@3.75.0 - @segment/actions-cli-internal@3.128.0 - @segment/actions-cli@3.128.0 - @segment/actions-core@3.52.0 - @segment/action-destinations@3.132.0 - @segment/destination-subscriptions@3.16.0 * Use correct defaultPath for messageId (#1041) * Google ads v11 to v12 (#1018) * gooogle conversion v12 upgrade * changes * bug fix for test cases * gooogle conversion v12 upgrade * changes * bug fix for test cases * flag name changes * flag name change in test cases * review changes * revert ga4-types file change * revert ga4-types file change * revert ga4-types file change --------- Co-authored-by: manoj kumar * [STRATCONN-1779]Add datadog stats for google ads api version (#1046) * adds stats for api version * adds missing statscontext parameter to getCustomVariables * refactor params * Voucherify-Segment.io Integration using action-destinations (#970) * initial destination configuration * identify customer action * add trackEvent * Generate screenEvent and pageEvent * Change the structure of track, page and screen events. * Delete test folder for now * Change the type definition * Delete timestamp * create group event * removed created_at property * hit to localhost address * Add unit tests * Some fixes * Change the URLs in perform method * Update URLs in tests * Delete unused testing authentication fn * Add snapshots * Add ability to pass a custom URL * Set type to required in page and screen events * Delete snapshots * Replace api endpoint (with regions) with custom URL * if there's no userId then use the anonymousId * removed space * added type property to rest of events * changed name of property 'name' to 'event' * Update index.ts * Update generated-types.ts * Delete unnecessary test - 'should throw an error when the name is not provided using page event' * Delete the 'voucherify' prefix * Slightly change the descriptions * generated types * Separate URL functions into separate files Change the file names to be more descriptive. * Reduce the getVoucherifyEndpointURL function * delete * Update the descriptions - Also deleted the 'event' prop from screen/page events and now the 'name' in screen/page event is no longer required. * Commit the generated types * Reduce the number of events to three. Track Custom Event, Identify Customer, Add group to customer metadata * Add customer attributes to traits in upsertCustomer action * Update generated-types.ts * Add testAuthentication * Update names of actions in unit tests * add firstName and lastName * Add email to custom event processing * Delete email from upsertCustomer (leave it only in traits) * Add email to description * Add email to customer processing * Update testAuthentication * update addCustomEvent * Update packages/destination-actions/src/destinations/voucherify/upsertCustomer/index.ts Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> * Fix issue with mapping the user attributes and improve Presets * generate types * change email properties * generated types * custom url information * generated types * Update the desc of Custom URL * minor changes from last PR * Update index.ts --------- Co-authored-by: Patryk Smolarz Co-authored-by: Patryk Smolarz <77458595+patricioo1@users.noreply.github.com> Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> * HGI 372 - Fix Segment Profiles Destination (#1047) * Added `All: false` in Tracking API event * Changed PAPI Token field from `string` to `password` * Fixed minor typos * HGI-368 | Fixed the missing x-signature in request header when batching is enabled (#1043) * worked on HGI-368 Fix * added a unit test case for same fix --------- Co-authored-by: Gaurav Kochar * Mixpanel destination: use user-agent for browser data (#1035) * remove device manufacturer - use user-agent * userAgent is only sent on web * New SalesWings API urls (#1039) * Use new SalesWings API urls * Do not check response status in testAuthentication * Fix testAuthentication * Remove unused file --------- Co-authored-by: Nick Aguilar Co-authored-by: Stella Chung Co-authored-by: Simon Co-authored-by: A Murphy Co-authored-by: kishoredevarasettyn <97026912+kishoredevarasettyn@users.noreply.github.com> Co-authored-by: kishoredevarasettyn Co-authored-by: Denis Egorushkin <98813888+denis-egorushkin-sw@users.noreply.github.com> Co-authored-by: Yevgeny Terov <73266004+yevsw@users.noreply.github.com> Co-authored-by: Marín Alcaraz Co-authored-by: Yevgeny Terov Co-authored-by: maryamsharif <99763167+maryamsharif@users.noreply.github.com> Co-authored-by: Innovative-GauravKochar <117165746+Innovative-GauravKochar@users.noreply.github.com> Co-authored-by: Gaurav Kochar Co-authored-by: Nolan Chan Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> Co-authored-by: Nolan Chan Co-authored-by: Dan Co-authored-by: Seth Silesky <5115498+silesky@users.noreply.github.com> Co-authored-by: rhall-twilio <103517471+rhall-twilio@users.noreply.github.com> Co-authored-by: alfrimpong <119889384+alfrimpong@users.noreply.github.com> Co-authored-by: SyedWasiHaider Co-authored-by: Dan Lasky Co-authored-by: drakauskas <119876674+drakauskas@users.noreply.github.com> Co-authored-by: Carl Lee Co-authored-by: Wasi Haider Co-authored-by: Thomas Kainrad <7394822+tkainrad@users.noreply.github.com> Co-authored-by: Thomas Kainrad Co-authored-by: aradhakrishnan-twilio <116877054+aradhakrishnan-twilio@users.noreply.github.com> Co-authored-by: twilio-hwong <91703194+twilio-hwong@users.noreply.github.com> Co-authored-by: rvadera12 <89420099+rvadera12@users.noreply.github.com> Co-authored-by: Varadarajan V Co-authored-by: Ankit Gupta Co-authored-by: Neek Sandhu Co-authored-by: Abhishek Kansal Co-authored-by: Sayan Das <109198085+sayan-das-in@users.noreply.github.com> Co-authored-by: Wesley Walser Co-authored-by: Beatrice Parfait Co-authored-by: Prathamesh Tamanekar Co-authored-by: Gediminas Rapolavicius Co-authored-by: Yiyang Li <93153941+yiyangli-heap@users.noreply.github.com> Co-authored-by: murphpdx Co-authored-by: Stefan Sabev Co-authored-by: Logan Luque <98849774+LLuque-twilio@users.noreply.github.com> Co-authored-by: rvadera12 Co-authored-by: Logan Luque Co-authored-by: immanojkumar <117071418+immanojkumar@users.noreply.github.com> Co-authored-by: manoj kumar Co-authored-by: Varadarajan V <109586712+varadarajan-tw@users.noreply.github.com> Co-authored-by: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Co-authored-by: Patryk Smolarz Co-authored-by: Patryk Smolarz <77458595+patricioo1@users.noreply.github.com> Co-authored-by: Sonya Park <68977514+spjtls9@users.noreply.github.com> --- .github/CODEOWNERS | 2 +- package.json | 2 +- packages/actions-shared/package.json | 4 +- packages/browser-destinations/package.json | 12 +- .../scripts/build-web-stage.sh | 11 + .../browser-destinations/scripts/build-web.sh | 8 + .../__tests__/addPaymentInfo.test.ts | 101 +++++ .../__tests__/addToCart.test.ts | 95 +++++ .../__tests__/addToWishlist.test.ts | 95 +++++ .../__tests__/beginCheckout.test.ts | 101 +++++ .../__tests__/customEvent.test.ts | 70 ++++ .../__tests__/generateLead.test.ts | 68 ++++ .../__tests__/login.test.ts | 63 +++ .../__tests__/purchase.test.ts | 103 +++++ .../__tests__/refund.test.ts | 103 +++++ .../__tests__/removeFromCart.test.ts | 99 +++++ .../__tests__/search.test.ts | 64 +++ .../__tests__/selectItem.test.ts | 96 +++++ .../__tests__/selectPromotion.test.ts | 111 ++++++ .../__tests__/signUp.test.ts | 62 +++ .../__tests__/viewCart.test.ts | 96 +++++ .../__tests__/viewItem.test.ts | 96 +++++ .../__tests__/viewItemList.test.ts | 96 +++++ .../__tests__/viewPromotion.test.ts | 111 ++++++ .../addPaymentInfo/generated-types.ts | 117 ++++++ .../addPaymentInfo/index.ts | 48 +++ .../addToCart/generated-types.ts | 109 ++++++ .../google-analytics-4-web/addToCart/index.ts | 36 ++ .../addToWishlist/generated-types.ts | 109 ++++++ .../addToWishlist/index.ts | 38 ++ .../beginCheckout/generated-types.ts | 113 ++++++ .../beginCheckout/index.ts | 38 ++ .../customEvent/generated-types.ts | 28 ++ .../customEvent/index.ts | 52 +++ .../google-analytics-4-web/ga4-functions.ts | 8 + .../google-analytics-4-web/ga4-properties.ts | 368 ++++++++++++++++++ .../google-analytics-4-web/ga4-types.ts | 28 ++ .../generateLead/generated-types.ts | 28 ++ .../generateLead/index.ts | 32 ++ .../google-analytics-4-web/generated-types.ts | 60 +++ .../google-analytics-4-web/index.ts | 200 ++++++++++ .../login/generated-types.ts | 24 ++ .../google-analytics-4-web/login/index.ts | 31 ++ .../purchase/generated-types.ts | 125 ++++++ .../google-analytics-4-web/purchase/index.ts | 54 +++ .../refund/generated-types.ts | 129 ++++++ .../google-analytics-4-web/refund/index.ts | 57 +++ .../removeFromCart/generated-types.ts | 109 ++++++ .../removeFromCart/index.ts | 37 ++ .../search/generated-types.ts | 24 ++ .../google-analytics-4-web/search/index.ts | 30 ++ .../selectItem/generated-types.ts | 109 ++++++ .../selectItem/index.ts | 43 ++ .../selectPromotion/generated-types.ts | 137 +++++++ .../selectPromotion/index.ts | 67 ++++ .../setConfigurationFields/generated-types.ts | 70 ++++ .../setConfigurationFields/index.ts | 142 +++++++ .../signUp/generated-types.ts | 24 ++ .../google-analytics-4-web/signUp/index.ts | 29 ++ .../google-analytics-4-web/types.ts | 3 + .../viewCart/generated-types.ts | 109 ++++++ .../google-analytics-4-web/viewCart/index.ts | 36 ++ .../viewItem/generated-types.ts | 109 ++++++ .../google-analytics-4-web/viewItem/index.ts | 37 ++ .../viewItemList/generated-types.ts | 109 ++++++ .../viewItemList/index.ts | 36 ++ .../viewPromotion/generated-types.ts | 137 +++++++ .../viewPromotion/index.ts | 67 ++++ .../src/destinations/index.ts | 3 +- packages/cli-internal/package.json | 8 +- packages/cli/package.json | 10 +- packages/core/package.json | 4 +- packages/destination-actions/package.json | 6 +- .../__snapshots__/snapshot.test.ts.snap | 128 ++++++ .../algolia-insights/__tests__/index.test.ts | 58 +++ .../__tests__/snapshot.test.ts | 77 ++++ .../algolia-insights/algolia-insight-api.ts | 29 ++ .../__snapshots__/snapshot.test.ts.snap | 47 +++ .../conversionEvents/__tests__/index.test.ts | 84 ++++ .../__tests__/snapshot.test.ts | 79 ++++ .../conversionEvents/generated-types.ts | 26 ++ .../conversionEvents/index.ts | 87 +++++ .../algolia-insights/generated-types.ts | 12 + .../destinations/algolia-insights/index.ts | 63 +++ .../__snapshots__/snapshot.test.ts.snap | 47 +++ .../__tests__/index.test.ts | 68 ++++ .../__tests__/snapshot.test.ts | 79 ++++ .../productClickedEvents/generated-types.ts | 28 ++ .../productClickedEvents/index.ts | 94 +++++ .../__snapshots__/snapshot.test.ts.snap | 39 ++ .../__tests__/index.test.ts | 65 ++++ .../__tests__/snapshot.test.ts | 79 ++++ .../productViewedEvents/generated-types.ts | 24 ++ .../productViewedEvents/index.ts | 84 ++++ .../__tests__/uploadCallConversion.test.ts | 117 ++++++ .../__tests__/uploadClickConversion.test.ts | 177 +++++++++ .../uploadConversionAdjustment.test.ts | 171 ++++++++ .../google-enhanced-conversions/functions.ts | 20 +- .../google-enhanced-conversions/types.ts | 2 - .../uploadCallConversion/index.ts | 19 +- .../uploadClickConversion/index.ts | 19 +- .../uploadConversionAdjustment/index.ts | 10 +- .../__snapshots__/snapshot.test.ts.snap | 47 ++- .../destinations/heap/__tests__/flat.test.ts | 64 +-- .../src/destinations/heap/flat.ts | 17 +- .../src/destinations/heap/heapUtils.ts | 46 +++ .../__snapshots__/snapshot.test.ts.snap | 47 ++- .../heap/trackEvent/__tests__/index.test.ts | 82 ++-- .../heap/trackEvent/generated-types.ts | 6 +- .../src/destinations/heap/trackEvent/index.ts | 67 ++-- .../src/destinations/index.ts | 6 + .../destinations/launchpad/generated-types.ts | 16 + .../__snapshots__/snapshot.test.ts.snap | 27 ++ .../groupIdentifyUser/__tests__/index.test.ts | 55 +++ .../__tests__/snapshot.test.ts | 76 ++++ .../groupIdentifyUser/generated-types.ts | 26 ++ .../launchpad/groupIdentifyUser/index.ts | 89 +++++ .../__snapshots__/snapshot.test.ts.snap | 29 ++ .../identifyUser/__tests__/index.test.ts | 76 ++++ .../identifyUser/__tests__/snapshot.test.ts | 76 ++++ .../launchpad/identifyUser/generated-types.ts | 22 ++ .../launchpad/identifyUser/index.ts | 92 +++++ .../src/destinations/launchpad/index.ts | 99 +++++ .../destinations/launchpad/launchpad-types.ts | 29 ++ .../__snapshots__/snapshot.test.ts.snap | 38 ++ .../trackEvent/__tests__/index.test.ts | 63 +++ .../trackEvent/__tests__/snapshot.test.ts | 75 ++++ .../launchpad/trackEvent/generated-types.ts | 50 +++ .../launchpad/trackEvent/index.ts | 72 ++++ .../trackEvent/launchpad-properties.ts | 89 +++++ .../src/destinations/launchpad/utils.ts | 20 + .../__snapshots__/snapshot.test.ts.snap | 32 ++ .../livelike-cloud/__tests__/index.test.ts | 26 ++ .../livelike-cloud/__tests__/snapshot.test.ts | 77 ++++ .../livelike-cloud/generated-types.ts | 12 + .../src/destinations/livelike-cloud/index.ts | 84 ++++ .../destinations/livelike-cloud/properties.ts | 1 + .../__snapshots__/snapshot.test.ts.snap | 32 ++ .../trackEvent/__tests__/index.test.ts | 250 ++++++++++++ .../trackEvent/__tests__/snapshot.test.ts | 75 ++++ .../trackEvent/generated-types.ts | 34 ++ .../livelike-cloud/trackEvent/index.ts | 107 +++++ .../mixpanel/trackEvent/functions.ts | 117 +++--- .../src/destinations/mixpanel/utils.ts | 16 +- .../__tests__/index.test.ts | 22 ++ .../pinterest-conversions/generated-types.ts | 12 + .../pinterest-conversions/index.ts | 41 ++ .../reportConversionEvent/generated-types.ts | 3 + .../reportConversionEvent/index.ts | 18 + .../__tests__/index.test.ts | 15 +- .../__snapshots__/snapshot.test.ts.snap | 3 +- .../ripe/group/__tests__/snapshot.test.ts | 2 + .../src/destinations/ripe/group/index.ts | 2 +- .../__snapshots__/snapshot.test.ts.snap | 3 +- .../ripe/identify/__tests__/snapshot.test.ts | 2 + .../src/destinations/ripe/identify/index.ts | 2 +- .../__snapshots__/snapshot.test.ts.snap | 3 +- .../ripe/page/__tests__/snapshot.test.ts | 2 + .../src/destinations/ripe/page/index.ts | 2 +- .../__snapshots__/snapshot.test.ts.snap | 3 +- .../ripe/track/__tests__/snapshot.test.ts | 2 + .../src/destinations/ripe/track/index.ts | 2 +- .../saleswings/__tests__/index.test.ts | 17 +- .../src/destinations/saleswings/api.ts | 51 +-- .../src/destinations/saleswings/common.ts | 29 +- .../src/destinations/saleswings/fields.ts | 4 +- .../saleswings/generated-types.ts | 4 + .../src/destinations/saleswings/index.ts | 16 +- .../__snapshots__/snapshot.test.ts.snap | 46 +-- .../__tests__/index.test.ts | 91 ++--- .../__tests__/snapshot.test.ts | 6 +- .../submitIdentifyEvent/generated-types.ts | 4 +- .../saleswings/submitIdentifyEvent/index.ts | 25 +- .../__snapshots__/snapshot.test.ts.snap | 38 +- .../submitPageEvent/__tests__/index.test.ts | 78 ++-- .../__tests__/snapshot.test.ts | 6 +- .../submitPageEvent/generated-types.ts | 4 +- .../saleswings/submitPageEvent/index.ts | 24 +- .../__snapshots__/snapshot.test.ts.snap | 64 ++- .../submitScreenEvent/__tests__/index.test.ts | 75 ++-- .../__tests__/snapshot.test.ts | 6 +- .../submitScreenEvent/generated-types.ts | 4 +- .../saleswings/submitScreenEvent/index.ts | 27 +- .../__snapshots__/snapshot.test.ts.snap | 64 ++- .../submitTrackEvent/__tests__/index.test.ts | 176 ++------- .../__tests__/snapshot.test.ts | 6 +- .../submitTrackEvent/generated-types.ts | 4 +- .../saleswings/submitTrackEvent/index.ts | 27 +- .../src/destinations/saleswings/testing.ts | 34 +- .../__snapshots__/snapshot.test.ts.snap | 12 + .../destinations/segment-profiles/index.ts | 2 +- .../segment-profiles/segment-properties.ts | 4 +- .../__snapshots__/index.test.ts.snap | 3 + .../__snapshots__/snapshot.test.ts.snap | 6 + .../sendGroup/generated-types.ts | 2 +- .../segment-profiles/sendGroup/index.ts | 5 + .../__snapshots__/index.test.ts.snap | 3 + .../__snapshots__/snapshot.test.ts.snap | 6 + .../sendIdentify/generated-types.ts | 2 +- .../segment-profiles/sendIdentify/index.ts | 5 + .../voucherify/__tests__/customEvent.test.ts | 148 +++++++ .../voucherify/__tests__/customer.test.ts | 55 +++ .../addCustomEvent/generated-types.ts | 26 ++ .../voucherify/addCustomEvent/index.ts | 81 ++++ .../assignCustomerToGroup/generated-types.ts | 26 ++ .../voucherify/assignCustomerToGroup/index.ts | 72 ++++ .../voucherify/generated-types.ts | 16 + .../src/destinations/voucherify/index.ts | 116 ++++++ .../upsertCustomer/generated-types.ts | 34 ++ .../voucherify/upsertCustomer/index.ts | 107 +++++ .../destinations/voucherify/url-provider.ts | 9 + .../destinations/voucherify/url-validator.ts | 17 + .../__snapshots__/snapshot.test.ts.snap | 8 +- .../vwo/identifyUser/__tests__/index.test.ts | 4 +- .../vwo/identifyUser/generated-types.ts | 10 +- .../destinations/vwo/identifyUser/index.ts | 12 +- .../src/destinations/vwo/index.ts | 2 +- .../vwo/pageVisit/generated-types.ts | 10 +- .../src/destinations/vwo/pageVisit/index.ts | 12 +- .../__snapshots__/snapshot.test.ts.snap | 4 +- .../vwo/trackEvent/generated-types.ts | 12 +- .../src/destinations/vwo/trackEvent/index.ts | 14 +- .../src/destinations/vwo/utility.ts | 4 +- .../webhook/__test__/webhook.test.ts | 52 +++ .../src/destinations/webhook/index.ts | 7 +- .../destination-subscriptions/package.json | 4 +- .../destination-subscriptions/scripts/size.sh | 8 + scripts/test-browser.sh | 8 + 228 files changed, 10215 insertions(+), 974 deletions(-) create mode 100755 packages/browser-destinations/scripts/build-web-stage.sh create mode 100755 packages/browser-destinations/scripts/build-web.sh create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/addPaymentInfo.test.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/addToCart.test.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/addToWishlist.test.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/beginCheckout.test.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/customEvent.test.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/generateLead.test.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/login.test.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/purchase.test.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/refund.test.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/removeFromCart.test.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/search.test.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/selectItem.test.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/selectPromotion.test.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/signUp.test.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/viewCart.test.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/viewItem.test.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/viewItemList.test.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/viewPromotion.test.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/addPaymentInfo/generated-types.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/addPaymentInfo/index.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/addToCart/generated-types.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/addToCart/index.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/addToWishlist/generated-types.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/addToWishlist/index.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/beginCheckout/generated-types.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/beginCheckout/index.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/customEvent/generated-types.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/customEvent/index.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/ga4-functions.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/ga4-properties.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/ga4-types.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/generateLead/generated-types.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/generateLead/index.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/generated-types.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/index.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/login/generated-types.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/login/index.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/purchase/generated-types.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/purchase/index.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/refund/generated-types.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/refund/index.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/removeFromCart/generated-types.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/removeFromCart/index.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/search/generated-types.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/search/index.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/selectItem/generated-types.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/selectItem/index.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/selectPromotion/generated-types.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/selectPromotion/index.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/setConfigurationFields/generated-types.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/setConfigurationFields/index.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/signUp/generated-types.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/signUp/index.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/types.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/viewCart/generated-types.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/viewCart/index.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/viewItem/generated-types.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/viewItem/index.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/viewItemList/generated-types.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/viewItemList/index.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/viewPromotion/generated-types.ts create mode 100644 packages/browser-destinations/src/destinations/google-analytics-4-web/viewPromotion/index.ts create mode 100644 packages/destination-actions/src/destinations/algolia-insights/__tests__/__snapshots__/snapshot.test.ts.snap create mode 100644 packages/destination-actions/src/destinations/algolia-insights/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/algolia-insights/__tests__/snapshot.test.ts create mode 100644 packages/destination-actions/src/destinations/algolia-insights/algolia-insight-api.ts create mode 100644 packages/destination-actions/src/destinations/algolia-insights/conversionEvents/__tests__/__snapshots__/snapshot.test.ts.snap create mode 100644 packages/destination-actions/src/destinations/algolia-insights/conversionEvents/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/algolia-insights/conversionEvents/__tests__/snapshot.test.ts create mode 100644 packages/destination-actions/src/destinations/algolia-insights/conversionEvents/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/algolia-insights/conversionEvents/index.ts create mode 100644 packages/destination-actions/src/destinations/algolia-insights/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/algolia-insights/index.ts create mode 100644 packages/destination-actions/src/destinations/algolia-insights/productClickedEvents/__tests__/__snapshots__/snapshot.test.ts.snap create mode 100644 packages/destination-actions/src/destinations/algolia-insights/productClickedEvents/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/algolia-insights/productClickedEvents/__tests__/snapshot.test.ts create mode 100644 packages/destination-actions/src/destinations/algolia-insights/productClickedEvents/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/algolia-insights/productClickedEvents/index.ts create mode 100644 packages/destination-actions/src/destinations/algolia-insights/productViewedEvents/__tests__/__snapshots__/snapshot.test.ts.snap create mode 100644 packages/destination-actions/src/destinations/algolia-insights/productViewedEvents/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/algolia-insights/productViewedEvents/__tests__/snapshot.test.ts create mode 100644 packages/destination-actions/src/destinations/algolia-insights/productViewedEvents/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/algolia-insights/productViewedEvents/index.ts create mode 100644 packages/destination-actions/src/destinations/heap/heapUtils.ts create mode 100644 packages/destination-actions/src/destinations/launchpad/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/launchpad/groupIdentifyUser/__tests__/__snapshots__/snapshot.test.ts.snap create mode 100644 packages/destination-actions/src/destinations/launchpad/groupIdentifyUser/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/launchpad/groupIdentifyUser/__tests__/snapshot.test.ts create mode 100644 packages/destination-actions/src/destinations/launchpad/groupIdentifyUser/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/launchpad/groupIdentifyUser/index.ts create mode 100644 packages/destination-actions/src/destinations/launchpad/identifyUser/__tests__/__snapshots__/snapshot.test.ts.snap create mode 100644 packages/destination-actions/src/destinations/launchpad/identifyUser/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/launchpad/identifyUser/__tests__/snapshot.test.ts create mode 100644 packages/destination-actions/src/destinations/launchpad/identifyUser/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/launchpad/identifyUser/index.ts create mode 100644 packages/destination-actions/src/destinations/launchpad/index.ts create mode 100644 packages/destination-actions/src/destinations/launchpad/launchpad-types.ts create mode 100644 packages/destination-actions/src/destinations/launchpad/trackEvent/__tests__/__snapshots__/snapshot.test.ts.snap create mode 100644 packages/destination-actions/src/destinations/launchpad/trackEvent/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/launchpad/trackEvent/__tests__/snapshot.test.ts create mode 100644 packages/destination-actions/src/destinations/launchpad/trackEvent/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/launchpad/trackEvent/index.ts create mode 100644 packages/destination-actions/src/destinations/launchpad/trackEvent/launchpad-properties.ts create mode 100644 packages/destination-actions/src/destinations/launchpad/utils.ts create mode 100644 packages/destination-actions/src/destinations/livelike-cloud/__tests__/__snapshots__/snapshot.test.ts.snap create mode 100644 packages/destination-actions/src/destinations/livelike-cloud/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/livelike-cloud/__tests__/snapshot.test.ts create mode 100644 packages/destination-actions/src/destinations/livelike-cloud/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/livelike-cloud/index.ts create mode 100644 packages/destination-actions/src/destinations/livelike-cloud/properties.ts create mode 100644 packages/destination-actions/src/destinations/livelike-cloud/trackEvent/__tests__/__snapshots__/snapshot.test.ts.snap create mode 100644 packages/destination-actions/src/destinations/livelike-cloud/trackEvent/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/livelike-cloud/trackEvent/__tests__/snapshot.test.ts create mode 100644 packages/destination-actions/src/destinations/livelike-cloud/trackEvent/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/livelike-cloud/trackEvent/index.ts create mode 100644 packages/destination-actions/src/destinations/pinterest-conversions/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/pinterest-conversions/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/pinterest-conversions/index.ts create mode 100644 packages/destination-actions/src/destinations/pinterest-conversions/reportConversionEvent/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/pinterest-conversions/reportConversionEvent/index.ts create mode 100644 packages/destination-actions/src/destinations/voucherify/__tests__/customEvent.test.ts create mode 100644 packages/destination-actions/src/destinations/voucherify/__tests__/customer.test.ts create mode 100644 packages/destination-actions/src/destinations/voucherify/addCustomEvent/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/voucherify/addCustomEvent/index.ts create mode 100644 packages/destination-actions/src/destinations/voucherify/assignCustomerToGroup/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/voucherify/assignCustomerToGroup/index.ts create mode 100644 packages/destination-actions/src/destinations/voucherify/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/voucherify/index.ts create mode 100644 packages/destination-actions/src/destinations/voucherify/upsertCustomer/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/voucherify/upsertCustomer/index.ts create mode 100644 packages/destination-actions/src/destinations/voucherify/url-provider.ts create mode 100644 packages/destination-actions/src/destinations/voucherify/url-validator.ts create mode 100755 packages/destination-subscriptions/scripts/size.sh create mode 100755 scripts/test-browser.sh diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 60e448fc91..99bb07a8a3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -21,7 +21,7 @@ cli/ @segmentio/build-experience-team core/ @segmentio/build-experience-team # Destination definitions and their actions -destination-actions/ @segmentio/build-experience-team @segmentio/strategic-connections-team +destination-actions/ @segmentio/strategic-connections-team # Utilities for event payload validation against an action's subscription AST. destination-subscriptions/ @segmentio/build-experience-team diff --git a/package.json b/package.json index 4c6c356c70..7ac0838a75 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "subscriptions": "NODE_OPTIONS=--openssl-legacy-provider yarn workspace @segment/destination-subscriptions", "test": "lerna run test --stream", "test-partners": "lerna run test --stream --ignore @segment/actions-core --ignore @segment/actions-cli --ignore @segment/ajv-human-errors", - "test-browser": "lerna run build:karma --stream && NODE_OPTIONS=--openssl-legacy-provider karma start", + "test-browser": "bash scripts/test-browser.sh", "typecheck": "lerna run typecheck --stream", "alpha": "lerna publish --canary --preid $(git branch --show-current) --include-merged-tags", "prepare": "husky install", diff --git a/packages/actions-shared/package.json b/packages/actions-shared/package.json index a917d3bfca..f68940420b 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.33.0", + "version": "1.34.0", "repository": { "type": "git", "url": "https://github.com/segmentio/action-destinations", @@ -37,7 +37,7 @@ }, "dependencies": { "@amplitude/ua-parser-js": "^0.7.25", - "@segment/actions-core": "^3.51.0", + "@segment/actions-core": "^3.52.0", "cheerio": "^1.0.0-rc.10", "dayjs": "^1.10.7", "escape-goat": "^3", diff --git a/packages/browser-destinations/package.json b/packages/browser-destinations/package.json index c05cfad30c..c0838c6bac 100644 --- a/packages/browser-destinations/package.json +++ b/packages/browser-destinations/package.json @@ -1,6 +1,6 @@ { "name": "@segment/browser-destinations", - "version": "3.74.0", + "version": "3.75.0", "description": "Action based browser destinations", "author": "Netto Farah", "license": "MIT", @@ -19,8 +19,8 @@ "build": "yarn clean && yarn build-ts && yarn build-cjs && yarn build-web", "build-ts": "yarn tsc -b tsconfig.build.json", "build-cjs": "yarn tsc -p ./tsconfig.build.json -m commonjs --outDir ./dist/cjs/", - "build-web": "NODE_ENV=production ASSET_ENV=production NODE_OPTIONS=--openssl-legacy-provider yarn webpack -c webpack.config.js", - "build-web-stage": "NODE_ENV=production ASSET_ENV=stage NODE_OPTIONS=--openssl-legacy-provider yarn webpack -c webpack.config.js", + "build-web": "bash scripts/build-web.sh", + "build-web-stage": "bash scripts/build-web-stage.sh", "deploy-prod": "yarn build-web && aws s3 sync ./dist/web/ s3://segment-ajs-next-destinations-production/next-integrations/actions --grants read=id=$npm_config_prod_cdn_oai,id=$npm_config_prod_custom_domain_oai", "deploy-stage": "yarn build-web-stage && aws-okta exec plat-write -- aws s3 sync ./dist/web/ s3://segment-ajs-next-destinations-stage/next-integrations/actions --grants read=id=$npm_config_stage_cdn_oai,id=$npm_config_stage_custom_domain_oai", "clean": "tsc -b tsconfig.build.json --clean", @@ -34,9 +34,9 @@ "@braze/web-sdk": "npm:@braze/web-sdk@^4.1.0", "@braze/web-sdk-v3": "npm:@braze/web-sdk@^3.5.1", "@fullstory/browser": "^1.4.9", - "@segment/actions-shared": "^1.33.0", + "@segment/actions-shared": "^1.34.0", "@segment/analytics-next": "^1.29.3", - "@segment/destination-subscriptions": "^3.15.0", + "@segment/destination-subscriptions": "^3.16.0", "dayjs": "^1.10.7", "logrocket": "^3.0.1", "tslib": "^2.3.1", @@ -48,7 +48,7 @@ "@babel/plugin-transform-modules-commonjs": "^7.13.8", "@babel/preset-env": "^7.13.10", "@babel/preset-typescript": "^7.13.0", - "@segment/actions-core": "^3.51.0", + "@segment/actions-core": "^3.52.0", "@types/amplitude-js": "^7.0.1", "@types/jest": "^27.0.0", "babel-jest": "^27.3.1", diff --git a/packages/browser-destinations/scripts/build-web-stage.sh b/packages/browser-destinations/scripts/build-web-stage.sh new file mode 100755 index 0000000000..73f5a4dc75 --- /dev/null +++ b/packages/browser-destinations/scripts/build-web-stage.sh @@ -0,0 +1,11 @@ +#!/bin/bash +NODE_VERSION="$(node --version)"; +NODE_VERSION_MAJOR="${NODE_VERSION:1}"; # strip the "v" prefix +NODE_VERSION_MAJOR="${NODE_VERSION_MAJOR%%.*}"; #get everything before the first dot +if [ "$NODE_VERSION_MAJOR" -ge "18" ]; then +NODE_ENV=production ASSET_ENV=stage NODE_OPTIONS=--openssl-legacy-provider yarn webpack -c webpack.config.js +else NODE_ENV=production ASSET_ENV=stage yarn webpack -c webpack.config.js +fi + + + diff --git a/packages/browser-destinations/scripts/build-web.sh b/packages/browser-destinations/scripts/build-web.sh new file mode 100755 index 0000000000..fcd1016d78 --- /dev/null +++ b/packages/browser-destinations/scripts/build-web.sh @@ -0,0 +1,8 @@ +#!/bin/bash +NODE_VERSION="$(node --version)"; +NODE_VERSION_MAJOR="${NODE_VERSION:1}"; # strip the "v" prefix +NODE_VERSION_MAJOR="${NODE_VERSION_MAJOR%%.*}"; #get everything before the first dot +if [ "$NODE_VERSION_MAJOR" -ge "18" ]; then +NODE_ENV=production ASSET_ENV=production NODE_OPTIONS=--openssl-legacy-provider yarn webpack -c webpack.config.js +else NODE_ENV=production ASSET_ENV=production yarn webpack -c webpack.config.js +fi diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/addPaymentInfo.test.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/addPaymentInfo.test.ts new file mode 100644 index 0000000000..203ce1add6 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/addPaymentInfo.test.ts @@ -0,0 +1,101 @@ +import { Subscription } from '../../../lib/browser-destinations' +import { Analytics, Context } from '@segment/analytics-next' +import googleAnalytics4Web, { destination } from '../index' +import { GA } from '../types' + +const subscriptions: Subscription[] = [ + { + partnerAction: 'addPaymentInfo', + name: 'Add Payment Info', + enabled: true, + subscribe: 'type = "track"', + mapping: { + currency: { + '@path': '$.properties.currency' + }, + value: { + '@path': '$.properties.value' + }, + coupon: { + '@path': '$.properties.coupon' + }, + items: [ + { + item_name: { + '@path': `$.properties.products.0.name` + }, + item_id: { + '@path': `$.properties.products.0.product_id` + }, + currency: { + '@path': `$.properties.products.0.currency` + }, + price: { + '@path': `$.properties.products.0.price` + }, + quantity: { + '@path': `$.properties.products.0.quantity` + } + } + ] + } + } +] + +describe('GoogleAnalytics4Web.addPaymentInfo', () => { + const settings = { + measurementID: 'test123' + } + + let mockGA4: GA + let addPaymentInfoEvent: any + beforeEach(async () => { + jest.restoreAllMocks() + + const [trackEventPlugin] = await googleAnalytics4Web({ + ...settings, + subscriptions + }) + addPaymentInfoEvent = trackEventPlugin + + jest.spyOn(destination, 'initialize').mockImplementation(() => { + mockGA4 = { + gtag: jest.fn() + } + return Promise.resolve(mockGA4.gtag) + }) + await trackEventPlugin.load(Context.system(), {} as Analytics) + }) + + test('GA4 addPaymentInfo Event', 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.gtag).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 + }) + ) + }) +}) diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/addToCart.test.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/addToCart.test.ts new file mode 100644 index 0000000000..c3e4a6c370 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/addToCart.test.ts @@ -0,0 +1,95 @@ +import { Subscription } from '../../../lib/browser-destinations' +import { Analytics, Context } from '@segment/analytics-next' +import googleAnalytics4Web, { destination } from '../index' +import { GA } from '../types' + +const subscriptions: Subscription[] = [ + { + partnerAction: 'addToCart', + name: 'Add To Cart', + enabled: true, + subscribe: 'type = "track"', + mapping: { + currency: { + '@path': '$.properties.currency' + }, + value: { + '@path': '$.properties.value' + }, + items: [ + { + item_name: { + '@path': `$.properties.products.0.name` + }, + item_id: { + '@path': `$.properties.products.0.product_id` + }, + currency: { + '@path': `$.properties.products.0.currency` + }, + price: { + '@path': `$.properties.products.0.price` + }, + quantity: { + '@path': `$.properties.products.0.quantity` + } + } + ] + } + } +] + +describe('GoogleAnalytics4Web.addToCart', () => { + const settings = { + measurementID: 'test123' + } + + let mockGA4: GA + let addToCartEvent: any + beforeEach(async () => { + jest.restoreAllMocks() + + const [trackEventPlugin] = await googleAnalytics4Web({ + ...settings, + subscriptions + }) + addToCartEvent = trackEventPlugin + + jest.spyOn(destination, 'initialize').mockImplementation(() => { + mockGA4 = { + gtag: jest.fn() + } + return Promise.resolve(mockGA4.gtag) + }) + await trackEventPlugin.load(Context.system(), {} as Analytics) + }) + + test('GA4 addToCart Event', 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.gtag).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 + }) + ) + }) +}) diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/addToWishlist.test.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/addToWishlist.test.ts new file mode 100644 index 0000000000..f4e63b2901 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/addToWishlist.test.ts @@ -0,0 +1,95 @@ +import { Subscription } from '../../../lib/browser-destinations' +import { Analytics, Context } from '@segment/analytics-next' +import googleAnalytics4Web, { destination } from '../index' +import { GA } from '../types' + +const subscriptions: Subscription[] = [ + { + partnerAction: 'addToWishlist', + name: 'Add To Wishlist', + enabled: true, + subscribe: 'type = "track"', + mapping: { + currency: { + '@path': '$.properties.currency' + }, + value: { + '@path': '$.properties.value' + }, + items: [ + { + item_name: { + '@path': `$.properties.products.0.name` + }, + item_id: { + '@path': `$.properties.products.0.product_id` + }, + currency: { + '@path': `$.properties.products.0.currency` + }, + price: { + '@path': `$.properties.products.0.price` + }, + quantity: { + '@path': `$.properties.products.0.quantity` + } + } + ] + } + } +] + +describe('GoogleAnalytics4Web.addToWishlist', () => { + const settings = { + measurementID: 'test123' + } + + let mockGA4: GA + let addToWishlistEvent: any + beforeEach(async () => { + jest.restoreAllMocks() + + const [trackEventPlugin] = await googleAnalytics4Web({ + ...settings, + subscriptions + }) + addToWishlistEvent = trackEventPlugin + + jest.spyOn(destination, 'initialize').mockImplementation(() => { + mockGA4 = { + gtag: jest.fn() + } + return Promise.resolve(mockGA4.gtag) + }) + await trackEventPlugin.load(Context.system(), {} as Analytics) + }) + + test('Track call without parameters', 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.gtag).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 + }) + ) + }) +}) diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/beginCheckout.test.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/beginCheckout.test.ts new file mode 100644 index 0000000000..233b692b73 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/beginCheckout.test.ts @@ -0,0 +1,101 @@ +import { Subscription } from '../../../lib/browser-destinations' +import { Analytics, Context } from '@segment/analytics-next' +import googleAnalytics4Web, { destination } from '../index' +import { GA } from '../types' + +const subscriptions: Subscription[] = [ + { + partnerAction: 'beginCheckout', + name: 'Begin Checkout', + enabled: true, + subscribe: 'type = "track"', + mapping: { + currency: { + '@path': '$.properties.currency' + }, + value: { + '@path': '$.properties.value' + }, + coupon: { + '@path': '$.properties.coupon' + }, + items: [ + { + item_name: { + '@path': `$.properties.products.0.name` + }, + item_id: { + '@path': `$.properties.products.0.product_id` + }, + currency: { + '@path': `$.properties.products.0.currency` + }, + price: { + '@path': `$.properties.products.0.price` + }, + quantity: { + '@path': `$.properties.products.0.quantity` + } + } + ] + } + } +] + +describe('GoogleAnalytics4Web.beginCheckout', () => { + const settings = { + measurementID: 'test123' + } + + let mockGA4: GA + let beginCheckoutEvent: any + beforeEach(async () => { + jest.restoreAllMocks() + + const [trackEventPlugin] = await googleAnalytics4Web({ + ...settings, + subscriptions + }) + beginCheckoutEvent = trackEventPlugin + + jest.spyOn(destination, 'initialize').mockImplementation(() => { + mockGA4 = { + gtag: jest.fn() + } + return Promise.resolve(mockGA4.gtag) + }) + await trackEventPlugin.load(Context.system(), {} as Analytics) + }) + + test('GA4 beginCheckout Event', async () => { + const context = new Context({ + event: 'Begin Checkout', + 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 beginCheckoutEvent.track?.(context) + + expect(mockGA4.gtag).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 + }) + ) + }) +}) diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/customEvent.test.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/customEvent.test.ts new file mode 100644 index 0000000000..da8b4d73f7 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/customEvent.test.ts @@ -0,0 +1,70 @@ +import { Subscription } from '../../../lib/browser-destinations' +import { Analytics, Context } from '@segment/analytics-next' +import googleAnalytics4Web, { destination } from '../index' +import { GA } from '../types' + +const subscriptions: Subscription[] = [ + { + partnerAction: 'customEvent', + name: 'Custom Event', + enabled: true, + subscribe: 'type = "track"', + mapping: { + name: { + '@path': '$.event' + }, + params: { + '@path': '$.properties.params' + } + } + } +] + +describe('GoogleAnalytics4Web.customEvent', () => { + const settings = { + measurementID: 'test123' + } + + let mockGA4: GA + let customEvent: any + beforeEach(async () => { + jest.restoreAllMocks() + + const [trackEventPlugin] = await googleAnalytics4Web({ + ...settings, + subscriptions + }) + customEvent = trackEventPlugin + + jest.spyOn(destination, 'initialize').mockImplementation(() => { + mockGA4 = { + gtag: jest.fn() + } + return Promise.resolve(mockGA4.gtag) + }) + await trackEventPlugin.load(Context.system(), {} as Analytics) + }) + + test('GA4 customEvent Event', async () => { + const context = new Context({ + event: 'Custom Event', + type: 'track', + properties: { + params: [ + { + paramOne: 'test123', + paramTwo: 'test123', + paramThree: 123 + } + ] + } + }) + await customEvent.track?.(context) + + expect(mockGA4.gtag).toHaveBeenCalledWith( + expect.anything(), + expect.stringContaining('Custom_Event'), + expect.objectContaining([{ paramOne: 'test123', paramThree: 123, paramTwo: 'test123' }]) + ) + }) +}) diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/generateLead.test.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/generateLead.test.ts new file mode 100644 index 0000000000..59a0e63319 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/generateLead.test.ts @@ -0,0 +1,68 @@ +import { Subscription } from '../../../lib/browser-destinations' +import { Analytics, Context } from '@segment/analytics-next' +import googleAnalytics4Web, { destination } from '../index' +import { GA } from '../types' + +const subscriptions: Subscription[] = [ + { + partnerAction: 'generateLead', + name: 'Generate Leaad', + enabled: true, + subscribe: 'type = "track"', + mapping: { + currency: { + '@path': '$.properties.currency' + }, + value: { + '@path': '$.properties.value' + } + } + } +] + +describe('GoogleAnalytics4Web.generateLead', () => { + const settings = { + measurementID: 'test123' + } + + let mockGA4: GA + let generateLeadEvent: any + beforeEach(async () => { + jest.restoreAllMocks() + + const [trackEventPlugin] = await googleAnalytics4Web({ + ...settings, + subscriptions + }) + generateLeadEvent = trackEventPlugin + + jest.spyOn(destination, 'initialize').mockImplementation(() => { + mockGA4 = { + gtag: jest.fn() + } + return Promise.resolve(mockGA4.gtag) + }) + await trackEventPlugin.load(Context.system(), {} as Analytics) + }) + + test('GA4 generateLead Event', async () => { + const context = new Context({ + event: 'Generate Lead', + type: 'track', + properties: { + currency: 'USD', + value: 10 + } + }) + await generateLeadEvent.track?.(context) + + expect(mockGA4.gtag).toHaveBeenCalledWith( + expect.anything(), + expect.stringContaining('generate_lead'), + expect.objectContaining({ + currency: 'USD', + value: 10 + }) + ) + }) +}) diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/login.test.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/login.test.ts new file mode 100644 index 0000000000..f13b217708 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/login.test.ts @@ -0,0 +1,63 @@ +import { Subscription } from '../../../lib/browser-destinations' +import { Analytics, Context } from '@segment/analytics-next' +import googleAnalytics4Web, { destination } from '../index' +import { GA } from '../types' + +const subscriptions: Subscription[] = [ + { + partnerAction: 'login', + name: 'Login', + enabled: true, + subscribe: 'type = "track"', + mapping: { + method: { + '@path': '$.properties.method' + } + } + } +] + +describe('GoogleAnalytics4Web.login', () => { + const settings = { + measurementID: 'test123' + } + + let mockGA4: GA + let loginEvent: any + beforeEach(async () => { + jest.restoreAllMocks() + + const [trackEventPlugin] = await googleAnalytics4Web({ + ...settings, + subscriptions + }) + loginEvent = trackEventPlugin + + jest.spyOn(destination, 'initialize').mockImplementation(() => { + mockGA4 = { + gtag: jest.fn() + } + return Promise.resolve(mockGA4.gtag) + }) + await trackEventPlugin.load(Context.system(), {} as Analytics) + }) + + test('GA4 login Event', async () => { + const context = new Context({ + event: 'Login', + type: 'track', + properties: { + method: 'Google' + } + }) + await loginEvent.track?.(context) + + expect(mockGA4.gtag).toHaveBeenCalledWith( + expect.anything(), + expect.stringContaining('login'), + expect.objectContaining({ + method: 'Google' + }) + ) + }) +}) diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/purchase.test.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/purchase.test.ts new file mode 100644 index 0000000000..dd603919d7 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/purchase.test.ts @@ -0,0 +1,103 @@ +import { Subscription } from '../../../lib/browser-destinations' +import { Analytics, Context } from '@segment/analytics-next' +import googleAnalytics4Web, { destination } from '../index' +import { GA } from '../types' + +const subscriptions: Subscription[] = [ + { + partnerAction: 'purchase', + name: 'Purchase', + enabled: true, + subscribe: 'type = "track"', + mapping: { + currency: { + '@path': '$.properties.currency' + }, + value: { + '@path': '$.properties.value' + }, + coupon: { + '@path': '$.properties.coupon' + }, + transaction_id: { + '@path': '$.properties.transaction_id' + }, + items: [ + { + item_name: { + '@path': `$.properties.products.0.name` + }, + item_id: { + '@path': `$.properties.products.0.product_id` + }, + currency: { + '@path': `$.properties.products.0.currency` + }, + price: { + '@path': `$.properties.products.0.price` + }, + quantity: { + '@path': `$.properties.products.0.quantity` + } + } + ] + } + } +] + +describe('GoogleAnalytics4Web.purchase', () => { + const settings = { + measurementID: 'test123' + } + + let mockGA4: GA + let purchaseEvent: any + beforeEach(async () => { + jest.restoreAllMocks() + + const [trackEventPlugin] = await googleAnalytics4Web({ + ...settings, + subscriptions + }) + purchaseEvent = trackEventPlugin + + jest.spyOn(destination, 'initialize').mockImplementation(() => { + mockGA4 = { + gtag: jest.fn() + } + return Promise.resolve(mockGA4.gtag) + }) + await trackEventPlugin.load(Context.system(), {} as Analytics) + }) + + test('GA4 purchase Event', 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.gtag).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 + }) + ) + }) +}) diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/refund.test.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/refund.test.ts new file mode 100644 index 0000000000..bc8db11d10 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/refund.test.ts @@ -0,0 +1,103 @@ +import { Subscription } from '../../../lib/browser-destinations' +import { Analytics, Context } from '@segment/analytics-next' +import googleAnalytics4Web, { destination } from '../index' +import { GA } from '../types' + +const subscriptions: Subscription[] = [ + { + partnerAction: 'refund', + name: 'refund', + enabled: true, + subscribe: 'type = "track"', + mapping: { + currency: { + '@path': '$.properties.currency' + }, + value: { + '@path': '$.properties.value' + }, + coupon: { + '@path': '$.properties.coupon' + }, + transaction_id: { + '@path': '$.properties.transaction_id' + }, + items: [ + { + item_name: { + '@path': `$.properties.products.0.name` + }, + item_id: { + '@path': `$.properties.products.0.product_id` + }, + currency: { + '@path': `$.properties.products.0.currency` + }, + price: { + '@path': `$.properties.products.0.price` + }, + quantity: { + '@path': `$.properties.products.0.quantity` + } + } + ] + } + } +] + +describe('GoogleAnalytics4Web.refund', () => { + const settings = { + measurementID: 'test123' + } + + let mockGA4: GA + let refundEvent: any + beforeEach(async () => { + jest.restoreAllMocks() + + const [trackEventPlugin] = await googleAnalytics4Web({ + ...settings, + subscriptions + }) + refundEvent = trackEventPlugin + + jest.spyOn(destination, 'initialize').mockImplementation(() => { + mockGA4 = { + gtag: jest.fn() + } + return Promise.resolve(mockGA4.gtag) + }) + await trackEventPlugin.load(Context.system(), {} as Analytics) + }) + + test('GA4 Refund Event', async () => { + const context = new Context({ + event: 'Refund', + type: 'track', + properties: { + currency: 'USD', + value: 10, + transaction_id: 12321, + products: [ + { + product_id: '12345', + name: 'Monopoly: 3rd Edition', + currency: 'USD' + } + ] + } + }) + await refundEvent.track?.(context) + + expect(mockGA4.gtag).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 + }) + ) + }) +}) diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/removeFromCart.test.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/removeFromCart.test.ts new file mode 100644 index 0000000000..287e331366 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/removeFromCart.test.ts @@ -0,0 +1,99 @@ +import { Subscription } from '../../../lib/browser-destinations' +import { Analytics, Context } from '@segment/analytics-next' +import googleAnalytics4Web, { destination } from '../index' +import { GA } from '../types' + +const subscriptions: Subscription[] = [ + { + partnerAction: 'removeFromCart', + name: 'Remove from cart', + enabled: true, + subscribe: 'type = "track"', + mapping: { + currency: { + '@path': '$.properties.currency' + }, + value: { + '@path': '$.properties.value' + }, + coupon: { + '@path': '$.properties.coupon' + }, + items: [ + { + item_name: { + '@path': `$.properties.products.0.name` + }, + item_id: { + '@path': `$.properties.products.0.product_id` + }, + currency: { + '@path': `$.properties.products.0.currency` + }, + price: { + '@path': `$.properties.products.0.price` + }, + quantity: { + '@path': `$.properties.products.0.quantity` + } + } + ] + } + } +] + +describe('GoogleAnalytics4Web.removeFromCart', () => { + const settings = { + measurementID: 'test123' + } + + let mockGA4: GA + let removeFromCartEvent: any + beforeEach(async () => { + jest.restoreAllMocks() + + const [trackEventPlugin] = await googleAnalytics4Web({ + ...settings, + subscriptions + }) + removeFromCartEvent = trackEventPlugin + + jest.spyOn(destination, 'initialize').mockImplementation(() => { + mockGA4 = { + gtag: jest.fn() + } + return Promise.resolve(mockGA4.gtag) + }) + await trackEventPlugin.load(Context.system(), {} as Analytics) + }) + + test('GA4 removeFromCart Event', 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.gtag).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 + }) + ) + }) +}) diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/search.test.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/search.test.ts new file mode 100644 index 0000000000..07fb86902f --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/search.test.ts @@ -0,0 +1,64 @@ +import { Subscription } from '../../../lib/browser-destinations' +import { Analytics, Context } from '@segment/analytics-next' +import googleAnalytics4Web, { destination } from '../index' +import { GA } from '../types' + +const subscriptions: Subscription[] = [ + { + partnerAction: 'search', + name: 'search', + enabled: true, + subscribe: 'type = "track"', + mapping: { + search_term: { + '@path': '$.properties.search_term' + } + } + } +] + +describe('GoogleAnalytics4Web.search', () => { + const settings = { + measurementID: 'test123' + } + + let mockGA4: GA + let searchEvent: any + beforeEach(async () => { + jest.restoreAllMocks() + + const [trackEventPlugin] = await googleAnalytics4Web({ + ...settings, + subscriptions + }) + searchEvent = trackEventPlugin + + jest.spyOn(destination, 'initialize').mockImplementation(() => { + mockGA4 = { + gtag: jest.fn() + } + return Promise.resolve(mockGA4.gtag) + }) + await trackEventPlugin.load(Context.system(), {} as Analytics) + }) + + test('GA4 search Event', async () => { + const context = new Context({ + event: 'search', + type: 'track', + properties: { + search_term: 'Monopoly: 3rd Edition' + } + }) + + await searchEvent.track?.(context) + + expect(mockGA4.gtag).toHaveBeenCalledWith( + expect.anything(), + expect.stringContaining('search'), + expect.objectContaining({ + search_term: 'Monopoly: 3rd Edition' + }) + ) + }) +}) diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/selectItem.test.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/selectItem.test.ts new file mode 100644 index 0000000000..93bbadd676 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/selectItem.test.ts @@ -0,0 +1,96 @@ +import { Subscription } from '../../../lib/browser-destinations' +import { Analytics, Context } from '@segment/analytics-next' +import googleAnalytics4Web, { destination } from '../index' +import { GA } from '../types' + +const subscriptions: Subscription[] = [ + { + partnerAction: 'selectItem', + name: 'Select Item', + enabled: true, + subscribe: 'type = "track"', + mapping: { + item_list_id: { + '@path': '$.properties.item_list_id' + }, + item_list_name: { + '@path': '$.properties.item_list_name' + }, + items: [ + { + item_name: { + '@path': `$.properties.products.0.name` + }, + item_id: { + '@path': `$.properties.products.0.product_id` + }, + currency: { + '@path': `$.properties.products.0.currency` + }, + price: { + '@path': `$.properties.products.0.price` + }, + quantity: { + '@path': `$.properties.products.0.quantity` + } + } + ] + } + } +] + +describe('GoogleAnalytics4Web.selectItem', () => { + const settings = { + measurementID: 'test123' + } + + let mockGA4: GA + let selectItemEvent: any + beforeEach(async () => { + jest.restoreAllMocks() + + const [trackEventPlugin] = await googleAnalytics4Web({ + ...settings, + subscriptions + }) + selectItemEvent = trackEventPlugin + + jest.spyOn(destination, 'initialize').mockImplementation(() => { + mockGA4 = { + gtag: jest.fn() + } + return Promise.resolve(mockGA4.gtag) + }) + await trackEventPlugin.load(Context.system(), {} as Analytics) + }) + + test('GA4 selectItem Event', async () => { + const context = new Context({ + event: 'Select Item', + type: 'track', + properties: { + item_list_id: 12321, + item_list_name: 'Monopoly: 3rd Edition', + products: [ + { + product_id: '12345', + name: 'Monopoly: 3rd Edition', + currency: 'USD' + } + ] + } + }) + + await selectItemEvent.track?.(context) + + expect(mockGA4.gtag).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' }] + }) + ) + }) +}) diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/selectPromotion.test.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/selectPromotion.test.ts new file mode 100644 index 0000000000..1b4abc0456 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/selectPromotion.test.ts @@ -0,0 +1,111 @@ +import { Subscription } from '../../../lib/browser-destinations' +import { Analytics, Context } from '@segment/analytics-next' +import googleAnalytics4Web, { destination } from '../index' +import { GA } from '../types' + +const subscriptions: Subscription[] = [ + { + partnerAction: 'selectPromotion', + name: 'Select Promotion', + enabled: true, + subscribe: 'type = "track"', + mapping: { + creative_name: { + '@path': '$.properties.creative_name' + }, + creative_slot: { + '@path': '$.properties.creative_slot' + }, + location_id: { + '@path': '$.properties.location_id' + }, + promotion_id: { + '@path': '$.properties.promotion_id' + }, + promotion_name: { + '@path': '$.properties.promotion_name' + }, + items: [ + { + item_name: { + '@path': `$.properties.products.0.name` + }, + item_id: { + '@path': `$.properties.products.0.product_id` + }, + currency: { + '@path': `$.properties.products.0.currency` + }, + price: { + '@path': `$.properties.products.0.price` + }, + quantity: { + '@path': `$.properties.products.0.quantity` + } + } + ] + } + } +] + +describe('GoogleAnalytics4Web.selectPromotion', () => { + const settings = { + measurementID: 'test123' + } + + let mockGA4: GA + let selectPromotionEvent: any + beforeEach(async () => { + jest.restoreAllMocks() + + const [trackEventPlugin] = await googleAnalytics4Web({ + ...settings, + subscriptions + }) + selectPromotionEvent = trackEventPlugin + + jest.spyOn(destination, 'initialize').mockImplementation(() => { + mockGA4 = { + gtag: jest.fn() + } + return Promise.resolve(mockGA4.gtag) + }) + await trackEventPlugin.load(Context.system(), {} as Analytics) + }) + + test('GA4 selectPromotion Event', 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', + products: [ + { + product_id: '12345', + name: 'Monopoly: 3rd Edition', + currency: 'USD' + } + ] + } + }) + + await selectPromotionEvent.track?.(context) + + expect(mockGA4.gtag).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' }] + }) + ) + }) +}) diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/signUp.test.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/signUp.test.ts new file mode 100644 index 0000000000..341043713f --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/signUp.test.ts @@ -0,0 +1,62 @@ +import { Subscription } from '../../../lib/browser-destinations' +import { Analytics, Context } from '@segment/analytics-next' +import googleAnalytics4Web, { destination } from '../index' +import { GA } from '../types' + +const subscriptions: Subscription[] = [ + { + partnerAction: 'signUp', + name: 'signUp', + enabled: true, + subscribe: 'type = "track"', + mapping: { + method: { + '@path': '$.properties.method' + } + } + } +] + +describe('GoogleAnalytics4Web.signUp', () => { + const settings = { + measurementID: 'test123' + } + + let mockGA4: GA + let signUpEvent: any + beforeEach(async () => { + jest.restoreAllMocks() + + const [trackEventPlugin] = await googleAnalytics4Web({ + ...settings, + subscriptions + }) + signUpEvent = trackEventPlugin + + jest.spyOn(destination, 'initialize').mockImplementation(() => { + mockGA4 = { + gtag: jest.fn() + } + return Promise.resolve(mockGA4.gtag) + }) + await trackEventPlugin.load(Context.system(), {} as Analytics) + }) + + test('GA4 signUp Event', async () => { + const context = new Context({ + event: 'signUp', + type: 'track', + properties: { + method: 'Google' + } + }) + + await signUpEvent.track?.(context) + + expect(mockGA4.gtag).toHaveBeenCalledWith( + expect.anything(), + expect.stringContaining('sign_up'), + expect.objectContaining({ method: 'Google' }) + ) + }) +}) diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/viewCart.test.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/viewCart.test.ts new file mode 100644 index 0000000000..e3d245338c --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/viewCart.test.ts @@ -0,0 +1,96 @@ +import { Subscription } from '../../../lib/browser-destinations' +import { Analytics, Context } from '@segment/analytics-next' +import googleAnalytics4Web, { destination } from '../index' +import { GA } from '../types' + +const subscriptions: Subscription[] = [ + { + partnerAction: 'viewCart', + name: 'View Cart', + enabled: true, + subscribe: 'type = "track"', + mapping: { + currency: { + '@path': '$.properties.currency' + }, + value: { + '@path': '$.properties.value' + }, + items: [ + { + item_name: { + '@path': `$.properties.products.0.name` + }, + item_id: { + '@path': `$.properties.products.0.product_id` + }, + currency: { + '@path': `$.properties.products.0.currency` + }, + price: { + '@path': `$.properties.products.0.price` + }, + quantity: { + '@path': `$.properties.products.0.quantity` + } + } + ] + } + } +] + +describe('GoogleAnalytics4Web.viewCart', () => { + const settings = { + measurementID: 'test123' + } + + let mockGA4: GA + let viewCartEvent: any + beforeEach(async () => { + jest.restoreAllMocks() + + const [trackEventPlugin] = await googleAnalytics4Web({ + ...settings, + subscriptions + }) + viewCartEvent = trackEventPlugin + + jest.spyOn(destination, 'initialize').mockImplementation(() => { + mockGA4 = { + gtag: jest.fn() + } + return Promise.resolve(mockGA4.gtag) + }) + await trackEventPlugin.load(Context.system(), {} as Analytics) + }) + + test('GA4 viewCart Event', 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.gtag).toHaveBeenCalledWith( + expect.anything(), + expect.stringContaining('view_cart'), + expect.objectContaining({ + currency: 'USD', + items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }], + value: 10 + }) + ) + }) +}) diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/viewItem.test.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/viewItem.test.ts new file mode 100644 index 0000000000..ce6aea21dc --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/viewItem.test.ts @@ -0,0 +1,96 @@ +import { Subscription } from '../../../lib/browser-destinations' +import { Analytics, Context } from '@segment/analytics-next' +import googleAnalytics4Web, { destination } from '../index' +import { GA } from '../types' + +const subscriptions: Subscription[] = [ + { + partnerAction: 'viewItem', + name: 'View Item', + enabled: true, + subscribe: 'type = "track"', + mapping: { + currency: { + '@path': '$.properties.currency' + }, + value: { + '@path': '$.properties.value' + }, + items: [ + { + item_name: { + '@path': `$.properties.products.0.name` + }, + item_id: { + '@path': `$.properties.products.0.product_id` + }, + currency: { + '@path': `$.properties.products.0.currency` + }, + price: { + '@path': `$.properties.products.0.price` + }, + quantity: { + '@path': `$.properties.products.0.quantity` + } + } + ] + } + } +] + +describe('GoogleAnalytics4Web.viewItem', () => { + const settings = { + measurementID: 'test123' + } + + let mockGA4: GA + let viewItemEvent: any + beforeEach(async () => { + jest.restoreAllMocks() + + const [trackEventPlugin] = await googleAnalytics4Web({ + ...settings, + subscriptions + }) + viewItemEvent = trackEventPlugin + + jest.spyOn(destination, 'initialize').mockImplementation(() => { + mockGA4 = { + gtag: jest.fn() + } + return Promise.resolve(mockGA4.gtag) + }) + await trackEventPlugin.load(Context.system(), {} as Analytics) + }) + + test('GA4 viewItem Event', async () => { + const context = new Context({ + event: 'View Item', + type: 'track', + properties: { + currency: 'USD', + value: 10, + products: [ + { + product_id: '12345', + name: 'Monopoly: 3rd Edition', + currency: 'USD' + } + ] + } + }) + + await viewItemEvent.track?.(context) + + expect(mockGA4.gtag).toHaveBeenCalledWith( + expect.anything(), + expect.stringContaining('view_item'), + expect.objectContaining({ + currency: 'USD', + items: [{ currency: 'USD', item_id: '12345', item_name: 'Monopoly: 3rd Edition' }], + value: 10 + }) + ) + }) +}) diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/viewItemList.test.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/viewItemList.test.ts new file mode 100644 index 0000000000..2d9519585b --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/viewItemList.test.ts @@ -0,0 +1,96 @@ +import { Subscription } from '../../../lib/browser-destinations' +import { Analytics, Context } from '@segment/analytics-next' +import googleAnalytics4Web, { destination } from '../index' +import { GA } from '../types' + +const subscriptions: Subscription[] = [ + { + partnerAction: 'viewItemList', + name: 'View Item List', + enabled: true, + subscribe: 'type = "track"', + mapping: { + item_list_id: { + '@path': '$.properties.item_list_id' + }, + item_list_name: { + '@path': '$.properties.item_list_name' + }, + items: [ + { + item_name: { + '@path': `$.properties.products.0.name` + }, + item_id: { + '@path': `$.properties.products.0.product_id` + }, + currency: { + '@path': `$.properties.products.0.currency` + }, + price: { + '@path': `$.properties.products.0.price` + }, + quantity: { + '@path': `$.properties.products.0.quantity` + } + } + ] + } + } +] + +describe('GoogleAnalytics4Web.viewItemList', () => { + const settings = { + measurementID: 'test123' + } + + let mockGA4: GA + let viewItemListEvent: any + beforeEach(async () => { + jest.restoreAllMocks() + + const [trackEventPlugin] = await googleAnalytics4Web({ + ...settings, + subscriptions + }) + viewItemListEvent = trackEventPlugin + + jest.spyOn(destination, 'initialize').mockImplementation(() => { + mockGA4 = { + gtag: jest.fn() + } + return Promise.resolve(mockGA4.gtag) + }) + await trackEventPlugin.load(Context.system(), {} as Analytics) + }) + + test('GA4 viewItemList Event', async () => { + const context = new Context({ + event: 'View Item List', + type: 'track', + properties: { + item_list_id: 12321, + item_list_name: 'Monopoly: 3rd Edition', + products: [ + { + product_id: '12345', + name: 'Monopoly: 3rd Edition', + currency: 'USD' + } + ] + } + }) + + await viewItemListEvent.track?.(context) + + expect(mockGA4.gtag).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' }] + }) + ) + }) +}) diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/viewPromotion.test.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/viewPromotion.test.ts new file mode 100644 index 0000000000..7dbcbc185e --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/__tests__/viewPromotion.test.ts @@ -0,0 +1,111 @@ +import { Subscription } from '../../../lib/browser-destinations' +import { Analytics, Context } from '@segment/analytics-next' +import googleAnalytics4Web, { destination } from '../index' +import { GA } from '../types' + +const subscriptions: Subscription[] = [ + { + partnerAction: 'viewPromotion', + name: 'Select Promotion', + enabled: true, + subscribe: 'type = "track"', + mapping: { + creative_name: { + '@path': '$.properties.creative_name' + }, + creative_slot: { + '@path': '$.properties.creative_slot' + }, + location_id: { + '@path': '$.properties.location_id' + }, + promotion_id: { + '@path': '$.properties.promotion_id' + }, + promotion_name: { + '@path': '$.properties.promotion_name' + }, + items: [ + { + item_name: { + '@path': `$.properties.products.0.name` + }, + item_id: { + '@path': `$.properties.products.0.product_id` + }, + currency: { + '@path': `$.properties.products.0.currency` + }, + price: { + '@path': `$.properties.products.0.price` + }, + quantity: { + '@path': `$.properties.products.0.quantity` + } + } + ] + } + } +] + +describe('GoogleAnalytics4Web.viewPromotion', () => { + const settings = { + measurementID: 'test123' + } + + let mockGA4: GA + let viewPromotionEvent: any + beforeEach(async () => { + jest.restoreAllMocks() + + const [trackEventPlugin] = await googleAnalytics4Web({ + ...settings, + subscriptions + }) + viewPromotionEvent = trackEventPlugin + + jest.spyOn(destination, 'initialize').mockImplementation(() => { + mockGA4 = { + gtag: jest.fn() + } + return Promise.resolve(mockGA4.gtag) + }) + await trackEventPlugin.load(Context.system(), {} as Analytics) + }) + + test('GA4 viewPromotion Event', 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', + products: [ + { + product_id: '12345', + name: 'Monopoly: 3rd Edition', + currency: 'USD' + } + ] + } + }) + + await viewPromotionEvent.track?.(context) + + expect(mockGA4.gtag).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' }] + }) + ) + }) +}) diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/addPaymentInfo/generated-types.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/addPaymentInfo/generated-types.ts new file mode 100644 index 0000000000..8babfe4438 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/addPaymentInfo/generated-types.ts @@ -0,0 +1,117 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * A unique identifier for a user. See Google's [User-ID for cross-platform analysis](https://support.google.com/analytics/answer/9213390) and [Reporting: deduplicate user counts](https://support.google.com/analytics/answer/9355949?hl=en) documentation for more information on this identifier. + */ + user_id?: string + /** + * Currency of the items associated with the event, in 3-letter ISO 4217 format. + */ + currency?: string + /** + * The monetary value of the event. + */ + value?: number + /** + * Coupon code used for a purchase. + */ + coupon?: string + /** + * The chosen method of payment. + */ + payment_type?: string + /** + * The list of products purchased. + */ + items: { + /** + * Identifier for the product being purchased. + */ + item_id?: string + /** + * Name of the product being purchased. + */ + item_name?: string + /** + * A product affiliation to designate a supplying company or brick and mortar store location. + */ + affiliation?: string + /** + * Coupon code used for a purchase. + */ + coupon?: string + /** + * Currency of the purchase or items associated with the event, in 3-letter ISO 4217 format. + */ + currency?: string + /** + * Monetary value of discount associated with a purchase. + */ + discount?: number + /** + * The index/position of the item in a list. + */ + index?: number + /** + * Brand associated with the product. + */ + item_brand?: string + /** + * Product category. + */ + item_category?: string + /** + * Product category 2. + */ + item_category2?: string + /** + * Product category 3. + */ + item_category3?: string + /** + * Product category 4. + */ + item_category4?: string + /** + * Product category 5. + */ + item_category5?: string + /** + * The ID of the list in which the item was presented to the user. + */ + item_list_id?: string + /** + * The name of the list in which the item was presented to the user. + */ + item_list_name?: string + /** + * Variant of the product (e.g. Black). + */ + item_variant?: string + /** + * The location associated with the item. + */ + location_id?: string + /** + * Price of the product being purchased, in units of the specified currency parameter. + */ + price?: number + /** + * Item quantity. + */ + quantity?: number + }[] + /** + * 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. + */ + user_properties?: { + [k: string]: unknown + } + /** + * The event parameters to send to Google Analytics 4. + */ + params?: { + [k: string]: unknown + } +} diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/addPaymentInfo/index.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/addPaymentInfo/index.ts new file mode 100644 index 0000000000..7b7002f20e --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/addPaymentInfo/index.ts @@ -0,0 +1,48 @@ +import type { BrowserActionDefinition } from '../../../lib/browser-destinations' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' +import { updateUser } from '../ga4-functions' +import { + user_id, + user_properties, + currency, + value, + coupon, + payment_type, + items_multi_products, + params +} from '../ga4-properties' + +// Change from unknown to the partner SDK types +const action: BrowserActionDefinition = { + title: 'Add Payment Info', + description: 'Send event when a user submits their payment information', + defaultSubscription: 'type = "track" and event = "Payment Info Entered"', + platform: 'web', + fields: { + user_id: { ...user_id }, + currency: { ...currency }, + value: { ...value }, + coupon: { ...coupon }, + payment_type: { ...payment_type }, + items: { + ...items_multi_products, + required: true + }, + user_properties: user_properties, + params: params + }, + perform: (gtag, { payload }) => { + updateUser(payload.user_id, payload.user_properties, gtag) + gtag('event', 'add_payment_info', { + currency: payload.currency, + value: payload.value, + coupon: payload.coupon, + payment_type: payload.payment_type, + items: payload.items, + ...payload.params + }) + } +} + +export default action diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/addToCart/generated-types.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/addToCart/generated-types.ts new file mode 100644 index 0000000000..6e0f5789e3 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/addToCart/generated-types.ts @@ -0,0 +1,109 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * A unique identifier for a user. See Google's [User-ID for cross-platform analysis](https://support.google.com/analytics/answer/9213390) and [Reporting: deduplicate user counts](https://support.google.com/analytics/answer/9355949?hl=en) documentation for more information on this identifier. + */ + user_id?: string + /** + * Currency of the items associated with the event, in 3-letter ISO 4217 format. + */ + currency?: string + /** + * The list of products purchased. + */ + items: { + /** + * Identifier for the product being purchased. + */ + item_id?: string + /** + * Name of the product being purchased. + */ + item_name?: string + /** + * A product affiliation to designate a supplying company or brick and mortar store location. + */ + affiliation?: string + /** + * Coupon code used for a purchase. + */ + coupon?: string + /** + * Currency of the purchase or items associated with the event, in 3-letter ISO 4217 format. + */ + currency?: string + /** + * Monetary value of discount associated with a purchase. + */ + discount?: number + /** + * The index/position of the item in a list. + */ + index?: number + /** + * Brand associated with the product. + */ + item_brand?: string + /** + * Product category. + */ + item_category?: string + /** + * Product category 2. + */ + item_category2?: string + /** + * Product category 3. + */ + item_category3?: string + /** + * Product category 4. + */ + item_category4?: string + /** + * Product category 5. + */ + item_category5?: string + /** + * The ID of the list in which the item was presented to the user. + */ + item_list_id?: string + /** + * The name of the list in which the item was presented to the user. + */ + item_list_name?: string + /** + * Variant of the product (e.g. Black). + */ + item_variant?: string + /** + * The location associated with the item. + */ + location_id?: string + /** + * Price of the product being purchased, in units of the specified currency parameter. + */ + price?: number + /** + * Item quantity. + */ + quantity?: number + }[] + /** + * The monetary value of the event. + */ + value?: number + /** + * 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. + */ + user_properties?: { + [k: string]: unknown + } + /** + * The event parameters to send to Google Analytics 4. + */ + params?: { + [k: string]: unknown + } +} diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/addToCart/index.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/addToCart/index.ts new file mode 100644 index 0000000000..790fdbfc3d --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/addToCart/index.ts @@ -0,0 +1,36 @@ +import type { BrowserActionDefinition } from '../../../lib/browser-destinations' +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' + +const action: BrowserActionDefinition = { + title: 'Add to Cart', + description: 'This event signifies that an item was added to a cart for purchase.', + defaultSubscription: 'type = "track" and event = "Product Added"', + platform: 'web', + fields: { + user_id: user_id, + currency: currency, + items: { + ...items_single_products, + required: true + }, + value: value, + user_properties: user_properties, + params: params + }, + perform: (gtag, { payload }) => { + updateUser(payload.user_id, payload.user_properties, gtag) + + gtag('event', 'add_to_cart', { + currency: payload.currency, + value: payload.value, + items: payload.items, + ...payload.params + }) + } +} + +export default action diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/addToWishlist/generated-types.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/addToWishlist/generated-types.ts new file mode 100644 index 0000000000..a7f5a2e20e --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/addToWishlist/generated-types.ts @@ -0,0 +1,109 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * A unique identifier for a user. See Google's [User-ID for cross-platform analysis](https://support.google.com/analytics/answer/9213390) and [Reporting: deduplicate user counts](https://support.google.com/analytics/answer/9355949?hl=en) documentation for more information on this identifier. + */ + user_id?: string + /** + * Currency of the items associated with the event, in 3-letter ISO 4217 format. + */ + currency?: string + /** + * The monetary value of the event. + */ + value?: number + /** + * The list of products purchased. + */ + items: { + /** + * Identifier for the product being purchased. + */ + item_id?: string + /** + * Name of the product being purchased. + */ + item_name?: string + /** + * A product affiliation to designate a supplying company or brick and mortar store location. + */ + affiliation?: string + /** + * Coupon code used for a purchase. + */ + coupon?: string + /** + * Currency of the purchase or items associated with the event, in 3-letter ISO 4217 format. + */ + currency?: string + /** + * Monetary value of discount associated with a purchase. + */ + discount?: number + /** + * The index/position of the item in a list. + */ + index?: number + /** + * Brand associated with the product. + */ + item_brand?: string + /** + * Product category. + */ + item_category?: string + /** + * Product category 2. + */ + item_category2?: string + /** + * Product category 3. + */ + item_category3?: string + /** + * Product category 4. + */ + item_category4?: string + /** + * Product category 5. + */ + item_category5?: string + /** + * The ID of the list in which the item was presented to the user. + */ + item_list_id?: string + /** + * The name of the list in which the item was presented to the user. + */ + item_list_name?: string + /** + * Variant of the product (e.g. Black). + */ + item_variant?: string + /** + * The location associated with the item. + */ + location_id?: string + /** + * Price of the product being purchased, in units of the specified currency parameter. + */ + price?: number + /** + * Item quantity. + */ + quantity?: number + }[] + /** + * 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. + */ + user_properties?: { + [k: string]: unknown + } + /** + * The event parameters to send to Google Analytics 4. + */ + params?: { + [k: string]: unknown + } +} diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/addToWishlist/index.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/addToWishlist/index.ts new file mode 100644 index 0000000000..c94378ee67 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/addToWishlist/index.ts @@ -0,0 +1,38 @@ +import type { BrowserActionDefinition } from '../../../lib/browser-destinations' +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' + +// Change from unknown to the partner SDK types +const action: BrowserActionDefinition = { + title: 'Add to Wishlist', + description: + 'The event signifies that an item was added to a wishlist. Use this event to identify popular gift items in your app.', + platform: 'web', + defaultSubscription: 'type = "track" and event = "Product Added to Wishlist"', + fields: { + user_id: user_id, + currency: currency, + value: value, + items: { + ...items_single_products, + required: true + }, + user_properties: user_properties, + params: params + }, + perform: (gtag, { payload }) => { + updateUser(payload.user_id, payload.user_properties, gtag) + + gtag('event', 'add_to_wishlist', { + currency: payload.currency, + value: payload.value, + items: payload.items, + ...payload.params + }) + } +} + +export default action diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/beginCheckout/generated-types.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/beginCheckout/generated-types.ts new file mode 100644 index 0000000000..284bf69d75 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/beginCheckout/generated-types.ts @@ -0,0 +1,113 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * A unique identifier for a user. See Google's [User-ID for cross-platform analysis](https://support.google.com/analytics/answer/9213390) and [Reporting: deduplicate user counts](https://support.google.com/analytics/answer/9355949?hl=en) documentation for more information on this identifier. + */ + user_id?: string + /** + * Coupon code used for a purchase. + */ + coupon?: string + /** + * Currency of the items associated with the event, in 3-letter ISO 4217 format. + */ + currency?: string + /** + * The list of products purchased. + */ + items: { + /** + * Identifier for the product being purchased. + */ + item_id?: string + /** + * Name of the product being purchased. + */ + item_name?: string + /** + * A product affiliation to designate a supplying company or brick and mortar store location. + */ + affiliation?: string + /** + * Coupon code used for a purchase. + */ + coupon?: string + /** + * Currency of the purchase or items associated with the event, in 3-letter ISO 4217 format. + */ + currency?: string + /** + * Monetary value of discount associated with a purchase. + */ + discount?: number + /** + * The index/position of the item in a list. + */ + index?: number + /** + * Brand associated with the product. + */ + item_brand?: string + /** + * Product category. + */ + item_category?: string + /** + * Product category 2. + */ + item_category2?: string + /** + * Product category 3. + */ + item_category3?: string + /** + * Product category 4. + */ + item_category4?: string + /** + * Product category 5. + */ + item_category5?: string + /** + * The ID of the list in which the item was presented to the user. + */ + item_list_id?: string + /** + * The name of the list in which the item was presented to the user. + */ + item_list_name?: string + /** + * Variant of the product (e.g. Black). + */ + item_variant?: string + /** + * The location associated with the item. + */ + location_id?: string + /** + * Price of the product being purchased, in units of the specified currency parameter. + */ + price?: number + /** + * Item quantity. + */ + quantity?: number + }[] + /** + * The monetary value of the event. + */ + value?: number + /** + * The event parameters to send to Google Analytics 4. + */ + params?: { + [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. + */ + user_properties?: { + [k: string]: unknown + } +} diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/beginCheckout/index.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/beginCheckout/index.ts new file mode 100644 index 0000000000..45ea69acfd --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/beginCheckout/index.ts @@ -0,0 +1,38 @@ +import type { BrowserActionDefinition } from '../../../lib/browser-destinations' +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' + +const action: BrowserActionDefinition = { + title: 'Begin Checkout', + description: 'This event signifies that a user has begun a checkout.', + defaultSubscription: 'type = "track" and event = "Checkout Started"', + platform: 'web', + fields: { + user_id: user_id, + coupon: { ...coupon, default: { '@path': '$.properties.coupon' } }, + currency: currency, + items: { + ...items_multi_products, + required: true + }, + value: value, + params: params, + user_properties: user_properties + }, + perform: (gtag, { payload }) => { + updateUser(payload.user_id, payload.user_properties, gtag) + + gtag('event', 'begin_checkout', { + currency: payload.currency, + value: payload.value, + coupon: payload.coupon, + items: payload.items, + ...payload.params + }) + } +} + +export default action diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/customEvent/generated-types.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/customEvent/generated-types.ts new file mode 100644 index 0000000000..3aa8ae426e --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/customEvent/generated-types.ts @@ -0,0 +1,28 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * The unique name of the custom event created in GA4. GA4 does not accept spaces in event names so Segment will replace any spaces with underscores. More information about GA4 event name rules is available in [their docs](https://support.google.com/analytics/answer/10085872?hl=en&ref_topic=9756175#event-name-rules&zippy=%2Cin-this-article.%2Cin-this-article). + */ + name: string + /** + * If true, the event name will be converted to lowercase before sending to Google. Event names are case sensitive in GA4 so enable this setting to avoid distinct events for casing differences. More information about GA4 event name rules is available in [their docs](https://support.google.com/analytics/answer/10085872?hl=en&ref_topic=9756175#event-name-rules&zippy=%2Cin-this-article.%2Cin-this-article). + */ + lowercase?: boolean + /** + * A unique identifier for a user. See Google's [User-ID for cross-platform analysis](https://support.google.com/analytics/answer/9213390) and [Reporting: deduplicate user counts](https://support.google.com/analytics/answer/9355949?hl=en) documentation for more information on this identifier. + */ + user_id?: string + /** + * 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. + */ + user_properties?: { + [k: string]: unknown + } + /** + * The event parameters to send to Google Analytics 4. + */ + params?: { + [k: string]: unknown + } +} diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/customEvent/index.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/customEvent/index.ts new file mode 100644 index 0000000000..4d0455318a --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/customEvent/index.ts @@ -0,0 +1,52 @@ +import type { BrowserActionDefinition } from '../../../lib/browser-destinations' +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' + +const normalizeEventName = (name: string, lowercase: boolean | undefined): string => { + name = name.trim() + name = name.replace(/\s/g, '_') + + if (lowercase) { + name = name.toLowerCase() + } + return name +} + +const action: BrowserActionDefinition = { + title: 'Custom Event', + description: 'Send any custom event', + defaultSubscription: 'type = "track"', + platform: 'web', + fields: { + name: { + label: 'Event Name', + description: + 'The unique name of the custom event created in GA4. GA4 does not accept spaces in event names so Segment will replace any spaces with underscores. More information about GA4 event name rules is available in [their docs](https://support.google.com/analytics/answer/10085872?hl=en&ref_topic=9756175#event-name-rules&zippy=%2Cin-this-article.%2Cin-this-article).', + type: 'string', + required: true, + default: { + '@path': '$.event' + } + }, + lowercase: { + label: 'Lowercase Event Name', + description: + 'If true, the event name will be converted to lowercase before sending to Google. Event names are case sensitive in GA4 so enable this setting to avoid distinct events for casing differences. More information about GA4 event name rules is available in [their docs](https://support.google.com/analytics/answer/10085872?hl=en&ref_topic=9756175#event-name-rules&zippy=%2Cin-this-article.%2Cin-this-article).', + type: 'boolean', + default: false + }, + user_id: { ...user_id }, + user_properties: user_properties, + params: params + }, + perform: (gtag, { payload }) => { + updateUser(payload.user_id, payload.user_properties, gtag) + const event_name = normalizeEventName(payload.name, payload.lowercase) + + gtag('event', event_name, payload.params) + } +} + +export default action diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/ga4-functions.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/ga4-functions.ts new file mode 100644 index 0000000000..0525e99038 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/ga4-functions.ts @@ -0,0 +1,8 @@ +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/src/destinations/google-analytics-4-web/ga4-properties.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/ga4-properties.ts new file mode 100644 index 0000000000..3e1128a993 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/ga4-properties.ts @@ -0,0 +1,368 @@ +import { InputField } from '@segment/actions-core/src/destination-kit/types' + +export const formatUserProperties = (userProperties: object | undefined): object | undefined => { + if (!userProperties) { + return undefined + } + + let properties = {} + + Object.entries(userProperties).forEach(([key, value]) => { + properties = { ...properties, ...{ [key]: { value: value } } } + }) + + return { user_properties: properties } +} + +export const user_properties: InputField = { + label: 'User Properties', + description: + '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. ', + type: 'object', + additionalProperties: true, + defaultObjectUI: 'keyvalue' +} + +export const params: InputField = { + label: 'Event Parameters', + description: 'The event parameters to send to Google Analytics 4.', + type: 'object', + additionalProperties: true, + defaultObjectUI: 'keyvalue' +} +export const user_id: InputField = { + label: 'User ID', + type: 'string', + description: + "A unique identifier for a user. See Google's [User-ID for cross-platform analysis](https://support.google.com/analytics/answer/9213390) and [Reporting: deduplicate user counts](https://support.google.com/analytics/answer/9355949?hl=en) documentation for more information on this identifier." +} + +export const promotion_id: InputField = { + label: 'Promotion ID', + type: 'string', + description: 'The ID of the promotion associated with the event.' +} + +export const promotion_name: InputField = { + label: 'Promotion Name', + type: 'string', + description: 'The name of the promotion associated with the event.' +} + +export const creative_slot: InputField = { + label: 'Creative Slot', + type: 'string', + description: 'The name of the promotional creative slot associated with the event.' +} + +export const creative_name: InputField = { + label: 'Creative Name', + type: 'string', + description: 'The name of the promotional creative.' +} + +export const tax: InputField = { + label: 'Tax', + type: 'number', + description: 'Total tax associated with the transaction.', + default: { + '@path': '$.properties.tax' + } +} + +export const shipping: InputField = { + label: 'Shipping', + type: 'number', + description: 'Shipping cost associated with the transaction.', + default: { + '@path': '$.properties.shipping' + } +} + +export const transaction_id: InputField = { + label: 'Order Id', + type: 'string', + description: 'The unique identifier of a transaction.', + default: { + '@path': '$.properties.order_id' + } +} + +export const affiliation: InputField = { + label: 'Affiliation', + type: 'string', + description: 'Store or affiliation from which this transaction occurred (e.g. Google Store).', + default: { + '@path': '$.properties.affiliation' + } +} + +export const client_id: InputField = { + label: 'Client ID', + description: 'Uniquely identifies a user instance of a web client.', + type: 'string', + required: true, + default: { + '@if': { + exists: { '@path': '$.userId' }, + then: { '@path': '$.userId' }, + else: { '@path': '$.anonymousId' } + } + } +} + +export const currency: InputField = { + label: 'Currency', + type: 'string', + description: 'Currency of the items associated with the event, in 3-letter ISO 4217 format.', + default: { '@path': '$.properties.currency' } +} + +export const value: InputField = { + label: 'Value', + type: 'number', + description: 'The monetary value of the event.', + default: { + '@path': '$.properties.value' + } +} + +export const coupon: InputField = { + label: 'Coupon', + type: 'string', + description: 'Coupon code used for a purchase.' +} + +export const payment_type: InputField = { + label: 'Payment Type', + type: 'string', + description: 'The chosen method of payment.', + default: { + '@path': '$.properties.payment_method' + } +} + +export const method: InputField = { + label: 'Method', + type: 'string', + description: 'The method used to login.', + default: { + '@path': '$.properties.method' + } +} + +export const search_term: InputField = { + label: 'Search Term', + type: 'string', + description: 'The term that was searched for.', + default: { + '@path': `$.properties.query` + } +} + +export const item_list_name: InputField = { + label: 'Item List Name', + description: 'The name of the list in which the item was presented to the user.', + type: 'string', + default: { + '@path': `$.properties.item_list_name` + } +} +export const item_list_id: InputField = { + label: 'Item List Id', + description: 'The ID of the list in which the item was presented to the user.', + type: 'string', + default: { + '@path': `$.properties.item_list_id` + } +} +export const location_id: InputField = { + label: 'Location ID', + type: 'string', + description: 'The ID of the location.', + default: { + '@path': '$.properties.position' + } +} + +export const minimal_items: InputField = { + label: 'Products', + description: 'The list of products purchased.', + type: 'object', + multiple: true, + properties: { + item_id: { + label: 'Product ID', + type: 'string', + description: 'Identifier for the product being purchased.' + }, + item_name: { + label: 'Name', + type: 'string', + description: 'Name of the product being purchased.' + }, + affiliation: { + label: 'Affiliation', + type: 'string', + description: 'A product affiliation to designate a supplying company or brick and mortar store location.' + }, + coupon: { + label: 'Coupon', + type: 'string', + description: 'Coupon code used for a purchase.' + }, + currency: { + label: 'Currency', + type: 'string', + description: 'Currency of the purchase or items associated with the event, in 3-letter ISO 4217 format.' + }, + discount: { + label: 'Discount', + type: 'number', + description: 'Monetary value of discount associated with a purchase.' + }, + index: { + label: 'Index', + type: 'number', + description: 'The index/position of the item in a list.' + }, + item_brand: { + label: 'Brand', + type: 'string', + description: 'Brand associated with the product.' + }, + item_category: { + label: 'Category', + type: 'string', + description: 'Product category.' + }, + item_category2: { + label: 'Category 2', + type: 'string', + description: 'Product category 2.' + }, + item_category3: { + label: 'Category 3', + type: 'string', + description: 'Product category 3.' + }, + item_category4: { + label: 'Category 4', + type: 'string', + description: 'Product category 4.' + }, + item_category5: { + label: 'Category 5', + type: 'string', + description: 'Product category 5.' + }, + item_list_id: { + label: 'Item List ID', + type: 'string', + description: 'The ID of the list in which the item was presented to the user.' + }, + item_list_name: { + label: 'Item List Name', + type: 'string', + description: 'The name of the list in which the item was presented to the user.' + }, + item_variant: { + label: 'Variant', + type: 'string', + description: 'Variant of the product (e.g. Black).' + }, + location_id: { + label: 'Location ID', + type: 'string', + description: 'The location associated with the item.' + }, + price: { + label: 'Price', + type: 'number', + description: 'Price of the product being purchased, in units of the specified currency parameter.' + }, + quantity: { + label: 'Quantity', + type: 'integer', + description: 'Item quantity.' + } + } +} + +export const items_single_products: InputField = { + ...minimal_items, + default: { + '@arrayPath': [ + '$.properties.products', + { + item_id: { + '@path': '$.product_id' + }, + item_name: { + '@path': '$.name' + }, + affiliation: { + '@path': '$.affiliation' + }, + coupon: { + '@path': '$.coupon' + }, + item_brand: { + '@path': '$.brand' + }, + item_category: { + '@path': '$.category' + }, + item_variant: { + '@path': '$.variant' + }, + price: { + '@path': '$.price' + }, + quantity: { + '@path': '$.quantity' + } + } + ] + } +} +export const items_multi_products: InputField = { + ...minimal_items, + default: { + '@arrayPath': [ + '$.properties.products', + { + item_id: { + '@path': '$.product_id' + }, + item_name: { + '@path': '$.name' + }, + affiliation: { + '@path': '$.affiliation' + }, + coupon: { + '@path': '$.coupon' + }, + index: { + '@path': '$.position' + }, + item_brand: { + '@path': '$.brand' + }, + item_category: { + '@path': '$.category' + }, + item_variant: { + '@path': '$.variant' + }, + price: { + '@path': '$.price' + }, + quantity: { + '@path': '$.quantity' + } + } + ] + } +} diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/ga4-types.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/ga4-types.ts new file mode 100644 index 0000000000..e6595abb74 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/ga4-types.ts @@ -0,0 +1,28 @@ +export interface ProductItem { + item_id?: string + item_name?: string + affiliation?: string + coupon?: string + currency?: string + discount?: number + index?: number + item_brand?: string + item_category?: string + item_category2?: string + item_category3?: string + item_category4?: string + item_category5?: string + item_list_id?: string + item_list_name?: string + item_variant?: string + location_id?: string + price?: number + quantity?: number +} + +export interface PromotionProductItem extends ProductItem { + creative_name?: string + creative_slot?: string + promotion_id?: string + promotion_name?: string +} diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/generateLead/generated-types.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/generateLead/generated-types.ts new file mode 100644 index 0000000000..2e7db72077 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/generateLead/generated-types.ts @@ -0,0 +1,28 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * A unique identifier for a user. See Google's [User-ID for cross-platform analysis](https://support.google.com/analytics/answer/9213390) and [Reporting: deduplicate user counts](https://support.google.com/analytics/answer/9355949?hl=en) documentation for more information on this identifier. + */ + user_id?: string + /** + * Currency of the items associated with the event, in 3-letter ISO 4217 format. + */ + currency?: string + /** + * The monetary value of the event. + */ + value?: number + /** + * 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. + */ + user_properties?: { + [k: string]: unknown + } + /** + * The event parameters to send to Google Analytics 4. + */ + params?: { + [k: string]: unknown + } +} diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/generateLead/index.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/generateLead/index.ts new file mode 100644 index 0000000000..1d4ef6e3a8 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/generateLead/index.ts @@ -0,0 +1,32 @@ +import type { BrowserActionDefinition } from '../../../lib/browser-destinations' +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' + +const action: BrowserActionDefinition = { + title: 'Generate Lead', + description: + 'Log this event when a lead has been generated to understand the efficacy of your re-engagement campaigns.', + platform: 'web', + defaultSubscription: 'type = "track"', + fields: { + user_id: user_id, + currency: currency, + value: value, + user_properties: user_properties, + params: params + }, + perform: (gtag, { payload }) => { + updateUser(payload.user_id, payload.user_properties, gtag) + + gtag('event', 'generate_lead', { + currency: payload.currency, + value: payload.value, + ...payload.params + }) + } +} + +export default action diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/generated-types.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/generated-types.ts new file mode 100644 index 0000000000..db2c9df709 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/generated-types.ts @@ -0,0 +1,60 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Settings { + /** + * The measurement ID associated with the web stream. Found in the Google Analytics UI under: Admin > Data Streams > Web > Measurement ID. + */ + measurementID: string + /** + * Set to false to prevent the default snippet from sending page views. Enabled by default. + */ + pageView?: boolean + /** + * Set to false to disable all advertising features. Set to true by default. + */ + allowGoogleSignals?: boolean + /** + * Set to false to disable all advertising features. Set to true by default. + */ + allowAdPersonalizationSignals?: boolean + /** + * Specifies the domain used to store the analytics cookie. Set to “auto” by default. + */ + cookieDomain?: string + /** + * Every time a hit is sent to GA4, the analytics cookie expiration time is updated to be the current time plus the value of this field. The default value is two years (63072000 seconds). Please input the expiration value in seconds. More information in [Google Documentation](https://developers.google.com/analytics/devguides/collection/ga4/reference/config#) + */ + cookieExpirationInSeconds?: number + /** + * 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. + */ + cookieFlags?: string[] + /** + * Specifies the subpath used to store the analytics cookie. + */ + cookiePath?: string[] + /** + * Specifies a prefix to prepend to the analytics cookie name. + */ + cookiePrefix?: string[] + /** + * Set to false to not update cookies on each page load. This has the effect of cookie expiration being relative to the first time a user visited. Set to true by default so update cookies on each page load. + */ + cookieUpdate?: 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 + /** + * 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 + /** + * 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. + */ + waitTimeToUpdateConsentStage?: number +} diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/index.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/index.ts new file mode 100644 index 0000000000..7c83981344 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/index.ts @@ -0,0 +1,200 @@ +import type { Settings } from './generated-types' +import type { BrowserDestinationDefinition } from '../../lib/browser-destinations' +import { browserDestination } from '../../runtime/shim' + +import addPaymentInfo from './addPaymentInfo' +import addToCart from './addToCart' +import addToWishlist from './addToWishlist' +import beginCheckout from './beginCheckout' +import customEvent from './customEvent' +import login from './login' +import generateLead from './generateLead' +import purchase from './purchase' +import refund from './refund' +import removeFromCart from './removeFromCart' +import search from './search' +import selectItem from './selectItem' +import selectPromotion from './selectPromotion' +import signUp from './signUp' +import viewCart from './viewCart' +import viewItem from './viewItem' +import viewItemList from './viewItemList' +import viewPromotion from './viewPromotion' +import setConfigurationFields from './setConfigurationFields' +import { defaultValues, DestinationDefinition } from '@segment/actions-core' + +declare global { + interface Window { + gtag: Function + dataLayer: any + } +} + +const presets: DestinationDefinition['presets'] = [ + { + name: `Set Configuration Fields`, + subscribe: 'type = "page" or type = "identify"', + partnerAction: 'setConfigurationFields', + mapping: defaultValues(setConfigurationFields.fields) + } +] + +export const destination: BrowserDestinationDefinition = { + name: 'Google Analytics 4 Web', + slug: 'actions-google-analytics-4-web', + mode: 'device', + + settings: { + measurementID: { + description: + 'The measurement ID associated with the web stream. Found in the Google Analytics UI under: Admin > Data Streams > Web > Measurement ID.', + label: 'Measurement ID', + type: 'string', + required: true + }, + pageView: { + description: 'Set to false to prevent the default snippet from sending page views. Enabled by default.', + label: 'Page Views', + type: 'boolean', + default: true + }, + allowGoogleSignals: { + description: 'Set to false to disable all advertising features. Set to true by default.', + label: 'Allow Google Signals', + type: 'boolean', + default: true + }, + allowAdPersonalizationSignals: { + description: 'Set to false to disable all advertising features. Set to true by default.', + label: 'Allow Ad Personalization Signals', + type: 'boolean', + default: true + }, + cookieDomain: { + description: 'Specifies the domain used to store the analytics cookie. Set to “auto” by default.', + label: 'Cookie Domain', + type: 'string', + default: 'auto' + }, + cookieExpirationInSeconds: { + description: `Every time a hit is sent to GA4, the analytics cookie expiration time is updated to be the current time plus the value of this field. The default value is two years (63072000 seconds). Please input the expiration value in seconds. More information in [Google Documentation](https://developers.google.com/analytics/devguides/collection/ga4/reference/config#)`, + label: 'Cookie Expiration In Seconds', + type: 'number', + default: 63072000 + }, + cookieFlags: { + 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', + multiple: true + }, + cookiePath: { + description: `Specifies the subpath used to store the analytics cookie.`, + label: 'Cookie Path', + type: 'string', + multiple: true + }, + cookiePrefix: { + description: `Specifies a prefix to prepend to the analytics cookie name.`, + label: 'Cookie Prefix', + type: 'string', + multiple: true + }, + cookieUpdate: { + description: `Set to false to not update cookies on each page load. This has the effect of cookie expiration being relative to the first time a user visited. Set to true by default so update cookies on each page load.`, + label: 'Cookie Update', + type: 'boolean', + 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 + }, + 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: 'granted' + }, + 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: 'granted' + }, + waitTimeToUpdateConsentStage: { + description: + 'If your CMP loads asynchronously, it might not always run before the Google tag. To handle such situations, specify a millisecond value to control how long to wait before the consent state update is sent. Please input the wait_for_update in milliseconds.', + label: 'Wait Time to Update Consent State', + type: 'number' + } + }, + + initialize: async ({ settings }, deps) => { + const config = { + send_page_view: settings.pageView, + 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 + } + + window.dataLayer = window.dataLayer || [] + window.gtag = function () { + // eslint-disable-next-line prefer-rest-params + window.dataLayer.push(arguments) + } + + window.gtag('js', new Date()) + window.gtag('config', settings.measurementID, config) + if (settings.enableConsentMode) { + window.gtag('consent', 'default', { + ad_storage: settings.defaultAdsStorageConsentState, + analytics_storage: settings.defaultAnalyticsStorageConsentState, + wait_for_update: settings.waitTimeToUpdateConsentStage + }) + } + const script = `https://www.googletagmanager.com/gtag/js?id=${settings.measurementID}` + await deps.loadScript(script) + return window.gtag + }, + presets, + actions: { + addPaymentInfo, + login, + signUp, + search, + addToCart, + addToWishlist, + removeFromCart, + selectItem, + selectPromotion, + viewItem, + viewPromotion, + beginCheckout, + purchase, + refund, + viewCart, + viewItemList, + generateLead, + customEvent, + setConfigurationFields + } +} + +export default browserDestination(destination) diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/login/generated-types.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/login/generated-types.ts new file mode 100644 index 0000000000..c305ce6764 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/login/generated-types.ts @@ -0,0 +1,24 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * A unique identifier for a user. See Google's [User-ID for cross-platform analysis](https://support.google.com/analytics/answer/9213390) and [Reporting: deduplicate user counts](https://support.google.com/analytics/answer/9355949?hl=en) documentation for more information on this identifier. + */ + user_id?: string + /** + * The method used to login. + */ + method?: string + /** + * 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. + */ + user_properties?: { + [k: string]: unknown + } + /** + * The event parameters to send to Google Analytics 4. + */ + params?: { + [k: string]: unknown + } +} diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/login/index.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/login/index.ts new file mode 100644 index 0000000000..c02be7ab95 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/login/index.ts @@ -0,0 +1,31 @@ +import type { BrowserActionDefinition } from '../../../lib/browser-destinations' +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' + +// Change from unknown to the partner SDK types +const action: BrowserActionDefinition = { + title: 'Login', + description: 'Send event when a user logs in', + platform: 'web', + defaultSubscription: 'type = "track" and event = "Signed In"', + fields: { + user_id: user_id, + method: method, + user_properties: user_properties, + params: params + }, + + perform: (gtag, { payload }) => { + updateUser(payload.user_id, payload.user_properties, gtag) + + gtag('event', 'login', { + method: payload.method, + ...payload.params + }) + } +} + +export default action diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/purchase/generated-types.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/purchase/generated-types.ts new file mode 100644 index 0000000000..498bba5c46 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/purchase/generated-types.ts @@ -0,0 +1,125 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * A unique identifier for a user. See Google's [User-ID for cross-platform analysis](https://support.google.com/analytics/answer/9213390) and [Reporting: deduplicate user counts](https://support.google.com/analytics/answer/9355949?hl=en) documentation for more information on this identifier. + */ + user_id?: string + /** + * Coupon code used for a purchase. + */ + coupon?: string + /** + * Currency of the items associated with the event, in 3-letter ISO 4217 format. + */ + currency: string + /** + * The list of products purchased. + */ + items: { + /** + * Identifier for the product being purchased. + */ + item_id?: string + /** + * Name of the product being purchased. + */ + item_name?: string + /** + * A product affiliation to designate a supplying company or brick and mortar store location. + */ + affiliation?: string + /** + * Coupon code used for a purchase. + */ + coupon?: string + /** + * Currency of the purchase or items associated with the event, in 3-letter ISO 4217 format. + */ + currency?: string + /** + * Monetary value of discount associated with a purchase. + */ + discount?: number + /** + * The index/position of the item in a list. + */ + index?: number + /** + * Brand associated with the product. + */ + item_brand?: string + /** + * Product category. + */ + item_category?: string + /** + * Product category 2. + */ + item_category2?: string + /** + * Product category 3. + */ + item_category3?: string + /** + * Product category 4. + */ + item_category4?: string + /** + * Product category 5. + */ + item_category5?: string + /** + * The ID of the list in which the item was presented to the user. + */ + item_list_id?: string + /** + * The name of the list in which the item was presented to the user. + */ + item_list_name?: string + /** + * Variant of the product (e.g. Black). + */ + item_variant?: string + /** + * The location associated with the item. + */ + location_id?: string + /** + * Price of the product being purchased, in units of the specified currency parameter. + */ + price?: number + /** + * Item quantity. + */ + quantity?: number + }[] + /** + * The unique identifier of a transaction. + */ + transaction_id: string + /** + * Shipping cost associated with the transaction. + */ + shipping?: number + /** + * Total tax associated with the transaction. + */ + tax?: number + /** + * The monetary value of the event. + */ + value?: number + /** + * 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. + */ + user_properties?: { + [k: string]: unknown + } + /** + * The event parameters to send to Google Analytics 4. + */ + params?: { + [k: string]: unknown + } +} diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/purchase/index.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/purchase/index.ts new file mode 100644 index 0000000000..3ebd36aba8 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/purchase/index.ts @@ -0,0 +1,54 @@ +import type { BrowserActionDefinition } from '../../../lib/browser-destinations' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' +import { + coupon, + currency, + transaction_id, + value, + user_id, + shipping, + tax, + items_multi_products, + params, + user_properties +} from '../ga4-properties' +import { updateUser } from '../ga4-functions' + +const action: BrowserActionDefinition = { + title: 'Purchase', + description: 'This event signifies when one or more items is purchased by a user.', + defaultSubscription: 'type = "track" and event = "Order Completed"', + platform: 'web', + fields: { + user_id: user_id, + coupon: { ...coupon, default: { '@path': '$.properties.coupon' } }, + currency: { ...currency, required: true }, + items: { + ...items_multi_products, + required: true + }, + transaction_id: { ...transaction_id, required: true }, + shipping: shipping, + tax: tax, + value: { ...value, default: { '@path': '$.properties.total' } }, + user_properties: user_properties, + params: params + }, + perform: (gtag, { payload }) => { + updateUser(payload.user_id, payload.user_properties, gtag) + + gtag('event', 'purchase', { + currency: payload.currency, + transaction_id: payload.transaction_id, + value: payload.value, + coupon: payload.coupon, + tax: payload.tax, + shipping: payload.shipping, + items: payload.items, + ...payload.params + }) + } +} + +export default action diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/refund/generated-types.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/refund/generated-types.ts new file mode 100644 index 0000000000..e97d5a5bb2 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/refund/generated-types.ts @@ -0,0 +1,129 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * A unique identifier for a user. See Google's [User-ID for cross-platform analysis](https://support.google.com/analytics/answer/9213390) and [Reporting: deduplicate user counts](https://support.google.com/analytics/answer/9355949?hl=en) documentation for more information on this identifier. + */ + user_id?: string + /** + * Currency of the items associated with the event, in 3-letter ISO 4217 format. + */ + currency?: string + /** + * The unique identifier of a transaction. + */ + transaction_id: string + /** + * The monetary value of the event. + */ + value?: number + /** + * Store or affiliation from which this transaction occurred (e.g. Google Store). + */ + affiliation?: string + /** + * Coupon code used for a purchase. + */ + coupon?: string + /** + * Shipping cost associated with the transaction. + */ + shipping?: number + /** + * Total tax associated with the transaction. + */ + tax?: number + /** + * The list of products purchased. + */ + items?: { + /** + * Identifier for the product being purchased. + */ + item_id?: string + /** + * Name of the product being purchased. + */ + item_name?: string + /** + * A product affiliation to designate a supplying company or brick and mortar store location. + */ + affiliation?: string + /** + * Coupon code used for a purchase. + */ + coupon?: string + /** + * Currency of the purchase or items associated with the event, in 3-letter ISO 4217 format. + */ + currency?: string + /** + * Monetary value of discount associated with a purchase. + */ + discount?: number + /** + * The index/position of the item in a list. + */ + index?: number + /** + * Brand associated with the product. + */ + item_brand?: string + /** + * Product category. + */ + item_category?: string + /** + * Product category 2. + */ + item_category2?: string + /** + * Product category 3. + */ + item_category3?: string + /** + * Product category 4. + */ + item_category4?: string + /** + * Product category 5. + */ + item_category5?: string + /** + * The ID of the list in which the item was presented to the user. + */ + item_list_id?: string + /** + * The name of the list in which the item was presented to the user. + */ + item_list_name?: string + /** + * Variant of the product (e.g. Black). + */ + item_variant?: string + /** + * The location associated with the item. + */ + location_id?: string + /** + * Price of the product being purchased, in units of the specified currency parameter. + */ + price?: number + /** + * Item quantity. + */ + quantity?: number + }[] + /** + * 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. + */ + user_properties?: { + [k: string]: unknown + } + /** + * The event parameters to send to Google Analytics 4. + */ + params?: { + [k: string]: unknown + } +} diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/refund/index.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/refund/index.ts new file mode 100644 index 0000000000..9ebff36472 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/refund/index.ts @@ -0,0 +1,57 @@ +import type { BrowserActionDefinition } from '../../../lib/browser-destinations' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' + +import { + coupon, + transaction_id, + user_id, + currency, + value, + affiliation, + shipping, + items_multi_products, + params, + user_properties, + tax +} from '../ga4-properties' +import { updateUser } from '../ga4-functions' + +const action: BrowserActionDefinition = { + title: 'Refund', + description: 'This event signifies when one or more items is refunded to a user.', + defaultSubscription: 'type = "track" and event = "Order Refunded"', + platform: 'web', + fields: { + user_id: user_id, + currency: currency, + transaction_id: { ...transaction_id, required: true }, + value: { ...value, default: { '@path': '$.properties.total' } }, + affiliation: affiliation, + coupon: coupon, + shipping: shipping, + tax: tax, + items: { + ...items_multi_products + }, + user_properties: user_properties, + params: params + }, + perform: (gtag, { payload }) => { + updateUser(payload.user_id, payload.user_properties, gtag) + + gtag('event', 'refund', { + currency: payload.currency, + transaction_id: payload.transaction_id, // Transaction ID. Required for purchases and refunds. + value: payload.value, + affiliation: payload.affiliation, + coupon: payload.coupon, + shipping: payload.shipping, + tax: payload.tax, + items: payload.items, + ...payload.params + }) + } +} + +export default action diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/removeFromCart/generated-types.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/removeFromCart/generated-types.ts new file mode 100644 index 0000000000..a7f5a2e20e --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/removeFromCart/generated-types.ts @@ -0,0 +1,109 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * A unique identifier for a user. See Google's [User-ID for cross-platform analysis](https://support.google.com/analytics/answer/9213390) and [Reporting: deduplicate user counts](https://support.google.com/analytics/answer/9355949?hl=en) documentation for more information on this identifier. + */ + user_id?: string + /** + * Currency of the items associated with the event, in 3-letter ISO 4217 format. + */ + currency?: string + /** + * The monetary value of the event. + */ + value?: number + /** + * The list of products purchased. + */ + items: { + /** + * Identifier for the product being purchased. + */ + item_id?: string + /** + * Name of the product being purchased. + */ + item_name?: string + /** + * A product affiliation to designate a supplying company or brick and mortar store location. + */ + affiliation?: string + /** + * Coupon code used for a purchase. + */ + coupon?: string + /** + * Currency of the purchase or items associated with the event, in 3-letter ISO 4217 format. + */ + currency?: string + /** + * Monetary value of discount associated with a purchase. + */ + discount?: number + /** + * The index/position of the item in a list. + */ + index?: number + /** + * Brand associated with the product. + */ + item_brand?: string + /** + * Product category. + */ + item_category?: string + /** + * Product category 2. + */ + item_category2?: string + /** + * Product category 3. + */ + item_category3?: string + /** + * Product category 4. + */ + item_category4?: string + /** + * Product category 5. + */ + item_category5?: string + /** + * The ID of the list in which the item was presented to the user. + */ + item_list_id?: string + /** + * The name of the list in which the item was presented to the user. + */ + item_list_name?: string + /** + * Variant of the product (e.g. Black). + */ + item_variant?: string + /** + * The location associated with the item. + */ + location_id?: string + /** + * Price of the product being purchased, in units of the specified currency parameter. + */ + price?: number + /** + * Item quantity. + */ + quantity?: number + }[] + /** + * 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. + */ + user_properties?: { + [k: string]: unknown + } + /** + * The event parameters to send to Google Analytics 4. + */ + params?: { + [k: string]: unknown + } +} diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/removeFromCart/index.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/removeFromCart/index.ts new file mode 100644 index 0000000000..a66e7b027a --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/removeFromCart/index.ts @@ -0,0 +1,37 @@ +import type { BrowserActionDefinition } from '../../../lib/browser-destinations' +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' + +// Change from unknown to the partner SDK types +const action: BrowserActionDefinition = { + title: 'Remove from Cart', + description: 'This event signifies that an item was removed from a cart.', + platform: 'web', + defaultSubscription: 'type = "track" and event = "Product Removed"', + fields: { + user_id: user_id, + currency: currency, + value: value, + items: { + ...items_single_products, + required: true + }, + user_properties: user_properties, + params: params + }, + perform: (gtag, { payload }) => { + updateUser(payload.user_id, payload.user_properties, gtag) + + gtag('event', 'remove_from_cart', { + currency: payload.currency, + value: payload.value, + items: payload.items, + ...payload.params + }) + } +} + +export default action diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/search/generated-types.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/search/generated-types.ts new file mode 100644 index 0000000000..94b8ec5941 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/search/generated-types.ts @@ -0,0 +1,24 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * A unique identifier for a user. See Google's [User-ID for cross-platform analysis](https://support.google.com/analytics/answer/9213390) and [Reporting: deduplicate user counts](https://support.google.com/analytics/answer/9355949?hl=en) documentation for more information on this identifier. + */ + user_id?: string + /** + * 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. + */ + user_properties?: { + [k: string]: unknown + } + /** + * The event parameters to send to Google Analytics 4. + */ + params?: { + [k: string]: unknown + } + /** + * The term that was searched for. + */ + search_term?: string +} diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/search/index.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/search/index.ts new file mode 100644 index 0000000000..5f07360d66 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/search/index.ts @@ -0,0 +1,30 @@ +import type { BrowserActionDefinition } from '../../../lib/browser-destinations' +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' + +// Change from unknown to the partner SDK types +const action: BrowserActionDefinition = { + title: 'Search', + description: 'The term that was searched for.', + defaultSubscription: 'type = "track" and event = "Products Searched"', + platform: 'web', + fields: { + user_id: user_id, + user_properties: user_properties, + params: params, + search_term: search_term + }, + perform: (gtag, { payload }) => { + updateUser(payload.user_id, payload.user_properties, gtag) + + gtag('event', 'search', { + search_term: payload.search_term, + ...payload.params + }) + } +} + +export default action diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/selectItem/generated-types.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/selectItem/generated-types.ts new file mode 100644 index 0000000000..32d9891f1e --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/selectItem/generated-types.ts @@ -0,0 +1,109 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * A unique identifier for a user. See Google's [User-ID for cross-platform analysis](https://support.google.com/analytics/answer/9213390) and [Reporting: deduplicate user counts](https://support.google.com/analytics/answer/9355949?hl=en) documentation for more information on this identifier. + */ + user_id?: string + /** + * The name of the list in which the item was presented to the user. + */ + item_list_name?: string + /** + * The ID of the list in which the item was presented to the user. + */ + item_list_id?: string + /** + * The list of products purchased. + */ + items: { + /** + * Identifier for the product being purchased. + */ + item_id?: string + /** + * Name of the product being purchased. + */ + item_name?: string + /** + * A product affiliation to designate a supplying company or brick and mortar store location. + */ + affiliation?: string + /** + * Coupon code used for a purchase. + */ + coupon?: string + /** + * Currency of the purchase or items associated with the event, in 3-letter ISO 4217 format. + */ + currency?: string + /** + * Monetary value of discount associated with a purchase. + */ + discount?: number + /** + * The index/position of the item in a list. + */ + index?: number + /** + * Brand associated with the product. + */ + item_brand?: string + /** + * Product category. + */ + item_category?: string + /** + * Product category 2. + */ + item_category2?: string + /** + * Product category 3. + */ + item_category3?: string + /** + * Product category 4. + */ + item_category4?: string + /** + * Product category 5. + */ + item_category5?: string + /** + * The ID of the list in which the item was presented to the user. + */ + item_list_id?: string + /** + * The name of the list in which the item was presented to the user. + */ + item_list_name?: string + /** + * Variant of the product (e.g. Black). + */ + item_variant?: string + /** + * The location associated with the item. + */ + location_id?: string + /** + * Price of the product being purchased, in units of the specified currency parameter. + */ + price?: number + /** + * Item quantity. + */ + quantity?: number + }[] + /** + * 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. + */ + user_properties?: { + [k: string]: unknown + } + /** + * The event parameters to send to Google Analytics 4. + */ + params?: { + [k: string]: unknown + } +} diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/selectItem/index.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/selectItem/index.ts new file mode 100644 index 0000000000..6d62ade808 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/selectItem/index.ts @@ -0,0 +1,43 @@ +import type { BrowserActionDefinition } from '../../../lib/browser-destinations' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' + +import { + user_properties, + params, + user_id, + items_single_products, + item_list_name, + item_list_id +} from '../ga4-properties' +import { updateUser } from '../ga4-functions' + +const action: BrowserActionDefinition = { + title: 'Select Item', + description: 'This event signifies an item was selected from a list.', + defaultSubscription: 'type = "track" and event = "Product Clicked"', + platform: 'web', + fields: { + user_id: user_id, + item_list_name: item_list_name, + item_list_id: item_list_id, + items: { + ...items_single_products, + required: true + }, + user_properties: user_properties, + params: params + }, + perform: (gtag, { payload }) => { + updateUser(payload.user_id, payload.user_properties, gtag) + + gtag('event', 'select_item', { + item_list_id: payload.item_list_id, + item_list_name: payload.item_list_name, + items: payload.items, + ...payload.params + }) + } +} + +export default action diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/selectPromotion/generated-types.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/selectPromotion/generated-types.ts new file mode 100644 index 0000000000..a429b75c73 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/selectPromotion/generated-types.ts @@ -0,0 +1,137 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * A unique identifier for a user. See Google's [User-ID for cross-platform analysis](https://support.google.com/analytics/answer/9213390) and [Reporting: deduplicate user counts](https://support.google.com/analytics/answer/9355949?hl=en) documentation for more information on this identifier. + */ + user_id?: string + /** + * The name of the promotional creative. + */ + creative_name?: string + /** + * The name of the promotional creative slot associated with the event. + */ + creative_slot?: string + /** + * The ID of the location. + */ + location_id?: string + /** + * The ID of the promotion associated with the event. + */ + promotion_id?: string + /** + * The name of the promotion associated with the event. + */ + promotion_name?: string + /** + * The list of products purchased. + */ + items?: { + /** + * Identifier for the product being purchased. + */ + item_id?: string + /** + * Name of the product being purchased. + */ + item_name?: string + /** + * A product affiliation to designate a supplying company or brick and mortar store location. + */ + affiliation?: string + /** + * Coupon code used for a purchase. + */ + coupon?: string + /** + * Currency of the purchase or items associated with the event, in 3-letter ISO 4217 format. + */ + currency?: string + /** + * Monetary value of discount associated with a purchase. + */ + discount?: number + /** + * The index/position of the item in a list. + */ + index?: number + /** + * Brand associated with the product. + */ + item_brand?: string + /** + * Product category. + */ + item_category?: string + /** + * Product category 2. + */ + item_category2?: string + /** + * Product category 3. + */ + item_category3?: string + /** + * Product category 4. + */ + item_category4?: string + /** + * Product category 5. + */ + item_category5?: string + /** + * The ID of the list in which the item was presented to the user. + */ + item_list_id?: string + /** + * The name of the list in which the item was presented to the user. + */ + item_list_name?: string + /** + * Variant of the product (e.g. Black). + */ + item_variant?: string + /** + * The location associated with the item. + */ + location_id?: string + /** + * Price of the product being purchased, in units of the specified currency parameter. + */ + price?: number + /** + * Item quantity. + */ + quantity?: number + /** + * The name of the promotional creative. + */ + creative_name?: string + /** + * The name of the promotional creative slot associated with the event. + */ + creative_slot?: string + /** + * The name of the promotion associated with the event. + */ + promotion_name?: string + /** + * The ID of the promotion associated with the event. + */ + promotion_id?: string + }[] + /** + * 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. + */ + user_properties?: { + [k: string]: unknown + } + /** + * The event parameters to send to Google Analytics 4. + */ + params?: { + [k: string]: unknown + } +} diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/selectPromotion/index.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/selectPromotion/index.ts new file mode 100644 index 0000000000..e2bcf2ab38 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/selectPromotion/index.ts @@ -0,0 +1,67 @@ +import type { BrowserActionDefinition } from '../../../lib/browser-destinations' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' + +import { + creative_name, + user_id, + creative_slot, + promotion_id, + promotion_name, + minimal_items, + items_single_products, + params, + user_properties, + location_id +} 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.', + defaultSubscription: 'type = "track" and event = "Promotion Clicked"', + platform: 'web', + fields: { + user_id: user_id, + creative_name: creative_name, + creative_slot: { ...creative_slot, default: { '@path': '$.properties.creative' } }, + location_id: location_id, + promotion_id: { ...promotion_id, default: { '@path': '$.properties.promotion_id' } }, + promotion_name: { ...promotion_name, default: { '@path': '$.properties.name' } }, + items: { + ...items_single_products, + properties: { + ...minimal_items.properties, + creative_name: { + ...creative_name + }, + creative_slot: { + ...creative_slot + }, + promotion_name: { + ...promotion_name + }, + promotion_id: { + ...promotion_id + } + } + }, + user_properties: user_properties, + params: params + }, + perform: (gtag, { payload }) => { + updateUser(payload.user_id, payload.user_properties, gtag) + + gtag('event', 'select_promotion', { + creative_name: payload.creative_name, + creative_slot: payload.creative_slot, + location_id: payload.location_id, + promotion_id: payload.promotion_id, + promotion_name: payload.promotion_name, + items: payload.items, + ...payload.params + }) + } +} + +export default action diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/setConfigurationFields/generated-types.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/setConfigurationFields/generated-types.ts new file mode 100644 index 0000000000..bc813c4763 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/setConfigurationFields/generated-types.ts @@ -0,0 +1,70 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * A unique identifier for a user. See Google's [User-ID for cross-platform analysis](https://support.google.com/analytics/answer/9213390) and [Reporting: deduplicate user counts](https://support.google.com/analytics/answer/9355949?hl=en) documentation for more information on this identifier. + */ + user_id?: string + /** + * 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. + */ + user_properties?: { + [k: string]: unknown + } + /** + * 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. + */ + ads_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. + */ + analytics_storage_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. + */ + campaign_content?: string + /** + * Use campaign ID to identify a specific campaign. Setting this value will override the utm_id query parameter. + */ + campaign_id?: string + /** + * Use campaign medium to identify a medium such as email or cost-per-click. Setting this value will override the utm_medium query parameter. + */ + campaign_medium?: string + /** + * Use campaign name to identify a specific product promotion or strategic campaign. Setting this value will override the utm_name query parameter. + */ + campaign_name?: string + /** + * Use campaign source to identify a search engine, newsletter name, or other source. Setting this value will override the utm_source query parameter. + */ + campaign_source?: string + /** + * Use campaign term to note the keywords for this ad. Setting this value will override the utm_term query parameter. + */ + campaign_term?: string + /** + * Categorize pages and screens into custom buckets so you can see metrics for related groups of information. More information in [Google documentation](https://support.google.com/analytics/answer/11523339). + */ + content_group?: string + /** + * The language preference of the user. If not set, defaults to the user's navigator.language value. + */ + language?: string + /** + * The full URL of the page. If not set, defaults to the user's document.location value. + */ + page_location?: string + /** + * The referral source that brought traffic to a page. This value is also used to compute the traffic source. The format of this value is a URL. If not set, defaults to the user's document.referrer value. + */ + page_referrer?: string + /** + * The title of the page or document. If not set, defaults to the user's document.title value. + */ + page_title?: string + /** + * 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 +} diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/setConfigurationFields/index.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/setConfigurationFields/index.ts new file mode 100644 index 0000000000..b00845ea08 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/setConfigurationFields/index.ts @@ -0,0 +1,142 @@ +import type { BrowserActionDefinition } from '../../../lib/browser-destinations' +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' + +// 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"', + fields: { + user_id: user_id, + user_properties: user_properties, + ads_storage_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: 'Ads Storage Consent State', + type: 'string' + }, + analytics_storage_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: 'Analytics Storage Consent State', + type: 'string' + }, + 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.', + label: 'Campaign Content', + type: 'string' + }, + campaign_id: { + description: + 'Use campaign ID to identify a specific campaign. Setting this value will override the utm_id query parameter. ', + label: 'Campaign ID', + type: 'string' + }, + campaign_medium: { + description: + 'Use campaign medium to identify a medium such as email or cost-per-click. Setting this value will override the utm_medium query parameter.', + label: 'Campaign Medium', + type: 'string' + }, + campaign_name: { + description: + 'Use campaign name to identify a specific product promotion or strategic campaign. Setting this value will override the utm_name query parameter.', + label: 'Campaign Name', + type: 'string' + }, + campaign_source: { + description: + 'Use campaign source to identify a search engine, newsletter name, or other source. Setting this value will override the utm_source query parameter.', + label: 'Campaign Source', + type: 'string' + }, + campaign_term: { + description: + 'Use campaign term to note the keywords for this ad. Setting this value will override the utm_term query parameter.', + label: 'Campaign Term', + type: 'string' + }, + content_group: { + description: `Categorize pages and screens into custom buckets so you can see metrics for related groups of information. More information in [Google documentation](https://support.google.com/analytics/answer/11523339).`, + label: 'Content Group', + type: 'string' + }, + language: { + description: `The language preference of the user. If not set, defaults to the user's navigator.language value.`, + label: 'Language', + type: 'string' + }, + page_location: { + description: `The full URL of the page. If not set, defaults to the user's document.location value.`, + label: 'Page Location', + type: 'string' + }, + page_referrer: { + description: `The referral source that brought traffic to a page. This value is also used to compute the traffic source. The format of this value is a URL. If not set, defaults to the user's document.referrer value.`, + label: 'Page Referrer', + type: 'string' + }, + page_title: { + description: `The title of the page or document. If not set, defaults to the user's document.title value.`, + label: 'Page Title', + type: 'string' + }, + screen_resolution: { + 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' + } + }, + perform: (gtag, { payload, settings }) => { + if (settings.enableConsentMode) { + window.gtag('consent', 'update', { + ad_storage: payload.ads_storage_consent_state, + analytics_storage: payload.analytics_storage_consent_state + }) + } + if (payload.screen_resolution) { + gtag('set', { screen_resolution: payload.screen_resolution }) + } + if (payload.page_title) { + gtag('set', { page_title: payload.page_title }) + } + if (payload.page_referrer) { + gtag('set', { page_referrer: payload.page_referrer }) + } + if (payload.page_location) { + gtag('set', { page_location: payload.page_location }) + } + if (payload.language) { + gtag('set', { language: payload.language }) + } + if (payload.content_group) { + gtag('set', { content_group: payload.content_group }) + } + if (payload.campaign_term) { + gtag('set', { campaign_term: payload.campaign_term }) + } + if (payload.campaign_source) { + gtag('set', { campaign_source: payload.campaign_source }) + } + if (payload.campaign_name) { + gtag('set', { campaign_name: payload.campaign_name }) + } + if (payload.campaign_medium) { + gtag('set', { campaign_medium: payload.campaign_medium }) + } + if (payload.campaign_id) { + gtag('set', { campaign_id: payload.campaign_id }) + } + if (payload.campaign_content) { + gtag('set', { campaign_content: payload.campaign_content }) + } + updateUser(payload.user_id, payload.user_properties, gtag) + } +} + +export default action diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/signUp/generated-types.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/signUp/generated-types.ts new file mode 100644 index 0000000000..c305ce6764 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/signUp/generated-types.ts @@ -0,0 +1,24 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * A unique identifier for a user. See Google's [User-ID for cross-platform analysis](https://support.google.com/analytics/answer/9213390) and [Reporting: deduplicate user counts](https://support.google.com/analytics/answer/9355949?hl=en) documentation for more information on this identifier. + */ + user_id?: string + /** + * The method used to login. + */ + method?: string + /** + * 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. + */ + user_properties?: { + [k: string]: unknown + } + /** + * The event parameters to send to Google Analytics 4. + */ + params?: { + [k: string]: unknown + } +} diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/signUp/index.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/signUp/index.ts new file mode 100644 index 0000000000..db4074f3bf --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/signUp/index.ts @@ -0,0 +1,29 @@ +import type { BrowserActionDefinition } from '../../../lib/browser-destinations' +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' + +const action: BrowserActionDefinition = { + title: 'Sign Up', + description: 'The method used for sign up.', + defaultSubscription: 'type = "track" and event = "Signed Up"', + platform: 'web', + fields: { + user_id: user_id, + method: method, + user_properties: user_properties, + params: params + }, + perform: (gtag, { payload }) => { + updateUser(payload.user_id, payload.user_properties, gtag) + + gtag('event', 'sign_up', { + method: payload.method, + ...payload.params + }) + } +} + +export default action diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/types.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/types.ts new file mode 100644 index 0000000000..7d63ebb01a --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/types.ts @@ -0,0 +1,3 @@ +export type GA = { + gtag: Function +} diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/viewCart/generated-types.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/viewCart/generated-types.ts new file mode 100644 index 0000000000..a7f5a2e20e --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/viewCart/generated-types.ts @@ -0,0 +1,109 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * A unique identifier for a user. See Google's [User-ID for cross-platform analysis](https://support.google.com/analytics/answer/9213390) and [Reporting: deduplicate user counts](https://support.google.com/analytics/answer/9355949?hl=en) documentation for more information on this identifier. + */ + user_id?: string + /** + * Currency of the items associated with the event, in 3-letter ISO 4217 format. + */ + currency?: string + /** + * The monetary value of the event. + */ + value?: number + /** + * The list of products purchased. + */ + items: { + /** + * Identifier for the product being purchased. + */ + item_id?: string + /** + * Name of the product being purchased. + */ + item_name?: string + /** + * A product affiliation to designate a supplying company or brick and mortar store location. + */ + affiliation?: string + /** + * Coupon code used for a purchase. + */ + coupon?: string + /** + * Currency of the purchase or items associated with the event, in 3-letter ISO 4217 format. + */ + currency?: string + /** + * Monetary value of discount associated with a purchase. + */ + discount?: number + /** + * The index/position of the item in a list. + */ + index?: number + /** + * Brand associated with the product. + */ + item_brand?: string + /** + * Product category. + */ + item_category?: string + /** + * Product category 2. + */ + item_category2?: string + /** + * Product category 3. + */ + item_category3?: string + /** + * Product category 4. + */ + item_category4?: string + /** + * Product category 5. + */ + item_category5?: string + /** + * The ID of the list in which the item was presented to the user. + */ + item_list_id?: string + /** + * The name of the list in which the item was presented to the user. + */ + item_list_name?: string + /** + * Variant of the product (e.g. Black). + */ + item_variant?: string + /** + * The location associated with the item. + */ + location_id?: string + /** + * Price of the product being purchased, in units of the specified currency parameter. + */ + price?: number + /** + * Item quantity. + */ + quantity?: number + }[] + /** + * 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. + */ + user_properties?: { + [k: string]: unknown + } + /** + * The event parameters to send to Google Analytics 4. + */ + params?: { + [k: string]: unknown + } +} diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/viewCart/index.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/viewCart/index.ts new file mode 100644 index 0000000000..b7d6c48bf6 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/viewCart/index.ts @@ -0,0 +1,36 @@ +import type { BrowserActionDefinition } from '../../../lib/browser-destinations' +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' + +const action: BrowserActionDefinition = { + title: 'View Cart', + description: 'This event signifies that a user viewed their cart.', + defaultSubscription: 'type = "track" and event = "Cart Viewed"', + platform: 'web', + fields: { + user_id: user_id, + currency: currency, + value: value, + items: { + ...items_multi_products, + required: true + }, + user_properties: user_properties, + params: params + }, + perform: (gtag, { payload }) => { + updateUser(payload.user_id, payload.user_properties, gtag) + + gtag('event', 'view_cart', { + currency: payload.currency, + value: payload.value, + items: payload.items, + ...payload.params + }) + } +} + +export default action diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/viewItem/generated-types.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/viewItem/generated-types.ts new file mode 100644 index 0000000000..a7f5a2e20e --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/viewItem/generated-types.ts @@ -0,0 +1,109 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * A unique identifier for a user. See Google's [User-ID for cross-platform analysis](https://support.google.com/analytics/answer/9213390) and [Reporting: deduplicate user counts](https://support.google.com/analytics/answer/9355949?hl=en) documentation for more information on this identifier. + */ + user_id?: string + /** + * Currency of the items associated with the event, in 3-letter ISO 4217 format. + */ + currency?: string + /** + * The monetary value of the event. + */ + value?: number + /** + * The list of products purchased. + */ + items: { + /** + * Identifier for the product being purchased. + */ + item_id?: string + /** + * Name of the product being purchased. + */ + item_name?: string + /** + * A product affiliation to designate a supplying company or brick and mortar store location. + */ + affiliation?: string + /** + * Coupon code used for a purchase. + */ + coupon?: string + /** + * Currency of the purchase or items associated with the event, in 3-letter ISO 4217 format. + */ + currency?: string + /** + * Monetary value of discount associated with a purchase. + */ + discount?: number + /** + * The index/position of the item in a list. + */ + index?: number + /** + * Brand associated with the product. + */ + item_brand?: string + /** + * Product category. + */ + item_category?: string + /** + * Product category 2. + */ + item_category2?: string + /** + * Product category 3. + */ + item_category3?: string + /** + * Product category 4. + */ + item_category4?: string + /** + * Product category 5. + */ + item_category5?: string + /** + * The ID of the list in which the item was presented to the user. + */ + item_list_id?: string + /** + * The name of the list in which the item was presented to the user. + */ + item_list_name?: string + /** + * Variant of the product (e.g. Black). + */ + item_variant?: string + /** + * The location associated with the item. + */ + location_id?: string + /** + * Price of the product being purchased, in units of the specified currency parameter. + */ + price?: number + /** + * Item quantity. + */ + quantity?: number + }[] + /** + * 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. + */ + user_properties?: { + [k: string]: unknown + } + /** + * The event parameters to send to Google Analytics 4. + */ + params?: { + [k: string]: unknown + } +} diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/viewItem/index.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/viewItem/index.ts new file mode 100644 index 0000000000..bd17b3cebc --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/viewItem/index.ts @@ -0,0 +1,37 @@ +import type { BrowserActionDefinition } from '../../../lib/browser-destinations' +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' + +const action: BrowserActionDefinition = { + title: 'View Item', + description: + 'This event signifies that some content was shown to the user. Use this event to discover the most popular items viewed.', + defaultSubscription: 'type = "track" and event = "Product Viewed"', + platform: 'web', + fields: { + user_id: user_id, + currency: currency, + value: value, + items: { + ...items_single_products, + required: true + }, + user_properties: user_properties, + params: params + }, + perform: (gtag, { payload }) => { + updateUser(payload.user_id, payload.user_properties, gtag) + + gtag('event', 'view_item', { + currency: payload.currency, + value: payload.value, + items: payload.items, + ...payload.params + }) + } +} + +export default action diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/viewItemList/generated-types.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/viewItemList/generated-types.ts new file mode 100644 index 0000000000..ad463886d4 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/viewItemList/generated-types.ts @@ -0,0 +1,109 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * A unique identifier for a user. See Google's [User-ID for cross-platform analysis](https://support.google.com/analytics/answer/9213390) and [Reporting: deduplicate user counts](https://support.google.com/analytics/answer/9355949?hl=en) documentation for more information on this identifier. + */ + user_id?: string + /** + * The ID of the list in which the item was presented to the user. + */ + item_list_id?: string + /** + * The name of the list in which the item was presented to the user. + */ + item_list_name?: string + /** + * The list of products purchased. + */ + items: { + /** + * Identifier for the product being purchased. + */ + item_id?: string + /** + * Name of the product being purchased. + */ + item_name?: string + /** + * A product affiliation to designate a supplying company or brick and mortar store location. + */ + affiliation?: string + /** + * Coupon code used for a purchase. + */ + coupon?: string + /** + * Currency of the purchase or items associated with the event, in 3-letter ISO 4217 format. + */ + currency?: string + /** + * Monetary value of discount associated with a purchase. + */ + discount?: number + /** + * The index/position of the item in a list. + */ + index?: number + /** + * Brand associated with the product. + */ + item_brand?: string + /** + * Product category. + */ + item_category?: string + /** + * Product category 2. + */ + item_category2?: string + /** + * Product category 3. + */ + item_category3?: string + /** + * Product category 4. + */ + item_category4?: string + /** + * Product category 5. + */ + item_category5?: string + /** + * The ID of the list in which the item was presented to the user. + */ + item_list_id?: string + /** + * The name of the list in which the item was presented to the user. + */ + item_list_name?: string + /** + * Variant of the product (e.g. Black). + */ + item_variant?: string + /** + * The location associated with the item. + */ + location_id?: string + /** + * Price of the product being purchased, in units of the specified currency parameter. + */ + price?: number + /** + * Item quantity. + */ + quantity?: number + }[] + /** + * 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. + */ + user_properties?: { + [k: string]: unknown + } + /** + * The event parameters to send to Google Analytics 4. + */ + params?: { + [k: string]: unknown + } +} diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/viewItemList/index.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/viewItemList/index.ts new file mode 100644 index 0000000000..3663587747 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/viewItemList/index.ts @@ -0,0 +1,36 @@ +import type { BrowserActionDefinition } from '../../../lib/browser-destinations' +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' + +const action: BrowserActionDefinition = { + title: 'View Item List', + description: 'Log this event when the user has been presented with a list of items of a certain category.', + platform: 'web', + defaultSubscription: 'type = "track" and event = "Promotion Viewed"', + fields: { + user_id: user_id, + item_list_id: item_list_id, + item_list_name: item_list_name, + items: { + ...items_multi_products, + required: true + }, + user_properties: user_properties, + params: params + }, + perform: (gtag, { payload }) => { + updateUser(payload.user_id, payload.user_properties, gtag) + + gtag('event', 'view_item_list', { + item_list_id: payload.item_list_id, + item_list_name: payload.item_list_name, + items: payload.items, + ...payload.params + }) + } +} + +export default action diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/viewPromotion/generated-types.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/viewPromotion/generated-types.ts new file mode 100644 index 0000000000..a08bd958e6 --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/viewPromotion/generated-types.ts @@ -0,0 +1,137 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * A unique identifier for a user. See Google's [User-ID for cross-platform analysis](https://support.google.com/analytics/answer/9213390) and [Reporting: deduplicate user counts](https://support.google.com/analytics/answer/9355949?hl=en) documentation for more information on this identifier. + */ + user_id?: string + /** + * The name of the promotional creative. + */ + creative_name?: string + /** + * The name of the promotional creative slot associated with the event. + */ + creative_slot?: string + /** + * The ID of the location. + */ + location_id?: string + /** + * The ID of the promotion associated with the event. + */ + promotion_id?: string + /** + * The name of the promotion associated with the event. + */ + promotion_name?: string + /** + * The list of products purchased. + */ + items: { + /** + * Identifier for the product being purchased. + */ + item_id?: string + /** + * Name of the product being purchased. + */ + item_name?: string + /** + * A product affiliation to designate a supplying company or brick and mortar store location. + */ + affiliation?: string + /** + * Coupon code used for a purchase. + */ + coupon?: string + /** + * Currency of the purchase or items associated with the event, in 3-letter ISO 4217 format. + */ + currency?: string + /** + * Monetary value of discount associated with a purchase. + */ + discount?: number + /** + * The index/position of the item in a list. + */ + index?: number + /** + * Brand associated with the product. + */ + item_brand?: string + /** + * Product category. + */ + item_category?: string + /** + * Product category 2. + */ + item_category2?: string + /** + * Product category 3. + */ + item_category3?: string + /** + * Product category 4. + */ + item_category4?: string + /** + * Product category 5. + */ + item_category5?: string + /** + * The ID of the list in which the item was presented to the user. + */ + item_list_id?: string + /** + * The name of the list in which the item was presented to the user. + */ + item_list_name?: string + /** + * Variant of the product (e.g. Black). + */ + item_variant?: string + /** + * The location associated with the item. + */ + location_id?: string + /** + * Price of the product being purchased, in units of the specified currency parameter. + */ + price?: number + /** + * Item quantity. + */ + quantity?: number + /** + * The name of the promotional creative. + */ + creative_name?: string + /** + * The name of the promotional creative slot associated with the event. + */ + creative_slot?: string + /** + * The name of the promotion associated with the event. + */ + promotion_name?: string + /** + * The ID of the promotion associated with the event. + */ + promotion_id?: string + }[] + /** + * 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. + */ + user_properties?: { + [k: string]: unknown + } + /** + * The event parameters to send to Google Analytics 4. + */ + params?: { + [k: string]: unknown + } +} diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/viewPromotion/index.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/viewPromotion/index.ts new file mode 100644 index 0000000000..fe2950a56a --- /dev/null +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/viewPromotion/index.ts @@ -0,0 +1,67 @@ +import type { BrowserActionDefinition } from '../../../lib/browser-destinations' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' +import { + creative_name, + creative_slot, + promotion_id, + promotion_name, + user_id, + minimal_items, + items_single_products, + params, + user_properties, + location_id +} from '../ga4-properties' +import { updateUser } from '../ga4-functions' + +const action: BrowserActionDefinition = { + title: 'View Promotion', + description: 'This event signifies a promotion was viewed from a list.', + defaultSubscription: 'type = "track"', + platform: 'web', + fields: { + user_id: user_id, + creative_name: creative_name, + creative_slot: { ...creative_slot, default: { '@path': '$.properties.creative' } }, + location_id: location_id, + promotion_id: { ...promotion_id, default: { '@path': '$.properties.promotion_id' } }, + promotion_name: { ...promotion_name, default: { '@path': '$.properties.name' } }, + items: { + ...items_single_products, + required: true, + properties: { + ...minimal_items.properties, + creative_name: { + ...creative_name + }, + creative_slot: { + ...creative_slot + }, + promotion_name: { + ...promotion_name + }, + promotion_id: { + ...promotion_id + } + } + }, + user_properties: user_properties, + params: params + }, + perform: (gtag, { payload }) => { + updateUser(payload.user_id, payload.user_properties, gtag) + + gtag('event', 'view_promotion', { + creative_name: payload.creative_name, + creative_slot: payload.creative_slot, + location_id: payload.location_id, + promotion_id: payload.promotion_id, + promotion_name: payload.promotion_name, + items: payload.items, + ...payload.params + }) + } +} + +export default action diff --git a/packages/browser-destinations/src/destinations/index.ts b/packages/browser-destinations/src/destinations/index.ts index 87b663afe6..f0fc861ecc 100644 --- a/packages/browser-destinations/src/destinations/index.ts +++ b/packages/browser-destinations/src/destinations/index.ts @@ -47,5 +47,4 @@ register('6372e1e36d9c2181f3900834', './wisepops') register('637c192eba61b944e08ee158', './vwo') register('638f843c4520d424f63c9e51', './commandbar') register('63913b2bf906ea939f153851', './ripe') - - +register('63b557a652cb6b4f28a4d118', './google-analytics-4-web') diff --git a/packages/cli-internal/package.json b/packages/cli-internal/package.json index aa733167a0..19e799fe92 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.127.0", + "version": "3.128.0", "license": "MIT", "repository": { "type": "git", @@ -53,8 +53,8 @@ "@oclif/config": "^1", "@oclif/errors": "^1", "@oclif/plugin-help": "^3.3", - "@segment/action-destinations": "^3.131.0", - "@segment/actions-core": "^3.51.0", + "@segment/action-destinations": "^3.132.0", + "@segment/actions-core": "^3.52.0", "@types/node": "^18.11.15", "chalk": "^4.1.1", "chokidar": "^3.5.1", @@ -78,7 +78,7 @@ "tslib": "^2.3.1" }, "optionalDependencies": { - "@segment/browser-destinations": "^3.74.0", + "@segment/browser-destinations": "^3.75.0", "@segment/control-plane-service-client": "github:segmentio/control-plane-service-js-client.git#master" }, "oclif": { diff --git a/packages/cli/package.json b/packages/cli/package.json index f366efe509..e9d784dc43 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.127.0", + "version": "3.128.0", "license": "MIT", "repository": { "type": "git", @@ -56,8 +56,8 @@ "@oclif/config": "^1", "@oclif/errors": "^1", "@oclif/plugin-help": "^3.3", - "@segment/action-destinations": "^3.131.0", - "@segment/actions-core": "^3.51.0", + "@segment/action-destinations": "^3.132.0", + "@segment/actions-core": "^3.52.0", "@types/node": "^18.11.15", "chalk": "^4.1.1", "chokidar": "^3.5.1", @@ -81,8 +81,8 @@ "tslib": "^2.3.1" }, "optionalDependencies": { - "@segment/actions-cli-internal": "^3.127.0", - "@segment/browser-destinations": "^3.74.0" + "@segment/actions-cli-internal": "^3.128.0", + "@segment/browser-destinations": "^3.75.0" }, "oclif": { "commands": "./dist/commands", diff --git a/packages/core/package.json b/packages/core/package.json index 1ae5302ab0..48cf3e45a9 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.51.0", + "version": "3.52.0", "repository": { "type": "git", "url": "https://github.com/segmentio/fab-5-engine", @@ -55,7 +55,7 @@ "dependencies": { "@lukeed/uuid": "^2.0.0", "@segment/ajv-human-errors": "^2.2.0", - "@segment/destination-subscriptions": "^3.15.0", + "@segment/destination-subscriptions": "^3.16.0", "@types/node": "^18.11.15", "abort-controller": "^3.0.0", "aggregate-error": "^3.1.0", diff --git a/packages/destination-actions/package.json b/packages/destination-actions/package.json index 87d13ce546..44ee7ad781 100644 --- a/packages/destination-actions/package.json +++ b/packages/destination-actions/package.json @@ -1,7 +1,7 @@ { "name": "@segment/action-destinations", "description": "Destination Actions engine and definitions.", - "version": "3.131.0", + "version": "3.132.0", "repository": { "type": "git", "url": "https://github.com/segmentio/action-destinations", @@ -39,8 +39,8 @@ "dependencies": { "@amplitude/ua-parser-js": "^0.7.25", "@segment/a1-notation": "^2.1.4", - "@segment/actions-core": "^3.51.0", - "@segment/actions-shared": "^1.33.0", + "@segment/actions-core": "^3.52.0", + "@segment/actions-shared": "^1.34.0", "@types/node": "^18.11.15", "cheerio": "^1.0.0-rc.10", "dayjs": "^1.10.7", diff --git a/packages/destination-actions/src/destinations/algolia-insights/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/algolia-insights/__tests__/__snapshots__/snapshot.test.ts.snap new file mode 100644 index 0000000000..66b709e342 --- /dev/null +++ b/packages/destination-actions/src/destinations/algolia-insights/__tests__/__snapshots__/snapshot.test.ts.snap @@ -0,0 +1,128 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Testing snapshot for actions-algolia-insights destination: conversionEvents action - all fields 1`] = ` +Object { + "events": Array [ + Object { + "eventName": "Conversion Event", + "eventType": "conversion", + "index": "U[ABpE$k", + "objectIDs": Array [ + "U[ABpE$k", + ], + "products": Array [ + Object { + "product_id": "U[ABpE$k", + }, + ], + "queryID": "U[ABpE$k", + "timestamp": null, + "userToken": "U[ABpE$k", + }, + ], +} +`; + +exports[`Testing snapshot for actions-algolia-insights destination: conversionEvents action - required fields 1`] = ` +Object { + "events": Array [ + Object { + "eventName": "Conversion Event", + "eventType": "conversion", + "index": "U[ABpE$k", + "objectIDs": Array [ + "U[ABpE$k", + ], + "products": Array [ + Object { + "product_id": "U[ABpE$k", + }, + ], + "queryID": "U[ABpE$k", + "userToken": "U[ABpE$k", + }, + ], +} +`; + +exports[`Testing snapshot for actions-algolia-insights destination: productClickedEvents action - all fields 1`] = ` +Object { + "events": Array [ + Object { + "eventName": "Product Clicked", + "eventType": "click", + "index": "LLjxSD^^GnH", + "objectID": "LLjxSD^^GnH", + "objectIDs": Array [ + "LLjxSD^^GnH", + ], + "position": -1912532923056128, + "positions": Array [ + -1912532923056128, + ], + "queryID": "LLjxSD^^GnH", + "timestamp": null, + "userToken": "LLjxSD^^GnH", + }, + ], +} +`; + +exports[`Testing snapshot for actions-algolia-insights destination: productClickedEvents action - required fields 1`] = ` +Object { + "events": Array [ + Object { + "eventName": "Product Clicked", + "eventType": "click", + "index": "LLjxSD^^GnH", + "objectID": "LLjxSD^^GnH", + "objectIDs": Array [ + "LLjxSD^^GnH", + ], + "position": -1912532923056128, + "positions": Array [ + -1912532923056128, + ], + "queryID": "LLjxSD^^GnH", + "userToken": "LLjxSD^^GnH", + }, + ], +} +`; + +exports[`Testing snapshot for actions-algolia-insights destination: productViewedEvents action - all fields 1`] = ` +Object { + "events": Array [ + Object { + "eventName": "Product Viewed", + "eventType": "view", + "index": "BLFCPcmz", + "objectID": "BLFCPcmz", + "objectIDs": Array [ + "BLFCPcmz", + ], + "queryID": "BLFCPcmz", + "timestamp": null, + "userToken": "BLFCPcmz", + }, + ], +} +`; + +exports[`Testing snapshot for actions-algolia-insights destination: productViewedEvents action - required fields 1`] = ` +Object { + "events": Array [ + Object { + "eventName": "Product Viewed", + "eventType": "view", + "index": "BLFCPcmz", + "objectID": "BLFCPcmz", + "objectIDs": Array [ + "BLFCPcmz", + ], + "queryID": "BLFCPcmz", + "userToken": "BLFCPcmz", + }, + ], +} +`; diff --git a/packages/destination-actions/src/destinations/algolia-insights/__tests__/index.test.ts b/packages/destination-actions/src/destinations/algolia-insights/__tests__/index.test.ts new file mode 100644 index 0000000000..bbd90aa9c0 --- /dev/null +++ b/packages/destination-actions/src/destinations/algolia-insights/__tests__/index.test.ts @@ -0,0 +1,58 @@ +import nock from 'nock' +import { createTestIntegration } from '@segment/actions-core' +import Definition from '../index' +import { algoliaApiPermissionsUrl } from '../algolia-insight-api' + +const testDestination = createTestIntegration(Definition) + +describe('Algolia Insights', () => { + describe('testAuthentication', () => { + it('should validate authentication inputs', async () => { + const settings = { + appId: 'algolia-application-id', + apiKey: 'algolia-api-key' + } + const authenticateUrl = algoliaApiPermissionsUrl(settings) + + nock(authenticateUrl.slice(0, authenticateUrl.indexOf('/1/'))) + .get('/1/keys/algolia-api-key') + .reply(200, { acl: ['search'] }) + + // there is no testAuthentication function for this destination so this just validates the authData schema + await expect(testDestination.testAuthentication(settings)).resolves.not.toThrowError() + }) + + it('should reject invalid credentials', async () => { + const settings = { + appId: 'algolia-application-id', + apiKey: 'algolia-api-key' + } + const authenticateUrl = algoliaApiPermissionsUrl(settings) + + nock(authenticateUrl.slice(0, authenticateUrl.indexOf('/1/'))) + .get('/1/keys/algolia-api-key') + .reply(403, { + message: 'Invalid Application-ID or API key', + status: 403 + }) + + // there is no testAuthentication function for this destination so this just validates the authData schema + await expect(testDestination.testAuthentication(settings)).rejects.toThrow() + }) + + it('should reject invalid acl', async () => { + const settings = { + appId: 'algolia-application-id', + apiKey: 'algolia-api-key' + } + const authenticateUrl = algoliaApiPermissionsUrl(settings) + + nock(authenticateUrl.slice(0, authenticateUrl.indexOf('/1/'))) + .get('/1/keys/algolia-api-key') + .reply(200, { acl: ['listIndexes'] }) + + // there is no testAuthentication function for this destination so this just validates the authData schema + await expect(testDestination.testAuthentication(settings)).rejects.toThrow() + }) + }) +}) diff --git a/packages/destination-actions/src/destinations/algolia-insights/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/algolia-insights/__tests__/snapshot.test.ts new file mode 100644 index 0000000000..83ba321da9 --- /dev/null +++ b/packages/destination-actions/src/destinations/algolia-insights/__tests__/snapshot.test.ts @@ -0,0 +1,77 @@ +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import { generateTestData } from '../../../lib/test-data' +import destination from '../index' +import nock from 'nock' + +const testDestination = createTestIntegration(destination) +const destinationSlug = 'actions-algolia-insights' + +describe(`Testing snapshot for ${destinationSlug} destination:`, () => { + for (const actionSlug in destination.actions) { + it(`${actionSlug} action - required fields`, async () => { + const seedName = `${destinationSlug}#${actionSlug}` + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, true) + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + properties: eventData + }) + + const responses = await testDestination.testAction(actionSlug, { + event: event, + mapping: event.properties, + settings: settingsData, + auth: undefined + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + + expect(request.headers).toMatchSnapshot() + }) + + it(`${actionSlug} action - all fields`, async () => { + const seedName = `${destinationSlug}#${actionSlug}` + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, false) + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + properties: eventData + }) + + const responses = await testDestination.testAction(actionSlug, { + event: event, + mapping: event.properties, + settings: settingsData, + auth: undefined + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + }) + } +}) diff --git a/packages/destination-actions/src/destinations/algolia-insights/algolia-insight-api.ts b/packages/destination-actions/src/destinations/algolia-insights/algolia-insight-api.ts new file mode 100644 index 0000000000..c047331cb0 --- /dev/null +++ b/packages/destination-actions/src/destinations/algolia-insights/algolia-insight-api.ts @@ -0,0 +1,29 @@ +import type { Settings } from './generated-types' +export const BaseAlgoliaInsightsURL = 'https://insights.algolia.io' +export const AlgoliaBehaviourURL = BaseAlgoliaInsightsURL + '/1/events' +export const algoliaApiPermissionsUrl = (settings: Settings) => + `https://${settings.appId}.algolia.net/1/keys/${settings.apiKey}` + +type EventCommon = { + eventName: string + index: string + userToken: string + objectIDs: string[] + timestamp?: number +} + +export type AlgoliaProductViewedEvent = EventCommon & { + eventType: 'view' +} + +export type AlgoliaProductClickedEvent = EventCommon & { + eventType: 'click' + queryID: string + positions: number[] +} + +export type AlgoliaConversionEvent = EventCommon & { + eventType: 'conversion' +} + +export type AlgoliaApiPermissions = { acl: string[] } diff --git a/packages/destination-actions/src/destinations/algolia-insights/conversionEvents/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/algolia-insights/conversionEvents/__tests__/__snapshots__/snapshot.test.ts.snap new file mode 100644 index 0000000000..de7dfb0988 --- /dev/null +++ b/packages/destination-actions/src/destinations/algolia-insights/conversionEvents/__tests__/__snapshots__/snapshot.test.ts.snap @@ -0,0 +1,47 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Testing snapshot for AlgoliaInsights's conversionEvents destination action: all fields 1`] = ` +Object { + "events": Array [ + Object { + "eventName": "Conversion Event", + "eventType": "conversion", + "index": ")j)vR5%1AP*epuo8A%R", + "objectIDs": Array [ + ")j)vR5%1AP*epuo8A%R", + ], + "products": Array [ + Object { + "product_id": ")j)vR5%1AP*epuo8A%R", + }, + ], + "queryID": ")j)vR5%1AP*epuo8A%R", + "timestamp": null, + "userToken": ")j)vR5%1AP*epuo8A%R", + }, + ], +} +`; + +exports[`Testing snapshot for AlgoliaInsights's conversionEvents destination action: required fields 1`] = ` +Object { + "events": Array [ + Object { + "eventName": "Conversion Event", + "eventType": "conversion", + "index": ")j)vR5%1AP*epuo8A%R", + "objectIDs": Array [ + ")j)vR5%1AP*epuo8A%R", + ], + "products": Array [ + Object { + "product_id": ")j)vR5%1AP*epuo8A%R", + }, + ], + "queryID": ")j)vR5%1AP*epuo8A%R", + "timestamp": 1674843786677, + "userToken": ")j)vR5%1AP*epuo8A%R", + }, + ], +} +`; diff --git a/packages/destination-actions/src/destinations/algolia-insights/conversionEvents/__tests__/index.test.ts b/packages/destination-actions/src/destinations/algolia-insights/conversionEvents/__tests__/index.test.ts new file mode 100644 index 0000000000..c0a1182040 --- /dev/null +++ b/packages/destination-actions/src/destinations/algolia-insights/conversionEvents/__tests__/index.test.ts @@ -0,0 +1,84 @@ +import nock from 'nock' +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import Destination, { ALGOLIA_INSIGHTS_USER_AGENT } from '../../index' +import { AlgoliaConversionEvent, BaseAlgoliaInsightsURL } from '../../algolia-insight-api' +import { SegmentEvent } from '@segment/actions-core' + +const testDestination = createTestIntegration(Destination) + +const algoliaDestinationActionSettings = { + appId: 'algolia-application-id', + apiKey: 'algolia-api-key' +} +const testAlgoliaDestination = async (event: SegmentEvent): Promise => { + nock(BaseAlgoliaInsightsURL).post('/1/events').reply(200, {}) + const segmentEvent = { + event: { ...event }, + settings: algoliaDestinationActionSettings, + useDefaultMappings: true + } + const actionResponse = await testDestination.testAction('conversionEvents', segmentEvent) + const actionRequest = actionResponse[0].request + + expect(actionResponse.length).toBe(1) + expect(actionRequest.headers.get('X-Algolia-Application-Id')).toBe(algoliaDestinationActionSettings.appId) + expect(actionRequest.headers.get('X-Algolia-API-Key')).toBe(algoliaDestinationActionSettings.apiKey) + expect(actionRequest.headers.get('X-Algolia-Agent')).toBe(ALGOLIA_INSIGHTS_USER_AGENT) + + const rawBody = await actionRequest.text() + return JSON.parse(rawBody)['events'][0] +} + +describe('AlgoliaInsights.conversionEvents', () => { + it('should submit conversion on track "Order Completed" event', async () => { + const event = createTestEvent({ + type: 'track', + event: 'Order Completed', + properties: { + query_id: '1234', + search_index: 'fashion_1', + products: [ + { + product_id: '9876', + product_name: 'skirt 1' + }, + { + product_id: '5432', + product_name: 'skirt 2' + } + ] + } + }) + const algoliaEvent = await testAlgoliaDestination(event) + + expect(algoliaEvent.eventName).toBe('Conversion Event') + expect(algoliaEvent.eventType).toBe('conversion') + expect(algoliaEvent.index).toBe(event.properties?.search_index) + expect(algoliaEvent.userToken).toBe(event.userId) + expect(algoliaEvent.objectIDs).toContain('9876') + expect(algoliaEvent.objectIDs).toContain('5432') + }) + + it('should pass timestamp if present', async () => { + const event = createTestEvent({ + type: 'track', + event: 'Order Completed', + properties: { + query_id: '1234', + search_index: 'fashion_1', + products: [ + { + product_id: '9876', + product_name: 'skirt 1' + }, + { + product_id: '5432', + product_name: 'skirt 2' + } + ] + } + }) + const algoliaEvent = await testAlgoliaDestination(event) + expect(algoliaEvent.timestamp).toBe(new Date(event.timestamp as string).valueOf()) + }) +}) diff --git a/packages/destination-actions/src/destinations/algolia-insights/conversionEvents/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/algolia-insights/conversionEvents/__tests__/snapshot.test.ts new file mode 100644 index 0000000000..5c61c409d8 --- /dev/null +++ b/packages/destination-actions/src/destinations/algolia-insights/conversionEvents/__tests__/snapshot.test.ts @@ -0,0 +1,79 @@ +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import { generateTestData } from '../../../../lib/test-data' +import destination from '../../index' +import nock from 'nock' + +const testDestination = createTestIntegration(destination) +const actionSlug = 'conversionEvents' +const destinationSlug = 'AlgoliaInsights' +const seedName = `${destinationSlug}#${actionSlug}` + +describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination action:`, () => { + it('required fields', async () => { + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, true) + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + timestamp: new Date('2023-01-27T18:23:06.677Z').toISOString(), + properties: eventData + }) + + const responses = await testDestination.testAction(actionSlug, { + useDefaultMappings: true, + event: event, + mapping: event.properties, + settings: settingsData, + auth: undefined + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + + expect(request.headers).toMatchSnapshot() + }) + + it('all fields', async () => { + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, false) + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + timestamp: new Date('2023-01-27T18:23:06.677Z').toISOString(), + properties: eventData + }) + + const responses = await testDestination.testAction(actionSlug, { + useDefaultMappings: true, + event: event, + mapping: event.properties, + settings: settingsData, + auth: undefined + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + }) +}) diff --git a/packages/destination-actions/src/destinations/algolia-insights/conversionEvents/generated-types.ts b/packages/destination-actions/src/destinations/algolia-insights/conversionEvents/generated-types.ts new file mode 100644 index 0000000000..37e027191e --- /dev/null +++ b/packages/destination-actions/src/destinations/algolia-insights/conversionEvents/generated-types.ts @@ -0,0 +1,26 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * An array of objects representing the purchased items. Each object must contains a product_id field. + */ + products: { + product_id: string + }[] + /** + * Name of the targeted search index. + */ + index: string + /** + * Query ID of the list on which the item was clicked. + */ + queryID: string + /** + * The ID associated with the user. + */ + userToken: string + /** + * The timestamp of the event. + */ + timestamp?: string +} diff --git a/packages/destination-actions/src/destinations/algolia-insights/conversionEvents/index.ts b/packages/destination-actions/src/destinations/algolia-insights/conversionEvents/index.ts new file mode 100644 index 0000000000..83e4a5d3a3 --- /dev/null +++ b/packages/destination-actions/src/destinations/algolia-insights/conversionEvents/index.ts @@ -0,0 +1,87 @@ +import type { ActionDefinition } from '@segment/actions-core' +import { Subscription, defaultValues } from '@segment/actions-core' +import { AlgoliaBehaviourURL, AlgoliaConversionEvent } from '../algolia-insight-api' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' + +export const conversionEvents: ActionDefinition = { + title: 'Conversion Events', + description: 'Successful product purcahses which can be tied back to an Algolia Search, Recommend or Predict result', + fields: { + products: { + label: 'Product Details', + description: + 'An array of objects representing the purchased items. Each object must contains a product_id field.', + type: 'object', + multiple: true, + properties: { product_id: { label: 'product_id', type: 'string', required: true } }, + required: true, + default: { + '@path': '$.properties.products' + } + }, + index: { + label: 'Index', + description: 'Name of the targeted search index.', + type: 'string', + required: true, + default: { + '@path': '$.properties.search_index' + } + }, + queryID: { + label: 'Query ID', + description: 'Query ID of the list on which the item was clicked.', + type: 'string', + required: true, + default: { + '@path': '$.properties.query_id' + } + }, + userToken: { + type: 'string', + required: true, + description: 'The ID associated with the user.', + label: 'userToken', + default: { + '@if': { + exists: { '@path': '$.userId' }, + then: { '@path': '$.userId' }, + else: { '@path': '$.anonymousId' } + } + } + }, + timestamp: { + type: 'string', + required: false, + description: 'The timestamp of the event.', + label: 'timestamp', + default: { '@path': '$.timestamp' } + } + }, + defaultSubscription: 'type = "track" and event = "Order Completed"', + perform: (request, data) => { + const insightEvent: AlgoliaConversionEvent = { + ...data.payload, + eventName: 'Conversion Event', + eventType: 'conversion', + objectIDs: data.payload.products.map((product) => product.product_id), + userToken: data.payload.userToken, + timestamp: data.payload.timestamp ? new Date(data.payload.timestamp).valueOf() : undefined + } + const insightPayload = { events: [insightEvent] } + + return request(AlgoliaBehaviourURL, { + method: 'post', + json: insightPayload + }) + } +} + +/** used in the quick setup */ +export const conversionPresets: Subscription = { + name: 'Send conversion events to Algolia', + subscribe: conversionEvents.defaultSubscription as string, + partnerAction: 'conversionEvents', + mapping: defaultValues(conversionEvents.fields) +} diff --git a/packages/destination-actions/src/destinations/algolia-insights/generated-types.ts b/packages/destination-actions/src/destinations/algolia-insights/generated-types.ts new file mode 100644 index 0000000000..6da2455a36 --- /dev/null +++ b/packages/destination-actions/src/destinations/algolia-insights/generated-types.ts @@ -0,0 +1,12 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Settings { + /** + * Your Algolia Application ID. + */ + appId: string + /** + * An API key which has write permissions to the Algolia Insights API + */ + apiKey: string +} diff --git a/packages/destination-actions/src/destinations/algolia-insights/index.ts b/packages/destination-actions/src/destinations/algolia-insights/index.ts new file mode 100644 index 0000000000..e191695039 --- /dev/null +++ b/packages/destination-actions/src/destinations/algolia-insights/index.ts @@ -0,0 +1,63 @@ +import type { DestinationDefinition } from '@segment/actions-core' +import type { Settings } from './generated-types' + +import { productClickedEvents, productClickPresets } from './productClickedEvents' + +import { conversionEvents, conversionPresets } from './conversionEvents' + +import { productViewedEvents, productViewedPresets } from './productViewedEvents' +import { AlgoliaApiPermissions, algoliaApiPermissionsUrl } from './algolia-insight-api' + +export const ALGOLIA_INSIGHTS_USER_AGENT = 'algolia-segment-action-destination: 0.1' + +const destination: DestinationDefinition = { + name: 'Algolia Insights (Actions)', + slug: 'actions-algolia-insights', + mode: 'cloud', + + authentication: { + scheme: 'custom', + fields: { + appId: { + label: 'appId', + description: 'Your Algolia Application ID.', + type: 'string', + required: true + }, + apiKey: { + label: 'apiKey', + description: 'An API key which has write permissions to the Algolia Insights API', + type: 'string', + required: true + } + }, + testAuthentication: async (request, { settings }) => { + const response = await request(algoliaApiPermissionsUrl(settings)) + + if (response.data.acl.indexOf('search') === -1) { + return Promise.reject('Invalid acl permissions.') + } + + return response + } + }, + + extendRequest: ({ settings }) => { + return { + headers: { + 'X-Algolia-Application-Id': settings.appId, + 'X-Algolia-API-Key': settings.apiKey, + 'X-Algolia-Agent': ALGOLIA_INSIGHTS_USER_AGENT + } + } + }, + // TODO: figure out how to pass multiple presets + presets: [productClickPresets, conversionPresets, productViewedPresets], + actions: { + productClickedEvents, + conversionEvents, + productViewedEvents + } +} + +export default destination diff --git a/packages/destination-actions/src/destinations/algolia-insights/productClickedEvents/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/algolia-insights/productClickedEvents/__tests__/__snapshots__/snapshot.test.ts.snap new file mode 100644 index 0000000000..b5db46af76 --- /dev/null +++ b/packages/destination-actions/src/destinations/algolia-insights/productClickedEvents/__tests__/__snapshots__/snapshot.test.ts.snap @@ -0,0 +1,47 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Testing snapshot for AlgoliaInsights's productClickedEvents destination action: all fields 1`] = ` +Object { + "events": Array [ + Object { + "eventName": "Product Clicked", + "eventType": "click", + "index": "tTO6#", + "objectID": "tTO6#", + "objectIDs": Array [ + "tTO6#", + ], + "position": -8127732168785920, + "positions": Array [ + -8127732168785920, + ], + "queryID": "tTO6#", + "timestamp": null, + "userToken": "tTO6#", + }, + ], +} +`; + +exports[`Testing snapshot for AlgoliaInsights's productClickedEvents destination action: required fields 1`] = ` +Object { + "events": Array [ + Object { + "eventName": "Product Clicked", + "eventType": "click", + "index": "tTO6#", + "objectID": "tTO6#", + "objectIDs": Array [ + "tTO6#", + ], + "position": -8127732168785920, + "positions": Array [ + -8127732168785920, + ], + "queryID": "tTO6#", + "timestamp": 1674843786677, + "userToken": "tTO6#", + }, + ], +} +`; diff --git a/packages/destination-actions/src/destinations/algolia-insights/productClickedEvents/__tests__/index.test.ts b/packages/destination-actions/src/destinations/algolia-insights/productClickedEvents/__tests__/index.test.ts new file mode 100644 index 0000000000..a1b71808bd --- /dev/null +++ b/packages/destination-actions/src/destinations/algolia-insights/productClickedEvents/__tests__/index.test.ts @@ -0,0 +1,68 @@ +import nock from 'nock' +import { createTestEvent, createTestIntegration, SegmentEvent } from '@segment/actions-core' +import Destination, { ALGOLIA_INSIGHTS_USER_AGENT } from '../../index' +import { AlgoliaProductClickedEvent, BaseAlgoliaInsightsURL } from '../../algolia-insight-api' + +const testDestination = createTestIntegration(Destination) + +const algoliaDestinationActionSettings = { + appId: 'algolia-application-id', + apiKey: 'algolia-api-key' +} +const testAlgoliaDestination = async (event: SegmentEvent): Promise => { + nock(BaseAlgoliaInsightsURL).post('/1/events').reply(200, {}) + const segmentEvent = { + event: { ...event }, + settings: algoliaDestinationActionSettings, + useDefaultMappings: true + } + const actionResponse = await testDestination.testAction('productClickedEvents', segmentEvent) + const actionRequest = actionResponse[0].request + + expect(actionResponse.length).toBe(1) + expect(actionRequest.headers.get('X-Algolia-Application-Id')).toBe(algoliaDestinationActionSettings.appId) + expect(actionRequest.headers.get('X-Algolia-API-Key')).toBe(algoliaDestinationActionSettings.apiKey) + expect(actionRequest.headers.get('X-Algolia-Agent')).toBe(ALGOLIA_INSIGHTS_USER_AGENT) + + const rawBody = await actionRequest.text() + return JSON.parse(rawBody)['events'][0] +} + +describe('AlgoliaInsights.productClickedEvents', () => { + it('should submit click on track "Product Clicked" event', async () => { + const event = createTestEvent({ + type: 'track', + event: 'Product Clicked', + properties: { + query_id: '1234', + search_index: 'fashion_1', + product_id: '9876', + position: 5 + } + }) + const algoliaEvent = await testAlgoliaDestination(event) + + expect(algoliaEvent.eventName).toBe('Product Clicked') + expect(algoliaEvent.eventType).toBe('click') + expect(algoliaEvent.index).toBe(event.properties?.search_index) + expect(algoliaEvent.userToken).toBe(event.userId) + expect(algoliaEvent.queryID).toBe(event.properties?.query_id) + expect(algoliaEvent.objectIDs).toContain('9876') + expect(algoliaEvent.positions).toContain(5) + }) + + it('should pass timestamp if present', async () => { + const event = createTestEvent({ + type: 'track', + event: 'Product Clicked', + properties: { + query_id: '1234', + search_index: 'fashion_1', + product_id: '9876', + position: 5 + } + }) + const algoliaEvent = await testAlgoliaDestination(event) + expect(algoliaEvent.timestamp).toBe(new Date(event.timestamp as string).valueOf()) + }) +}) diff --git a/packages/destination-actions/src/destinations/algolia-insights/productClickedEvents/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/algolia-insights/productClickedEvents/__tests__/snapshot.test.ts new file mode 100644 index 0000000000..475d8e3722 --- /dev/null +++ b/packages/destination-actions/src/destinations/algolia-insights/productClickedEvents/__tests__/snapshot.test.ts @@ -0,0 +1,79 @@ +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import { generateTestData } from '../../../../lib/test-data' +import destination from '../../index' +import nock from 'nock' + +const testDestination = createTestIntegration(destination) +const actionSlug = 'productClickedEvents' +const destinationSlug = 'AlgoliaInsights' +const seedName = `${destinationSlug}#${actionSlug}` + +describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination action:`, () => { + it('required fields', async () => { + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, true) + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + timestamp: new Date('2023-01-27T18:23:06.677Z').toISOString(), + properties: eventData + }) + + const responses = await testDestination.testAction(actionSlug, { + useDefaultMappings: true, + event: event, + mapping: event.properties, + settings: settingsData, + auth: undefined + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + + expect(request.headers).toMatchSnapshot() + }) + + it('all fields', async () => { + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, false) + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + timestamp: new Date('2023-01-27T18:23:06.677Z').toISOString(), + properties: eventData + }) + + const responses = await testDestination.testAction(actionSlug, { + useDefaultMappings: true, + event: event, + mapping: event.properties, + settings: settingsData, + auth: undefined + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + }) +}) diff --git a/packages/destination-actions/src/destinations/algolia-insights/productClickedEvents/generated-types.ts b/packages/destination-actions/src/destinations/algolia-insights/productClickedEvents/generated-types.ts new file mode 100644 index 0000000000..61fdbf4215 --- /dev/null +++ b/packages/destination-actions/src/destinations/algolia-insights/productClickedEvents/generated-types.ts @@ -0,0 +1,28 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * Product ID of the clicked item. + */ + objectID: string + /** + * Name of the targeted search index. + */ + index: string + /** + * Query ID of the list on which the item was clicked. + */ + queryID: string + /** + * Position of the click in the list of Algolia search results. + */ + position: number + /** + * The ID associated with the user. + */ + userToken: string + /** + * The timestamp of the event. + */ + timestamp?: string +} diff --git a/packages/destination-actions/src/destinations/algolia-insights/productClickedEvents/index.ts b/packages/destination-actions/src/destinations/algolia-insights/productClickedEvents/index.ts new file mode 100644 index 0000000000..bd51036e36 --- /dev/null +++ b/packages/destination-actions/src/destinations/algolia-insights/productClickedEvents/index.ts @@ -0,0 +1,94 @@ +import type { ActionDefinition } from '@segment/actions-core' +import { Subscription, defaultValues } from '@segment/actions-core' +import { AlgoliaBehaviourURL, AlgoliaProductClickedEvent } from '../algolia-insight-api' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' + +export const productClickedEvents: ActionDefinition = { + title: 'Product Clicked Events', + description: 'When a product is clicked within an Algolia Search, Recommend or Predict result', + fields: { + objectID: { + label: 'Product ID', + description: 'Product ID of the clicked item.', + type: 'string', + required: true, + default: { + '@path': '$.properties.product_id' + } + }, + index: { + label: 'Index', + description: 'Name of the targeted search index.', + type: 'string', + required: true, + default: { + '@path': '$.properties.search_index' + } + }, + queryID: { + label: 'Query ID', + description: 'Query ID of the list on which the item was clicked.', + type: 'string', + required: true, + default: { + '@path': '$.properties.query_id' + } + }, + position: { + label: 'Position', + description: 'Position of the click in the list of Algolia search results.', + type: 'integer', + required: true, + default: { + '@path': '$.properties.position' + } + }, + userToken: { + type: 'string', + required: true, + description: 'The ID associated with the user.', + label: 'userToken', + default: { + '@if': { + exists: { '@path': '$.userId' }, + then: { '@path': '$.userId' }, + else: { '@path': '$.anonymousId' } + } + } + }, + timestamp: { + type: 'string', + required: false, + description: 'The timestamp of the event.', + label: 'timestamp', + default: { '@path': '$.timestamp' } + } + }, + defaultSubscription: 'type = "track" and event = "Product Clicked"', + perform: (request, data) => { + const insightEvent: AlgoliaProductClickedEvent = { + ...data.payload, + eventName: 'Product Clicked', + eventType: 'click', + objectIDs: [data.payload.objectID], + userToken: data.payload.userToken, + positions: [data.payload.position], + timestamp: data.payload.timestamp ? new Date(data.payload.timestamp).valueOf() : undefined + } + const insightPayload = { events: [insightEvent] } + + return request(AlgoliaBehaviourURL, { + method: 'post', + json: insightPayload + }) + } +} + +/** used in the quick setup */ +export const productClickPresets: Subscription = { + name: 'Send product clicked events to Algolia', + subscribe: productClickedEvents.defaultSubscription as string, + partnerAction: 'productClickedEvents', + mapping: defaultValues(productClickedEvents.fields) +} diff --git a/packages/destination-actions/src/destinations/algolia-insights/productViewedEvents/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/algolia-insights/productViewedEvents/__tests__/__snapshots__/snapshot.test.ts.snap new file mode 100644 index 0000000000..68935427f7 --- /dev/null +++ b/packages/destination-actions/src/destinations/algolia-insights/productViewedEvents/__tests__/__snapshots__/snapshot.test.ts.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Testing snapshot for AlgoliaInsights's productViewedEvents destination action: all fields 1`] = ` +Object { + "events": Array [ + Object { + "eventName": "Product Viewed", + "eventType": "view", + "index": "og&DCP)aINw@qxe)", + "objectID": "og&DCP)aINw@qxe)", + "objectIDs": Array [ + "og&DCP)aINw@qxe)", + ], + "queryID": "og&DCP)aINw@qxe)", + "timestamp": null, + "userToken": "og&DCP)aINw@qxe)", + }, + ], +} +`; + +exports[`Testing snapshot for AlgoliaInsights's productViewedEvents destination action: required fields 1`] = ` +Object { + "events": Array [ + Object { + "eventName": "Product Viewed", + "eventType": "view", + "index": "og&DCP)aINw@qxe)", + "objectID": "og&DCP)aINw@qxe)", + "objectIDs": Array [ + "og&DCP)aINw@qxe)", + ], + "queryID": "og&DCP)aINw@qxe)", + "timestamp": 1674843786677, + "userToken": "og&DCP)aINw@qxe)", + }, + ], +} +`; diff --git a/packages/destination-actions/src/destinations/algolia-insights/productViewedEvents/__tests__/index.test.ts b/packages/destination-actions/src/destinations/algolia-insights/productViewedEvents/__tests__/index.test.ts new file mode 100644 index 0000000000..10e19c4b92 --- /dev/null +++ b/packages/destination-actions/src/destinations/algolia-insights/productViewedEvents/__tests__/index.test.ts @@ -0,0 +1,65 @@ +import nock from 'nock' +import { createTestEvent, createTestIntegration, SegmentEvent } from '@segment/actions-core' +import Destination, { ALGOLIA_INSIGHTS_USER_AGENT } from '../../index' +import { AlgoliaProductViewedEvent, BaseAlgoliaInsightsURL } from '../../algolia-insight-api' + +const testDestination = createTestIntegration(Destination) + +const algoliaDestinationActionSettings = { + appId: 'algolia-application-id', + apiKey: 'algolia-api-key' +} +const testAlgoliaDestination = async (event: SegmentEvent): Promise => { + nock(BaseAlgoliaInsightsURL).post('/1/events').reply(200, {}) + const segmentEvent = { + event: { ...event }, + settings: algoliaDestinationActionSettings, + useDefaultMappings: true + } + const actionResponse = await testDestination.testAction('productViewedEvents', segmentEvent) + const actionRequest = actionResponse[0].request + + expect(actionResponse.length).toBe(1) + expect(actionRequest.headers.get('X-Algolia-Application-Id')).toBe(algoliaDestinationActionSettings.appId) + expect(actionRequest.headers.get('X-Algolia-API-Key')).toBe(algoliaDestinationActionSettings.apiKey) + expect(actionRequest.headers.get('X-Algolia-Agent')).toBe(ALGOLIA_INSIGHTS_USER_AGENT) + + const rawBody = await actionRequest.text() + return JSON.parse(rawBody)['events'][0] +} + +describe('AlgoliaInsights.productViewedEvents', () => { + it('should submit click on track "Product Viewed" event', async () => { + const event = createTestEvent({ + type: 'track', + event: 'Product Viewed', + properties: { + query_id: '1234', + search_index: 'fashion_1', + product_id: '9876' + } + }) + const algoliaEvent = await testAlgoliaDestination(event) + + expect(algoliaEvent.eventName).toBe('Product Viewed') + expect(algoliaEvent.eventType).toBe('view') + expect(algoliaEvent.index).toBe(event.properties?.search_index) + expect(algoliaEvent.userToken).toBe(event.userId) + expect(algoliaEvent.objectIDs).toContain('9876') + }) + + it('should pass timestamp if present', async () => { + const event = createTestEvent({ + type: 'track', + event: 'Product Viewed', + properties: { + query_id: '1234', + search_index: 'fashion_1', + product_id: '9876' + }, + userId: undefined + }) + const algoliaEvent = await testAlgoliaDestination(event) + expect(algoliaEvent.timestamp).toBe(new Date(event.timestamp as string).valueOf()) + }) +}) diff --git a/packages/destination-actions/src/destinations/algolia-insights/productViewedEvents/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/algolia-insights/productViewedEvents/__tests__/snapshot.test.ts new file mode 100644 index 0000000000..9a30bd4a10 --- /dev/null +++ b/packages/destination-actions/src/destinations/algolia-insights/productViewedEvents/__tests__/snapshot.test.ts @@ -0,0 +1,79 @@ +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import { generateTestData } from '../../../../lib/test-data' +import destination from '../../index' +import nock from 'nock' + +const testDestination = createTestIntegration(destination) +const actionSlug = 'productViewedEvents' +const destinationSlug = 'AlgoliaInsights' +const seedName = `${destinationSlug}#${actionSlug}` + +describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination action:`, () => { + it('required fields', async () => { + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, true) + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + timestamp: new Date('2023-01-27T18:23:06.677Z').toISOString(), + properties: eventData + }) + + const responses = await testDestination.testAction(actionSlug, { + useDefaultMappings: true, + event: event, + mapping: event.properties, + settings: settingsData, + auth: undefined + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + + expect(request.headers).toMatchSnapshot() + }) + + it('all fields', async () => { + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, false) + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + timestamp: new Date('2023-01-27T18:23:06.677Z').toISOString(), + properties: eventData + }) + + const responses = await testDestination.testAction(actionSlug, { + useDefaultMappings: true, + event: event, + mapping: event.properties, + settings: settingsData, + auth: undefined + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + }) +}) diff --git a/packages/destination-actions/src/destinations/algolia-insights/productViewedEvents/generated-types.ts b/packages/destination-actions/src/destinations/algolia-insights/productViewedEvents/generated-types.ts new file mode 100644 index 0000000000..0796e10400 --- /dev/null +++ b/packages/destination-actions/src/destinations/algolia-insights/productViewedEvents/generated-types.ts @@ -0,0 +1,24 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * Product ID of the clicked item. + */ + objectID: string + /** + * Name of the targeted search index. + */ + index: string + /** + * Query ID of the list on which the item was clicked. + */ + queryID: string + /** + * The ID associated with the user. + */ + userToken: string + /** + * The timestamp of the event. + */ + timestamp?: string +} diff --git a/packages/destination-actions/src/destinations/algolia-insights/productViewedEvents/index.ts b/packages/destination-actions/src/destinations/algolia-insights/productViewedEvents/index.ts new file mode 100644 index 0000000000..07105f9849 --- /dev/null +++ b/packages/destination-actions/src/destinations/algolia-insights/productViewedEvents/index.ts @@ -0,0 +1,84 @@ +import type { ActionDefinition } from '@segment/actions-core' +import { Subscription, defaultValues } from '@segment/actions-core' +import { AlgoliaBehaviourURL, AlgoliaProductViewedEvent } from '../algolia-insight-api' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' + +export const productViewedEvents: ActionDefinition = { + title: 'Product Viewed Events', + description: 'Product views which can be tied back to an Algolia Search, Recommend or Predict result', + fields: { + objectID: { + label: 'Product ID', + description: 'Product ID of the clicked item.', + type: 'string', + required: true, + default: { + '@path': '$.properties.product_id' + } + }, + index: { + label: 'Index', + description: 'Name of the targeted search index.', + type: 'string', + required: true, + default: { + '@path': '$.properties.search_index' + } + }, + queryID: { + label: 'Query ID', + description: 'Query ID of the list on which the item was clicked.', + type: 'string', + required: true, + default: { + '@path': '$.properties.query_id' + } + }, + userToken: { + type: 'string', + required: true, + description: 'The ID associated with the user.', + label: 'userToken', + default: { + '@if': { + exists: { '@path': '$.userId' }, + then: { '@path': '$.userId' }, + else: { '@path': '$.anonymousId' } + } + } + }, + timestamp: { + type: 'string', + required: false, + description: 'The timestamp of the event.', + label: 'timestamp', + default: { '@path': '$.timestamp' } + } + }, + defaultSubscription: 'type = "track" and event = "Product Viewed"', + perform: (request, data) => { + const insightEvent: AlgoliaProductViewedEvent = { + ...data.payload, + eventName: 'Product Viewed', + eventType: 'view', + objectIDs: [data.payload.objectID], + timestamp: data.payload.timestamp ? new Date(data.payload.timestamp).valueOf() : undefined, + userToken: data.payload.userToken + } + const insightPayload = { events: [insightEvent] } + + return request(AlgoliaBehaviourURL, { + method: 'post', + json: insightPayload + }) + } +} + +/** used in the quick setup */ +export const productViewedPresets: Subscription = { + name: 'Send product viewed events to Algolia', + subscribe: productViewedEvents.defaultSubscription as string, + partnerAction: 'productViewedEvents', + mapping: defaultValues(productViewedEvents.fields) +} diff --git a/packages/destination-actions/src/destinations/google-enhanced-conversions/__tests__/uploadCallConversion.test.ts b/packages/destination-actions/src/destinations/google-enhanced-conversions/__tests__/uploadCallConversion.test.ts index 82b203cda0..d1c60a03b5 100644 --- a/packages/destination-actions/src/destinations/google-enhanced-conversions/__tests__/uploadCallConversion.test.ts +++ b/packages/destination-actions/src/destinations/google-enhanced-conversions/__tests__/uploadCallConversion.test.ts @@ -95,6 +95,61 @@ describe('GoogleEnhancedConversions', () => { expect(responses[1].status).toBe(201) }) + it('uses v12 when google-enhanced-v12 flag is enabled', async () => { + const event = createTestEvent({ + timestamp, + event: 'Test Event', + properties: { + email: 'test@gmail.com', + orderId: '1234', + total: '200', + currency: 'USD' + } + }) + + nock(`https://googleads.googleapis.com/v12/customers/${customerId}/googleAds:searchStream`) + .post('') + .reply(200, [ + { + results: [ + { + conversionCustomVariable: { + resourceName: 'customers/1234/conversionCustomVariables/123445', + id: '123445', + name: 'username' + } + } + ] + } + ]) + + nock(`https://googleads.googleapis.com/v12/customers/${customerId}:uploadCallConversions`) + .post('') + .reply(201, { results: [{}] }) + + const responses = await testDestination.testAction('uploadCallConversion', { + event, + features: { 'google-enhanced-v12': true }, + mapping: { + conversion_action: '12345', + caller_id: '+1234567890', + call_timestamp: timestamp, + custom_variables: { username: 'spongebob' } + }, + useDefaultMappings: true, + settings: { + customerId + } + }) + + expect(responses[1].options.body).toMatchInlineSnapshot( + `"{\\"conversions\\":[{\\"conversionAction\\":\\"customers/1234/conversionActions/12345\\",\\"callerId\\":\\"+1234567890\\",\\"callStartDateTime\\":\\"2021-06-10 18:08:04+00:00\\",\\"conversionDateTime\\":\\"2021-06-10 18:08:04+00:00\\",\\"conversionValue\\":200,\\"currencyCode\\":\\"USD\\",\\"customVariables\\":[{\\"conversionCustomVariable\\":\\"customers/1234/conversionCustomVariables/123445\\",\\"value\\":\\"spongebob\\"}]}],\\"partialFailure\\":true}"` + ) + + expect(responses.length).toBe(2) + expect(responses[1].status).toBe(201) + }) + it('fails if customerId not set', async () => { const event = createTestEvent({ timestamp, @@ -123,5 +178,67 @@ describe('GoogleEnhancedConversions', () => { expect(e.message).toBe('Customer ID is required for this action. Please set it in destination settings.') } }) + it('sends an event with default mappings', async () => { + const event = createTestEvent({ + timestamp, + event: 'Test Event', + properties: { + email: 'test@gmail.com', + orderId: '1234', + total: '200', + currency: 'USD' + } + }) + + nock(`https://googleads.googleapis.com/v12/customers/${customerId}:uploadCallConversions`) + .post('') + .reply(201, { results: [{}] }) + + const responses = await testDestination.testAction('uploadCallConversion', { + event, + features: { 'google-enhanced-v12': true }, + mapping: { conversion_action: '12345', caller_id: '+1234567890', call_timestamp: timestamp }, + useDefaultMappings: true, + settings: { + customerId + } + }) + + expect(responses[0].options.body).toMatchInlineSnapshot( + `"{\\"conversions\\":[{\\"conversionAction\\":\\"customers/1234/conversionActions/12345\\",\\"callerId\\":\\"+1234567890\\",\\"callStartDateTime\\":\\"2021-06-10 18:08:04+00:00\\",\\"conversionDateTime\\":\\"2021-06-10 18:08:04+00:00\\",\\"conversionValue\\":200,\\"currencyCode\\":\\"USD\\"}],\\"partialFailure\\":true}"` + ) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(201) + }) + it('fails if customerId not set', async () => { + const event = createTestEvent({ + timestamp, + event: 'Test Event', + properties: { + email: 'test@gmail.com', + orderId: '1234', + total: '200', + currency: 'USD' + } + }) + + nock(`https://googleads.googleapis.com/v12/customers/${customerId}:uploadCallConversions`) + .post('') + .reply(201, { results: [{}] }) + + try { + await testDestination.testAction('uploadCallConversion', { + event, + features: { 'google-enhanced-v12': true }, + mapping: { conversion_action: '12345', caller_id: '+1234567890', call_timestamp: timestamp }, + useDefaultMappings: true, + settings: {} + }) + fail('the test should have thrown an error') + } catch (e) { + expect(e.message).toBe('Customer ID is required for this action. Please set it in destination settings.') + } + }) }) }) diff --git a/packages/destination-actions/src/destinations/google-enhanced-conversions/__tests__/uploadClickConversion.test.ts b/packages/destination-actions/src/destinations/google-enhanced-conversions/__tests__/uploadClickConversion.test.ts index 3bd5cfb071..64f2bb0c17 100644 --- a/packages/destination-actions/src/destinations/google-enhanced-conversions/__tests__/uploadClickConversion.test.ts +++ b/packages/destination-actions/src/destinations/google-enhanced-conversions/__tests__/uploadClickConversion.test.ts @@ -146,6 +146,62 @@ describe('GoogleEnhancedConversions', () => { expect(responses.length).toBe(2) expect(responses[1].status).toBe(201) }) + it('uses v12 when google-enhanced-v12 flag is enabled', async () => { + const event = createTestEvent({ + timestamp, + event: 'Test Event', + properties: { + gclid: '54321', + orderId: '1234', + total: '200', + currency: 'USD', + products: [ + { + product_id: '1234', + quantity: 3, + price: 10.99 + } + ] + } + }) + + nock(`https://googleads.googleapis.com/v12/customers/${customerId}/googleAds:searchStream`) + .post('') + .reply(200, [ + { + results: [ + { + conversionCustomVariable: { + resourceName: 'customers/1234/conversionCustomVariables/123445', + id: '123445', + name: 'username' + } + } + ] + } + ]) + + nock(`https://googleads.googleapis.com/v12/customers/${customerId}:uploadClickConversions`) + .post('') + .reply(201, { results: [{}] }) + + const responses = await testDestination.testAction('uploadClickConversion', { + event, + features: { 'google-enhanced-v12': true }, + mapping: { conversion_action: '12345', custom_variables: { username: 'spongebob' } }, + useDefaultMappings: true, + settings: { + customerId + } + }) + + expect(responses[1].options.body).toMatchInlineSnapshot( + `"{\\"conversions\\":[{\\"conversionAction\\":\\"customers/1234/conversionActions/12345\\",\\"conversionDateTime\\":\\"2021-06-10 18:08:04+00:00\\",\\"orderId\\":\\"1234\\",\\"conversionValue\\":200,\\"currencyCode\\":\\"USD\\",\\"cartData\\":{\\"items\\":[{\\"productId\\":\\"1234\\",\\"quantity\\":3,\\"unitPrice\\":10.99}]},\\"userIdentifiers\\":[],\\"customVariables\\":[{\\"conversionCustomVariable\\":\\"customers/1234/conversionCustomVariables/123445\\",\\"value\\":\\"spongebob\\"}]}],\\"partialFailure\\":true}"` + ) + + expect(responses.length).toBe(2) + expect(responses[1].status).toBe(201) + }) it('fails if customerId not set', async () => { const event = createTestEvent({ @@ -183,5 +239,126 @@ describe('GoogleEnhancedConversions', () => { expect(e.message).toBe('Customer ID is required for this action. Please set it in destination settings.') } }) + it('sends an event with default mappings', async () => { + const event = createTestEvent({ + timestamp, + event: 'Test Event', + properties: { + gclid: '54321', + email: 'test@gmail.com', + orderId: '1234', + total: '200', + currency: 'USD', + products: [ + { + product_id: '1234', + quantity: 3, + price: 10.99 + } + ] + } + }) + + nock(`https://googleads.googleapis.com/v12/customers/${customerId}:uploadClickConversions`) + .post('') + .reply(201, { results: [{}] }) + + const responses = await testDestination.testAction('uploadClickConversion', { + event, + features: { 'google-enhanced-v12': true }, + mapping: { conversion_action: '12345' }, + useDefaultMappings: true, + settings: { + customerId + } + }) + + expect(responses[0].options.body).toMatchInlineSnapshot( + `"{\\"conversions\\":[{\\"conversionAction\\":\\"customers/1234/conversionActions/12345\\",\\"conversionDateTime\\":\\"2021-06-10 18:08:04+00:00\\",\\"orderId\\":\\"1234\\",\\"conversionValue\\":200,\\"currencyCode\\":\\"USD\\",\\"cartData\\":{\\"items\\":[{\\"productId\\":\\"1234\\",\\"quantity\\":3,\\"unitPrice\\":10.99}]},\\"userIdentifiers\\":[{\\"hashedEmail\\":\\"87924606b4131a8aceeeae8868531fbb9712aaa07a5d3a756b26ce0f5d6ca674\\"}]}],\\"partialFailure\\":true}"` + ) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(201) + }) + + it('sends email and phone user_identifiers', async () => { + const event = createTestEvent({ + timestamp, + event: 'Test Event', + properties: { + gclid: '54321', + email: 'test@gmail.com', + phone: '6161729102', + orderId: '1234', + total: '200', + currency: 'USD', + products: [ + { + product_id: '1234', + quantity: 3, + price: 10.99 + } + ] + } + }) + + nock(`https://googleads.googleapis.com/v12/customers/${customerId}:uploadClickConversions`) + .post('') + .reply(201, { results: [{}] }) + + const responses = await testDestination.testAction('uploadClickConversion', { + event, + features: { 'google-enhanced-v12': true }, + mapping: { conversion_action: '12345' }, + useDefaultMappings: true, + settings: { + customerId + } + }) + + expect(responses[0].options.body).toMatchInlineSnapshot( + `"{\\"conversions\\":[{\\"conversionAction\\":\\"customers/1234/conversionActions/12345\\",\\"conversionDateTime\\":\\"2021-06-10 18:08:04+00:00\\",\\"orderId\\":\\"1234\\",\\"conversionValue\\":200,\\"currencyCode\\":\\"USD\\",\\"cartData\\":{\\"items\\":[{\\"productId\\":\\"1234\\",\\"quantity\\":3,\\"unitPrice\\":10.99}]},\\"userIdentifiers\\":[{\\"hashedEmail\\":\\"87924606b4131a8aceeeae8868531fbb9712aaa07a5d3a756b26ce0f5d6ca674\\"},{\\"hashedPhoneNumber\\":\\"1dba01a96da19f6df771cff07e0a8d822126709b82ae7adc6a3839b3aaa68a16\\"}]}],\\"partialFailure\\":true}"` + ) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(201) + }) + it('fails if customerId not set', async () => { + const event = createTestEvent({ + timestamp, + event: 'Test Event', + properties: { + gclid: '54321', + email: 'test@gmail.com', + orderId: '1234', + total: '200', + currency: 'USD', + products: [ + { + product_id: '1234', + quantity: 3, + price: 10.99 + } + ] + } + }) + + nock(`https://googleads.googleapis.com/v12/customers/${customerId}:uploadClickConversions`) + .post('') + .reply(201, {}) + + try { + await testDestination.testAction('uploadClickConversion', { + event, + features: { 'google-enhanced-v12': true }, + mapping: { conversion_action: '12345' }, + useDefaultMappings: true, + settings: {} + }) + fail('the test should have thrown an error') + } catch (e) { + expect(e.message).toBe('Customer ID is required for this action. Please set it in destination settings.') + } + }) }) }) diff --git a/packages/destination-actions/src/destinations/google-enhanced-conversions/__tests__/uploadConversionAdjustment.test.ts b/packages/destination-actions/src/destinations/google-enhanced-conversions/__tests__/uploadConversionAdjustment.test.ts index 88fc1b53ee..63ea28f2a2 100644 --- a/packages/destination-actions/src/destinations/google-enhanced-conversions/__tests__/uploadConversionAdjustment.test.ts +++ b/packages/destination-actions/src/destinations/google-enhanced-conversions/__tests__/uploadConversionAdjustment.test.ts @@ -172,6 +172,177 @@ describe('GoogleEnhancedConversions', () => { `"{\\"conversionAdjustments\\":[{\\"conversionAction\\":\\"customers/1234/conversionActions/12345\\",\\"adjustmentType\\":\\"RETRACTION\\",\\"adjustmentDateTime\\":\\"2021-06-10 18:08:04+00:00\\",\\"gclidDateTimePair\\":{\\"gclid\\":\\"123a\\",\\"conversionDateTime\\":\\"2021-06-10 18:08:04+00:00\\"},\\"userIdentifiers\\":[{\\"hashedEmail\\":\\"87924606b4131a8aceeeae8868531fbb9712aaa07a5d3a756b26ce0f5d6ca674\\"},{\\"hashedPhoneNumber\\":\\"c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646\\"}],\\"userAgent\\":\\"Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1\\"}],\\"partialFailure\\":true}"` ) + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(201) + }) + it('uses v12 when google-enhanced-v12 flag is enabled', async () => { + const event = createTestEvent({ + timestamp, + event: 'Test Event', + properties: { + gclid: '54321', + email: 'test@gmail.com', + orderId: '1234', + phone: '1234567890', + firstName: 'Jane', + lastName: 'Doe', + currency: 'USD', + value: '123', + address: { + street: '123 Street SW', + city: 'San Diego', + state: 'CA', + postalCode: '982004' + } + } + }) + + nock(`https://googleads.googleapis.com/v12/customers/${customerId}:uploadConversionAdjustments`) + .post('') + .reply(201, { results: [{}] }) + + const responses = await testDestination.testAction('uploadConversionAdjustment', { + event, + features: { 'google-enhanced-v12': true }, + mapping: { + gclid: { + '@path': '$.properties.gclid' + }, + conversion_action: '12345', + adjustment_type: 'ENHANCEMENT', + conversion_timestamp: { + '@path': '$.timestamp' + }, + restatement_value: { + '@path': '$.properties.value' + }, + restatement_currency_code: { + '@path': '$.properties.currency' + } + }, + useDefaultMappings: true, + settings: { + customerId + } + }) + + expect(responses[0].options.body).toMatchInlineSnapshot( + `"{\\"conversionAdjustments\\":[{\\"conversionAction\\":\\"customers/1234/conversionActions/12345\\",\\"adjustmentType\\":\\"ENHANCEMENT\\",\\"adjustmentDateTime\\":\\"2021-06-10 18:08:04+00:00\\",\\"orderId\\":\\"1234\\",\\"gclidDateTimePair\\":{\\"gclid\\":\\"54321\\",\\"conversionDateTime\\":\\"2021-06-10 18:08:04+00:00\\"},\\"userIdentifiers\\":[{\\"hashedEmail\\":\\"87924606b4131a8aceeeae8868531fbb9712aaa07a5d3a756b26ce0f5d6ca674\\"},{\\"hashedPhoneNumber\\":\\"c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646\\"},{\\"addressInfo\\":{\\"hashedFirstName\\":\\"4f23798d92708359b734a18172c9c864f1d48044a754115a0d4b843bca3a5332\\",\\"hashedLastName\\":\\"fd53ef835b15485572a6e82cf470dcb41fd218ae5751ab7531c956a2a6bcd3c7\\"}}],\\"userAgent\\":\\"Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1\\",\\"restatementValue\\":{\\"adjustedValue\\":123,\\"currencyCode\\":\\"USD\\"}}],\\"partialFailure\\":true}"` + ) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(201) + }) + + it('fails if customerId not set', async () => { + const event = createTestEvent({ + timestamp, + event: 'Test Event', + properties: { + gclid: '54321', + email: 'test@gmail.com', + orderId: '1234' + } + }) + + nock(`https://googleads.googleapis.com/v12/customers/${customerId}:uploadConversionAdjustments`) + .post('') + .reply(201, { results: [{}] }) + + try { + await testDestination.testAction('uploadConversionAdjustment', { + event, + features: { 'google-enhanced-v12': true }, + mapping: { gclid: '123a', conversion_action: '12345', adjustment_type: 'ENHANCEMENT' }, + useDefaultMappings: true, + settings: {} + }) + fail('the test should have thrown an error') + } catch (e) { + expect(e.message).toBe('Customer ID is required for this action. Please set it in destination settings.') + } + }) + + it('sends restatement_value for restatements', async () => { + const event = createTestEvent({ + timestamp, + event: 'Test Event', + properties: { + email: 'test@gmail.com', + phone: '1234567890', + value: '123', + currency: 'USD' + } + }) + + nock(`https://googleads.googleapis.com/v12/customers/${customerId}:uploadConversionAdjustments`) + .post('') + .reply(201, { results: [{}] }) + + const responses = await testDestination.testAction('uploadConversionAdjustment', { + event, + features: { 'google-enhanced-v12': true }, + mapping: { + gclid: '123a', + conversion_action: '12345', + adjustment_type: 'RESTATEMENT', + conversion_timestamp: { + '@path': '$.timestamp' + }, + restatement_value: { + '@path': '$.properties.value' + }, + restatement_currency_code: { + '@path': '$.properties.currency' + } + }, + useDefaultMappings: true, + settings: { + customerId + } + }) + + expect(responses[0].options.body).toMatchInlineSnapshot( + `"{\\"conversionAdjustments\\":[{\\"conversionAction\\":\\"customers/1234/conversionActions/12345\\",\\"adjustmentType\\":\\"RESTATEMENT\\",\\"adjustmentDateTime\\":\\"2021-06-10 18:08:04+00:00\\",\\"gclidDateTimePair\\":{\\"gclid\\":\\"123a\\",\\"conversionDateTime\\":\\"2021-06-10 18:08:04+00:00\\"},\\"userIdentifiers\\":[{\\"hashedEmail\\":\\"87924606b4131a8aceeeae8868531fbb9712aaa07a5d3a756b26ce0f5d6ca674\\"},{\\"hashedPhoneNumber\\":\\"c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646\\"}],\\"userAgent\\":\\"Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1\\",\\"restatementValue\\":{\\"adjustedValue\\":123,\\"currencyCode\\":\\"USD\\"}}],\\"partialFailure\\":true}"` + ) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(201) + }) + + it('does not send restatement_value for retractions', async () => { + const event = createTestEvent({ + timestamp, + event: 'Test Event', + properties: { + email: 'test@gmail.com', + phone: '1234567890' + } + }) + + nock(`https://googleads.googleapis.com/v12/customers/${customerId}:uploadConversionAdjustments`) + .post('') + .reply(201, { results: [{}] }) + + const responses = await testDestination.testAction('uploadConversionAdjustment', { + event, + features: { 'google-enhanced-v12': true }, + mapping: { + gclid: '123a', + conversion_action: '12345', + adjustment_type: 'RETRACTION', + conversion_timestamp: timestamp + }, + useDefaultMappings: true, + settings: { + customerId + } + }) + + expect(responses[0].options.body).toMatchInlineSnapshot( + `"{\\"conversionAdjustments\\":[{\\"conversionAction\\":\\"customers/1234/conversionActions/12345\\",\\"adjustmentType\\":\\"RETRACTION\\",\\"adjustmentDateTime\\":\\"2021-06-10 18:08:04+00:00\\",\\"gclidDateTimePair\\":{\\"gclid\\":\\"123a\\",\\"conversionDateTime\\":\\"2021-06-10 18:08:04+00:00\\"},\\"userIdentifiers\\":[{\\"hashedEmail\\":\\"87924606b4131a8aceeeae8868531fbb9712aaa07a5d3a756b26ce0f5d6ca674\\"},{\\"hashedPhoneNumber\\":\\"c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646\\"}],\\"userAgent\\":\\"Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1\\"}],\\"partialFailure\\":true}"` + ) + expect(responses.length).toBe(1) expect(responses[0].status).toBe(201) }) diff --git a/packages/destination-actions/src/destinations/google-enhanced-conversions/functions.ts b/packages/destination-actions/src/destinations/google-enhanced-conversions/functions.ts index 57673d31cf..4fd319d79f 100644 --- a/packages/destination-actions/src/destinations/google-enhanced-conversions/functions.ts +++ b/packages/destination-actions/src/destinations/google-enhanced-conversions/functions.ts @@ -1,7 +1,8 @@ import { createHash } from 'crypto' import { ConversionCustomVariable, PartialErrorResponse, QueryResponse } from './types' import { ModifiedResponse, RequestClient, IntegrationError } from '@segment/actions-core' -import { GoogleAdsAPI } from './types' +import { Features } from '@segment/actions-core/src/mapping-kit' +import { StatsContext } from '@segment/actions-core/src/destination-kit' export function formatCustomVariables( customVariables: object, @@ -41,9 +42,11 @@ export const hash = (value: string | undefined): string | undefined => { export async function getCustomVariables( customerId: string, auth: any, - request: RequestClient + request: RequestClient, + features?: Features, + statsContext?: StatsContext ): Promise> { - return await request(`${GoogleAdsAPI}/${customerId}/googleAds:searchStream`, { + return await request(`${getUrlByVersion(features, statsContext)}/${customerId}/googleAds:searchStream`, { method: 'post', headers: { authorization: `Bearer ${auth?.accessToken}`, @@ -70,3 +73,14 @@ export function convertTimestamp(timestamp: string | undefined): string | undefi } return timestamp.replace(/T/, ' ').replace(/\..+/, '+00:00') } + +// Ticket to remove flagon - https://segment.atlassian.net/browse/STRATCONN-1953 +export function getUrlByVersion(features?: Features, statsContext?: StatsContext): string { + const statsClient = statsContext?.statsClient + const tags = statsContext?.tags + + const API_VERSION = features && features['google-enhanced-v12'] ? 'v12' : 'v11' + tags?.push(`version:${API_VERSION}`) + statsClient?.incr(`google_enhanced_conversions`, 1, tags) + return `https://googleads.googleapis.com/${API_VERSION}/customers` +} diff --git a/packages/destination-actions/src/destinations/google-enhanced-conversions/types.ts b/packages/destination-actions/src/destinations/google-enhanced-conversions/types.ts index 286a62e267..e052b13509 100644 --- a/packages/destination-actions/src/destinations/google-enhanced-conversions/types.ts +++ b/packages/destination-actions/src/destinations/google-enhanced-conversions/types.ts @@ -23,5 +23,3 @@ export interface PartialErrorResponse { } results: {}[] } - -export const GoogleAdsAPI = 'https://googleads.googleapis.com/v11/customers' diff --git a/packages/destination-actions/src/destinations/google-enhanced-conversions/uploadCallConversion/index.ts b/packages/destination-actions/src/destinations/google-enhanced-conversions/uploadCallConversion/index.ts index 123c296b83..332dc5a7e6 100644 --- a/packages/destination-actions/src/destinations/google-enhanced-conversions/uploadCallConversion/index.ts +++ b/packages/destination-actions/src/destinations/google-enhanced-conversions/uploadCallConversion/index.ts @@ -1,8 +1,14 @@ import { ActionDefinition, IntegrationError } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { convertTimestamp, formatCustomVariables, getCustomVariables, handleGoogleErrors } from '../functions' -import { GoogleAdsAPI, PartialErrorResponse } from '../types' +import { + convertTimestamp, + formatCustomVariables, + getCustomVariables, + getUrlByVersion, + handleGoogleErrors +} from '../functions' +import { PartialErrorResponse } from '../types' import { ModifiedResponse } from '@segment/actions-core' const action: ActionDefinition = { @@ -65,8 +71,8 @@ const action: ActionDefinition = { defaultObjectUI: 'keyvalue:only' } }, - perform: async (request, { auth, settings, payload }) => { - /* Enforcing this here since Customer ID is required for the Google Ads API + perform: async (request, { auth, settings, payload, features, statsContext }) => { + /* Enforcing this here since Customer ID is required for the Google Ads API but not for the Enhanced Conversions API. */ if (!settings.customerId) { throw new IntegrationError( @@ -89,15 +95,14 @@ const action: ActionDefinition = { // Retrieves all of the custom variables that the customer has created in their Google Ads account if (payload.custom_variables) { - const customVariableIds = await getCustomVariables(settings.customerId, auth, request) + const customVariableIds = await getCustomVariables(settings.customerId, auth, request, features, statsContext) request_object.customVariables = formatCustomVariables( payload.custom_variables, customVariableIds.data[0].results ) } - const response: ModifiedResponse = await request( - `${GoogleAdsAPI}/${settings.customerId}:uploadCallConversions`, + `${getUrlByVersion(features, statsContext)}/${settings.customerId}:uploadCallConversions`, { method: 'post', headers: { diff --git a/packages/destination-actions/src/destinations/google-enhanced-conversions/uploadClickConversion/index.ts b/packages/destination-actions/src/destinations/google-enhanced-conversions/uploadClickConversion/index.ts index e7376dc108..e882060be3 100644 --- a/packages/destination-actions/src/destinations/google-enhanced-conversions/uploadClickConversion/index.ts +++ b/packages/destination-actions/src/destinations/google-enhanced-conversions/uploadClickConversion/index.ts @@ -1,8 +1,15 @@ import { ActionDefinition, IntegrationError } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { CartItem, GoogleAdsAPI, PartialErrorResponse } from '../types' -import { formatCustomVariables, hash, getCustomVariables, handleGoogleErrors, convertTimestamp } from '../functions' +import { CartItem, PartialErrorResponse } from '../types' +import { + formatCustomVariables, + hash, + getCustomVariables, + handleGoogleErrors, + convertTimestamp, + getUrlByVersion +} from '../functions' import { ModifiedResponse } from '@segment/actions-core' const action: ActionDefinition = { @@ -179,8 +186,8 @@ const action: ActionDefinition = { defaultObjectUI: 'keyvalue:only' } }, - perform: async (request, { auth, settings, payload }) => { - /* Enforcing this here since Customer ID is required for the Google Ads API + perform: async (request, { auth, settings, payload, features, statsContext }) => { + /* Enforcing this here since Customer ID is required for the Google Ads API but not for the Enhanced Conversions API. */ if (!settings.customerId) { throw new IntegrationError( @@ -224,7 +231,7 @@ const action: ActionDefinition = { // Retrieves all of the custom variables that the customer has created in their Google Ads account if (payload.custom_variables) { - const customVariableIds = await getCustomVariables(settings.customerId, auth, request) + const customVariableIds = await getCustomVariables(settings.customerId, auth, request, features, statsContext) request_object.customVariables = formatCustomVariables( payload.custom_variables, customVariableIds.data[0].results @@ -240,7 +247,7 @@ const action: ActionDefinition = { } const response: ModifiedResponse = await request( - `${GoogleAdsAPI}/${settings.customerId}:uploadClickConversions`, + `${getUrlByVersion(features, statsContext)}/${settings.customerId}:uploadClickConversions`, { method: 'post', headers: { diff --git a/packages/destination-actions/src/destinations/google-enhanced-conversions/uploadConversionAdjustment/index.ts b/packages/destination-actions/src/destinations/google-enhanced-conversions/uploadConversionAdjustment/index.ts index 22e61b9ac5..2addf4b0af 100644 --- a/packages/destination-actions/src/destinations/google-enhanced-conversions/uploadConversionAdjustment/index.ts +++ b/packages/destination-actions/src/destinations/google-enhanced-conversions/uploadConversionAdjustment/index.ts @@ -1,8 +1,8 @@ import { ActionDefinition, IntegrationError } from '@segment/actions-core' -import { hash, handleGoogleErrors, convertTimestamp } from '../functions' +import { hash, handleGoogleErrors, convertTimestamp, getUrlByVersion } from '../functions' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { GoogleAdsAPI, PartialErrorResponse } from '../types' +import { PartialErrorResponse } from '../types' import { ModifiedResponse } from '@segment/actions-core' const action: ActionDefinition = { @@ -198,8 +198,8 @@ const action: ActionDefinition = { } } }, - perform: async (request, { settings, payload }) => { - /* Enforcing this here since Customer ID is required for the Google Ads API + perform: async (request, { settings, payload, features, statsContext }) => { + /* Enforcing this here since Customer ID is required for the Google Ads API but not for the Enhanced Conversions API. */ if (!settings.customerId) { throw new IntegrationError( @@ -268,7 +268,7 @@ const action: ActionDefinition = { } const response: ModifiedResponse = await request( - `${GoogleAdsAPI}/${settings.customerId}:uploadConversionAdjustments`, + `${getUrlByVersion(features, statsContext)}/${settings.customerId}:uploadConversionAdjustments`, { method: 'post', headers: { diff --git a/packages/destination-actions/src/destinations/heap/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/heap/__tests__/__snapshots__/snapshot.test.ts.snap index 99315202ab..b2e51ebc2e 100644 --- a/packages/destination-actions/src/destinations/heap/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/heap/__tests__/__snapshots__/snapshot.test.ts.snap @@ -19,27 +19,42 @@ Object { exports[`Testing snapshot for actions-heap destination: trackEvent action - all fields 1`] = ` Object { "app_id": "kFJwrNh$DP38FB", - "event": "track", - "idempotency_key": "kFJwrNh$DP38FB", - "identity": "kFJwrNh$DP38FB", - "properties": Object { - "segment_library": "cloud-mode-destination", - "testType": "kFJwrNh$DP38FB", - }, - "session_id": "kFJwrNh$DP38FB", - "timestamp": "2021-02-01T00:00:00.000Z", + "events": Array [ + Object { + "custom_properties": Object { + "testType": "kFJwrNh$DP38FB", + }, + "event": "track", + "idempotency_key": "kFJwrNh$DP38FB", + "properties": Object { + "segment_library": "cloud-mode-destination", + }, + "timestamp": "2021-02-01T00:00:00.000Z", + "user_identifier": Object { + "identity": "kFJwrNh$DP38FB", + }, + }, + ], + "library": "Segment", } `; exports[`Testing snapshot for actions-heap destination: trackEvent action - required fields 1`] = ` Object { "app_id": "kFJwrNh$DP38FB", - "event": "track", - "idempotency_key": "kFJwrNh$DP38FB", - "identity": "kFJwrNh$DP38FB", - "properties": Object { - "segment_library": "cloud-mode-destination", - }, - "session_id": "kFJwrNh$DP38FB", + "events": Array [ + Object { + "custom_properties": Object {}, + "event": "track", + "idempotency_key": "kFJwrNh$DP38FB", + "properties": Object { + "segment_library": "cloud-mode-destination", + }, + "user_identifier": Object { + "identity": "kFJwrNh$DP38FB", + }, + }, + ], + "library": "Segment", } `; diff --git a/packages/destination-actions/src/destinations/heap/__tests__/flat.test.ts b/packages/destination-actions/src/destinations/heap/__tests__/flat.test.ts index 7fecc0d316..f83ff76be5 100644 --- a/packages/destination-actions/src/destinations/heap/__tests__/flat.test.ts +++ b/packages/destination-actions/src/destinations/heap/__tests__/flat.test.ts @@ -28,27 +28,27 @@ export const embededObject = () => ({ }) export const flattenObject = () => ({ + firstName: 'John', + middleName: '', + lastName: 'Green', 'car.make': 'Honda', 'car.model': 'Civic', - 'car.revisions.0.changes': 0, + 'car.revisions.0.miles': '10150', 'car.revisions.0.code': 'REV01', - 'car.revisions.0.miles': 10150, - 'car.revisions.0.firstTime': true, - 'car.revisions.1.changes.0.desc': 'Left tire cap', - 'car.revisions.1.changes.0.price': 123.45, + 'car.revisions.0.changes': '0', + 'car.revisions.0.firstTime': 'true', + 'car.revisions.1.miles': '20021', + 'car.revisions.1.code': 'REV02', + 'car.revisions.1.firstTime': 'false', 'car.revisions.1.changes.0.type': 'asthetic', - 'car.revisions.1.changes.1.desc': 'Engine pressure regulator', - 'car.revisions.1.changes.1.engineer': null, + 'car.revisions.1.changes.0.desc': 'Left tire cap', + 'car.revisions.1.changes.0.price': '123.45', 'car.revisions.1.changes.1.type': 'mechanic', - 'car.revisions.1.firstTime': false, - 'car.revisions.1.code': 'REV02', - 'car.revisions.1.miles': 20021, - firstName: 'John', - lastName: 'Green', - middleName: '', + 'car.revisions.1.changes.1.desc': 'Engine pressure regulator', + 'car.revisions.1.changes.1.engineer': 'null', 'visits.0.date': '2015-01-01', 'visits.0.dealer': 'DEAL-001', - 'visits.0.useCoupon': true, + 'visits.0.useCoupon': 'true', 'visits.1.date': '2015-03-01', 'visits.1.dealer': 'DEAL-002' }) @@ -56,15 +56,15 @@ export const flattenObject = () => ({ describe('flattenObj for ', () => { describe('a flat kvp where the value is a ', () => { it('undefined', () => { - expect(flat({ myUndefined: undefined } as Properties)).toEqual({}) + expect(flat({ myUndefined: undefined } as Properties)).toEqual({ myUndefined: undefined }) }) it('null', () => { - expect(flat({ myNull: null })).toEqual({ myNull: null }) + expect(flat({ myNull: null })).toEqual({ myNull: 'null' }) }) it('number', () => { - expect(flat({ myNumber: 1 })).toEqual({ myNumber: 1 }) + expect(flat({ myNumber: 1 })).toEqual({ myNumber: '1' }) }) it('string', () => { @@ -72,26 +72,26 @@ describe('flattenObj for ', () => { }) it('boolean', () => { - expect(flat({ myBool: true })).toEqual({ myBool: true }) + expect(flat({ myBool: true })).toEqual({ myBool: 'true' }) }) }) describe('array of ', () => { it('nulls:', () => { expect(flat({ myNulls: [null, 1, null, 3] })).toEqual({ - 'myNulls.0': null, - 'myNulls.1': 1, - 'myNulls.2': null, - 'myNulls.3': 3 + 'myNulls.0': 'null', + 'myNulls.1': '1', + 'myNulls.2': 'null', + 'myNulls.3': '3' }) }) it('numbers:', () => { expect(flat({ myNumbers: [1, 2, 3, 4] })).toEqual({ - 'myNumbers.0': 1, - 'myNumbers.1': 2, - 'myNumbers.2': 3, - 'myNumbers.3': 4 + 'myNumbers.0': '1', + 'myNumbers.1': '2', + 'myNumbers.2': '3', + 'myNumbers.3': '4' }) }) @@ -106,16 +106,18 @@ describe('flattenObj for ', () => { it('booleans:', () => { expect(flat({ myBools: [true, false, true, false] })).toEqual({ - 'myBools.0': true, - 'myBools.1': false, - 'myBools.2': true, - 'myBools.3': false + 'myBools.0': 'true', + 'myBools.1': 'false', + 'myBools.2': 'true', + 'myBools.3': 'false' }) }) }) it('Embedded object', () => { - expect(flat(embededObject())).toEqual(flattenObject()) + const props = embededObject() + delete props.car.year + expect(flat(props)).toEqual(flattenObject()) }) it('primitive', () => { diff --git a/packages/destination-actions/src/destinations/heap/flat.ts b/packages/destination-actions/src/destinations/heap/flat.ts index 9b43a7f1a9..ca8726bef3 100644 --- a/packages/destination-actions/src/destinations/heap/flat.ts +++ b/packages/destination-actions/src/destinations/heap/flat.ts @@ -1,11 +1,9 @@ -import { JSONValue } from '@segment/actions-core' - export type Properties = { [k: string]: unknown } type FlattenProperties = object & { - [k: string]: JSONValue + [k: string]: string } export function flat(data: Properties, prefix = ''): FlattenProperties { @@ -15,8 +13,19 @@ export function flat(data: Properties, prefix = ''): FlattenProperties { const flatten = flat(data[key] as Properties, prefix + '.' + key) result = { ...result, ...flatten } } else { - result[(prefix + '.' + key).replace(/^\./, '')] = data[key] as JSONValue + const stringifiedValue = stringify(data[key]) + result[(prefix + '.' + key).replace(/^\./, '')] = stringifiedValue } } return result } + +function stringify(value: unknown): string { + if (typeof value === 'string') { + return value + } + if (typeof value === 'number' || typeof value === 'boolean') { + return value.toString() + } + return JSON.stringify(value) +} diff --git a/packages/destination-actions/src/destinations/heap/heapUtils.ts b/packages/destination-actions/src/destinations/heap/heapUtils.ts new file mode 100644 index 0000000000..d700fd429b --- /dev/null +++ b/packages/destination-actions/src/destinations/heap/heapUtils.ts @@ -0,0 +1,46 @@ +import { IntegrationError } from '@segment/actions-core' +import { Payload } from './trackEvent/generated-types' + +export const getUserIdentifier = ({ + identity, + anonymous_id +}: { + identity?: string | null + anonymous_id?: string | null +}): { [k: string]: string } => { + if (identity) { + return { + identity + } + } + + if (anonymous_id) { + return { + anonymous_id + } + } + throw new IntegrationError('Either identity or anonymous id are required.') +} + +export const getEventName = (payload: Payload) => { + let eventName: string | undefined + switch (payload.type) { + case 'track': + eventName = payload.event + break + case 'page': + eventName = payload.name ? `${payload.name} page viewed` : 'Page viewed' + break + case 'screen': + eventName = payload.name ? `${payload.name} screen viewed` : 'Screen viewed' + break + default: + eventName = 'track' + break + } + + if (!eventName) { + return 'track' + } + return eventName +} diff --git a/packages/destination-actions/src/destinations/heap/trackEvent/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/heap/trackEvent/__tests__/__snapshots__/snapshot.test.ts.snap index 6dfded4e61..6167382963 100644 --- a/packages/destination-actions/src/destinations/heap/trackEvent/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/heap/trackEvent/__tests__/__snapshots__/snapshot.test.ts.snap @@ -3,27 +3,42 @@ exports[`Testing snapshot for Heap's trackEvent destination action: all fields 1`] = ` Object { "app_id": "xqYHVWXiU0In", - "event": "track", - "idempotency_key": "xqYHVWXiU0In", - "identity": "xqYHVWXiU0In", - "properties": Object { - "segment_library": "cloud-mode-destination", - "testType": "xqYHVWXiU0In", - }, - "session_id": "xqYHVWXiU0In", - "timestamp": "2021-02-01T00:00:00.000Z", + "events": Array [ + Object { + "custom_properties": Object { + "testType": "xqYHVWXiU0In", + }, + "event": "track", + "idempotency_key": "xqYHVWXiU0In", + "properties": Object { + "segment_library": "cloud-mode-destination", + }, + "timestamp": "2021-02-01T00:00:00.000Z", + "user_identifier": Object { + "identity": "xqYHVWXiU0In", + }, + }, + ], + "library": "Segment", } `; exports[`Testing snapshot for Heap's trackEvent destination action: required fields 1`] = ` Object { "app_id": "xqYHVWXiU0In", - "event": "track", - "idempotency_key": "xqYHVWXiU0In", - "identity": "xqYHVWXiU0In", - "properties": Object { - "segment_library": "cloud-mode-destination", - }, - "session_id": "xqYHVWXiU0In", + "events": Array [ + Object { + "custom_properties": Object {}, + "event": "track", + "idempotency_key": "xqYHVWXiU0In", + "properties": Object { + "segment_library": "cloud-mode-destination", + }, + "user_identifier": Object { + "identity": "xqYHVWXiU0In", + }, + }, + ], + "library": "Segment", } `; diff --git a/packages/destination-actions/src/destinations/heap/trackEvent/__tests__/index.test.ts b/packages/destination-actions/src/destinations/heap/trackEvent/__tests__/index.test.ts index e42bdac598..57da0f1366 100644 --- a/packages/destination-actions/src/destinations/heap/trackEvent/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/heap/trackEvent/__tests__/index.test.ts @@ -11,26 +11,42 @@ describe('Heap.trackEvent', () => { const userId = 'foo@example.org' const messageId = '123' const eventName = 'Test Event' - let body: { - app_id: string + const heapURL = 'https://heapanalytics.com' + const integrationsTrackURI = '/api/integrations/track' + type heapEvent = { event: string idempotency_key: string - properties: { + timestamp: string + properties?: { [k: string]: string | null | boolean | number } - timestamp: string - identity?: string - user_id?: number + custom_properties?: { + [k: string]: string | null | boolean | number + } + user_identifier?: { + [k: string]: string + } + } + let body: { + app_id: string + events: Array + library: string } beforeEach(() => { body = { app_id: HEAP_TEST_APP_ID, - event: eventName, - idempotency_key: messageId, - properties: { - segment_library: HEAP_SEGMENT_CLOUD_LIBRARY_NAME - }, - timestamp + events: [ + { + event: eventName, + custom_properties: {}, + properties: { + segment_library: HEAP_SEGMENT_CLOUD_LIBRARY_NAME + }, + idempotency_key: messageId, + timestamp + } + ], + library: 'Segment' } }) @@ -45,14 +61,18 @@ describe('Heap.trackEvent', () => { }) it('should validate action fields for identified users', async () => { + const anonId = '5a41f0df-b69a-4a99-b656-79506a86c3f8' const event: Partial = createTestEvent({ timestamp, event: eventName, userId, - messageId + messageId, + anonymousId: anonId }) - body.identity = userId - nock('https://heapanalytics.com').post('/api/track', body).reply(200, {}) + body.events[0].user_identifier = { + anonymous_id: anonId + } + nock(heapURL).post(integrationsTrackURI, body).reply(200, {}) const responses = await testDestination.testAction('trackEvent', { event, @@ -68,6 +88,7 @@ describe('Heap.trackEvent', () => { it('should validate action fields for anonymous users', async () => { const testTimestampValue = '2021-08-17T15:21:15.449Z' + const anonId = '5a41f0df-b69a-4a99-b656-79506a86c3f8' const event = createTestEvent({ timestamp: testTimestampValue, event: 'Test Event', @@ -76,9 +97,11 @@ describe('Heap.trackEvent', () => { messageId: '123' }) - body.user_id = 8325872782136936 + body.events[0].user_identifier = { + anonymous_id: anonId + } - nock('https://heapanalytics.com').post('/api/track', body).reply(200, {}) + nock(heapURL).post(integrationsTrackURI, body).reply(200, {}) const responses = await testDestination.testAction('trackEvent', { event, @@ -97,21 +120,23 @@ describe('Heap.trackEvent', () => { const properties = embededObject() as unknown as { [k: string]: JSONValue } + const anonId = '5a41f0df-b69a-4a99-b656-79506a86c3f8' const event = createTestEvent({ timestamp: testTimestampValue, event: 'Test Event', - anonymousId: '5a41f0df-b69a-4a99-b656-79506a86c3f8', + anonymousId: anonId, userId: null, messageId: '123', properties }) - body.user_id = 8325872782136936 - body.properties = { - segment_library: HEAP_SEGMENT_CLOUD_LIBRARY_NAME, + body.events[0].user_identifier = { + anonymous_id: anonId + } + body.events[0].custom_properties = { ...flattenObject() } - nock('https://heapanalytics.com').post('/api/track', body).reply(200, {}) + nock(heapURL).post(integrationsTrackURI, body).reply(200, {}) const responses = await testDestination.testAction('trackEvent', { event, @@ -127,17 +152,21 @@ describe('Heap.trackEvent', () => { }) it('should get event field for different event type', async () => { + const anonId = '5a41f0df-b69a-4a99-b656-79506a86c3f8' const event: Partial = createTestEvent({ timestamp, event: undefined, userId, messageId, name: 'Home Page', - type: 'page' + type: 'page', + anonymousId: anonId }) - body.identity = userId - body.event = 'Home Page' - nock('https://heapanalytics.com').post('/api/track', body).reply(200, body) + body.events[0].user_identifier = { + anonymous_id: anonId + } + body.events[0].event = 'Home Page page viewed' + nock(heapURL).post(integrationsTrackURI, body).reply(200, {}) const responses = await testDestination.testAction('trackEvent', { event, @@ -148,6 +177,5 @@ describe('Heap.trackEvent', () => { }) expect(responses.length).toBe(1) expect(responses[0].status).toBe(200) - expect(responses[0].data).toEqual(expect.objectContaining({ event: 'Home Page' })) }) }) diff --git a/packages/destination-actions/src/destinations/heap/trackEvent/generated-types.ts b/packages/destination-actions/src/destinations/heap/trackEvent/generated-types.ts index ac84e4fc93..f05b290f57 100644 --- a/packages/destination-actions/src/destinations/heap/trackEvent/generated-types.ts +++ b/packages/destination-actions/src/destinations/heap/trackEvent/generated-types.ts @@ -6,7 +6,7 @@ export interface Payload { */ message_id: string /** - * An identity, typically corresponding to an existing user. If no such identity exists, then a new user will be created with that identity. Case-sensitive string, limited to 255 characters. + * A unique identity and maintain user histories across sessions and devices under a single profile. If no identity is provided we will add the anonymous_id to the event. More on identify: https://developers.heap.io/docs/using-identify */ identity?: string | null /** @@ -27,10 +27,6 @@ export interface Payload { * Defaults to the current time if not provided. */ timestamp?: string | number - /** - * A Heap session ID. The session ID can be retrived by calling getSessionId() on the heap api. If a session ID is not provided one will be created. - */ - session_id?: string /** * The type of call. Can be track, page, or screen. */ diff --git a/packages/destination-actions/src/destinations/heap/trackEvent/index.ts b/packages/destination-actions/src/destinations/heap/trackEvent/index.ts index 10f8144d6a..3acca0ffa2 100644 --- a/packages/destination-actions/src/destinations/heap/trackEvent/index.ts +++ b/packages/destination-actions/src/destinations/heap/trackEvent/index.ts @@ -3,21 +3,29 @@ import type { Settings } from '../generated-types' import type { Payload } from './generated-types' import dayjs from '../../../lib/dayjs' import { HEAP_SEGMENT_CLOUD_LIBRARY_NAME } from '../constants' -import { getHeapUserId } from '../userIdHash' import { flat } from '../flat' +import { getUserIdentifier, getEventName } from '../heapUtils' import { IntegrationError } from '@segment/actions-core' type HeapEvent = { - app_id: string - identity?: string - user_id?: number event: string | undefined + idempotency_key: string + timestamp?: string properties: { [k: string]: unknown } - idempotency_key: string - timestamp?: string - session_id?: string + custom_properties?: { + [k: string]: unknown + } + user_identifier?: { + [k: string]: string + } +} + +type IntegrationsTrackPayload = { + app_id: string + events: Array + library: string } const action: ActionDefinition = { @@ -39,10 +47,7 @@ const action: ActionDefinition = { type: 'string', allowNull: true, description: - 'An identity, typically corresponding to an existing user. If no such identity exists, then a new user will be created with that identity. Case-sensitive string, limited to 255 characters.', - default: { - '@path': '$.userId' - } + 'A unique identity and maintain user histories across sessions and devices under a single profile. If no identity is provided we will add the anonymous_id to the event. More on identify: https://developers.heap.io/docs/using-identify' }, anonymous_id: { label: 'Anonymous ID', @@ -78,15 +83,6 @@ const action: ActionDefinition = { '@path': '$.timestamp' } }, - session_id: { - label: 'Session ID', - type: 'string', - description: - 'A Heap session ID. The session ID can be retrived by calling getSessionId() on the heap api. If a session ID is not provided one will be created.', - default: { - '@path': '$.session_id' - } - }, type: { label: 'Type', type: 'string', @@ -110,27 +106,20 @@ const action: ActionDefinition = { } if (!payload.anonymous_id && !payload.identity) { - throw new IntegrationError('Either anonymous user id or identity should be specified.') + throw new IntegrationError('Either Anonymous id or Identity should be specified.') } - const defaultEventProperties = { segment_library: HEAP_SEGMENT_CLOUD_LIBRARY_NAME } - const flatten = flat(payload.properties || {}) - const eventProperties = Object.assign(defaultEventProperties, flatten) + const standardProperties = { segment_library: HEAP_SEGMENT_CLOUD_LIBRARY_NAME } + const flattenedProperties = flat(payload.properties || {}) - const heapPayload: HeapEvent = { - app_id: settings.appId, + const event: HeapEvent = { event: getEventName(payload), - properties: eventProperties, + custom_properties: flattenedProperties, + properties: standardProperties, idempotency_key: payload.message_id } - if (payload.anonymous_id && !payload.identity) { - heapPayload.user_id = getHeapUserId(payload.anonymous_id) - } - - if (payload.identity) { - heapPayload.identity = payload.identity - } + event.user_identifier = getUserIdentifier({ identity: payload.identity, anonymous_id: payload.anonymous_id }) if (payload.timestamp && dayjs.utc(payload.timestamp).isValid()) { heapPayload.timestamp = dayjs.utc(payload.timestamp).toISOString() @@ -140,9 +129,15 @@ const action: ActionDefinition = { heapPayload.session_id = payload.session_id } - return request('https://heapanalytics.com/api/track', { + const payLoad: IntegrationsTrackPayload = { + app_id: settings.appId, + events: [event], + library: 'Segment' + } + + return request('https://heapanalytics.com/api/integrations/track', { method: 'post', - json: heapPayload + json: payLoad }) } } diff --git a/packages/destination-actions/src/destinations/index.ts b/packages/destination-actions/src/destinations/index.ts index a14ec2b141..27a3e16b9e 100644 --- a/packages/destination-actions/src/destinations/index.ts +++ b/packages/destination-actions/src/destinations/index.ts @@ -72,6 +72,12 @@ register('63872c01c0c112b9b4d75412', './braze-cohorts') register('639c2dbb1309fdcad13951b6', './segment-profiles') register('63bedc136a8484a53739e013', './vwo') register('63d17a1e6ab3e62212278cd0', './saleswings') +register('63e42aa0ed203bc54eaabbee', './launchpad') +register('63e42b47479274407b671071', './livelike-cloud') +register('63e42bc78efe98bc2a8451c1', './twilio-studio') +register('63e42d44b0a59908dc4cacc6', './blackbaud-raisers-edge-nxt') +register('63e42e512566ad7c7ca6ba9b', './pinterest-conversions') +register('63e52bea7747fbc311d5b872', './algolia-insights') function register(id: MetadataId, destinationPath: string) { // eslint-disable-next-line @typescript-eslint/no-var-requires diff --git a/packages/destination-actions/src/destinations/launchpad/generated-types.ts b/packages/destination-actions/src/destinations/launchpad/generated-types.ts new file mode 100644 index 0000000000..ff3cfaa2ed --- /dev/null +++ b/packages/destination-actions/src/destinations/launchpad/generated-types.ts @@ -0,0 +1,16 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Settings { + /** + * Launchpad project secret. You can find that in the settings in your Launchpad.pm account. + */ + apiSecret: string + /** + * Learn about [EU data residency](https://help.launchpad.pm). + */ + apiRegion?: string + /** + * This value, if it's not blank, will be sent as segment_source_name to Launchpad for every event/page/screen call. + */ + sourceName?: string +} diff --git a/packages/destination-actions/src/destinations/launchpad/groupIdentifyUser/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/launchpad/groupIdentifyUser/__tests__/__snapshots__/snapshot.test.ts.snap new file mode 100644 index 0000000000..950688a978 --- /dev/null +++ b/packages/destination-actions/src/destinations/launchpad/groupIdentifyUser/__tests__/__snapshots__/snapshot.test.ts.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Testing snapshot for Launchpad's groupIdentifyUser destination action: all fields 1`] = ` +Object { + "$set": Object { + "group_id": "$WBBWnR", + "group_testType": "$WBBWnR", + }, + "anonymoud_id": "$WBBWnR", + "api_key": "$WBBWnR", + "distinct_id": "$WBBWnR", + "event": "$identify", + "type": "screen", + "user_id": "$WBBWnR", +} +`; + +exports[`Testing snapshot for Launchpad's groupIdentifyUser destination action: required fields 1`] = ` +Object { + "$set": Object { + "group_id": "$WBBWnR", + }, + "api_key": "$WBBWnR", + "event": "$identify", + "type": "screen", +} +`; diff --git a/packages/destination-actions/src/destinations/launchpad/groupIdentifyUser/__tests__/index.test.ts b/packages/destination-actions/src/destinations/launchpad/groupIdentifyUser/__tests__/index.test.ts new file mode 100644 index 0000000000..f654b72e86 --- /dev/null +++ b/packages/destination-actions/src/destinations/launchpad/groupIdentifyUser/__tests__/index.test.ts @@ -0,0 +1,55 @@ +import nock from 'nock' +import { createTestIntegration } from '@segment/actions-core' +import Destination from '../../index' +import { SegmentEvent } from '@segment/actions-core' + +const launchpadAPISecret = 'lp-api-key' +const timestamp = '2023-01-28T15:21:15.449Z' + +const testDestination = createTestIntegration(Destination) + +const expectedTraits = { + group_name: 'Launchpad', + group_industry: 'Technology', + group_employees: 3, + group_plan: '1', + 'group_ARR(m)': 1503 +} + +const testGroupIdentify: SegmentEvent = { + messageId: 'test-message-t73406chv4', + timestamp: timestamp, + type: 'group', + groupId: '12381923812', + userId: 'stephen@launchpad.pm', + traits: { + name: 'Launchpad', + industry: 'Technology', + employees: 3, + plan: '1', + 'ARR(m)': 1503 + } +} + +describe('Launchpad.groupIdentifyUser', () => { + it('should convert the type and event name', async () => { + nock('https://data.launchpad.pm').post('/capture').reply(200, {}) + + const responses = await testDestination.testAction('groupIdentifyUser', { + event: testGroupIdentify, + useDefaultMappings: true, + settings: { + apiSecret: launchpadAPISecret, + sourceName: 'example segment source name' + } + }) + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) + expect(responses[0].data).toMatchObject({}) + expect(responses[0].options.json).toMatchObject({ + event: '$identify', + type: 'screen', + $set: expect.objectContaining(expectedTraits) + }) + }) +}) diff --git a/packages/destination-actions/src/destinations/launchpad/groupIdentifyUser/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/launchpad/groupIdentifyUser/__tests__/snapshot.test.ts new file mode 100644 index 0000000000..d539e3259c --- /dev/null +++ b/packages/destination-actions/src/destinations/launchpad/groupIdentifyUser/__tests__/snapshot.test.ts @@ -0,0 +1,76 @@ +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import { generateTestData } from '../../../../lib/test-data' +import destination from '../../index' +import nock from 'nock' + +const testDestination = createTestIntegration(destination) +const actionSlug = 'groupIdentifyUser' +const destinationSlug = 'Launchpad' +const seedName = `${destinationSlug}#${actionSlug}` + +describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination action:`, () => { + it('required fields', async () => { + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, true) + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + properties: eventData + }) + event.userId = 'user1234' + + const responses = await testDestination.testAction(actionSlug, { + event: event, + mapping: event.properties, + settings: settingsData, + auth: undefined + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + + expect(request.headers).toMatchSnapshot() + }) + + it('all fields', async () => { + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, false) + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + properties: eventData + }) + + const responses = await testDestination.testAction(actionSlug, { + event: event, + mapping: event.properties, + settings: settingsData, + auth: undefined + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + }) +}) diff --git a/packages/destination-actions/src/destinations/launchpad/groupIdentifyUser/generated-types.ts b/packages/destination-actions/src/destinations/launchpad/groupIdentifyUser/generated-types.ts new file mode 100644 index 0000000000..5888ff90b7 --- /dev/null +++ b/packages/destination-actions/src/destinations/launchpad/groupIdentifyUser/generated-types.ts @@ -0,0 +1,26 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * The group key you specified in Launchpad under the company corresponding to the group. If this is not specified, it will be defaulted to "$group_id". This is helpful when you have a group of companies that should be joined together as in when you have a multinational. + */ + groupKey?: string + /** + * The unique identifier of the group. If there is a trait that matches the group key, it will override this value. + */ + groupId: string + /** + * The properties to set on the group profile. + */ + traits?: { + [k: string]: unknown + } + /** + * A unique ID for a known user. This will be used as the Distinct ID. This field is required if the Anonymous ID field is empty + */ + userId?: string + /** + * A unique ID for an anonymous user. This will be used as the Distinct ID if the User ID field is empty. This field is required if the User ID field is empty + */ + anonymousId?: string +} diff --git a/packages/destination-actions/src/destinations/launchpad/groupIdentifyUser/index.ts b/packages/destination-actions/src/destinations/launchpad/groupIdentifyUser/index.ts new file mode 100644 index 0000000000..4a577cc550 --- /dev/null +++ b/packages/destination-actions/src/destinations/launchpad/groupIdentifyUser/index.ts @@ -0,0 +1,89 @@ +import { ActionDefinition } from '@segment/actions-core' +import type { Settings } from '../generated-types' +import { getApiServerUrl } from '../utils' +import type { Payload } from './generated-types' + +const groupIdentifyUser: ActionDefinition = { + title: 'Group Identify User', + description: + 'Updates or adds properties to a group profile. The profile is created if it does not exist. [Learn more about Group Analytics.](https://help.Launchpad.pm)', + defaultSubscription: 'type = "group"', + fields: { + groupKey: { + label: 'Group Key', + type: 'string', + required: false, + description: + 'The group key you specified in Launchpad under the company corresponding to the group. If this is not specified, it will be defaulted to "$group_id". This is helpful when you have a group of companies that should be joined together as in when you have a multinational.' + }, + groupId: { + label: 'Group ID', + type: 'string', + description: + 'The unique identifier of the group. If there is a trait that matches the group key, it will override this value.', + required: true, + default: { + '@path': '$.groupId' + } + }, + traits: { + label: 'Group Properties', + type: 'object', + description: 'The properties to set on the group profile.', + required: false, + default: { + '@path': '$.traits' + } + }, + userId: { + label: 'User ID', + type: 'string', + description: + 'A unique ID for a known user. This will be used as the Distinct ID. This field is required if the Anonymous ID field is empty', + default: { + '@path': '$.userId' + } + }, + anonymousId: { + label: 'Anonymous ID', + type: 'string', + description: + 'A unique ID for an anonymous user. This will be used as the Distinct ID if the User ID field is empty. This field is required if the User ID field is empty', + default: { + '@path': '$.anonymousId' + } + } + }, + + perform: async (request, { payload, settings }) => { + const groupId = payload.groupId + const apiServerUrl = getApiServerUrl(settings.apiRegion) + let transformed_traits + + if (payload.traits) { + transformed_traits = { + ...Object.fromEntries(Object.entries(payload.traits).map(([k, v]) => [`group_${k}`, v])) + } + } + const groupIdentifyEvent = { + event: '$identify', + type: 'screen', + $set: { + group_id: groupId, + ...transformed_traits + }, + distinct_id: payload.userId ? payload.userId : payload.anonymousId, + user_id: payload.userId, + anonymoud_id: payload.anonymousId, + api_key: settings.apiSecret + } + const groupIdentifyResponse = await request(`${apiServerUrl}capture`, { + method: 'post', + json: groupIdentifyEvent + }) + + return groupIdentifyResponse + } +} + +export default groupIdentifyUser diff --git a/packages/destination-actions/src/destinations/launchpad/identifyUser/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/launchpad/identifyUser/__tests__/__snapshots__/snapshot.test.ts.snap new file mode 100644 index 0000000000..d48811b5c9 --- /dev/null +++ b/packages/destination-actions/src/destinations/launchpad/identifyUser/__tests__/__snapshots__/snapshot.test.ts.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Testing snapshot for Launchpad's identifyUser destination action: all fields 1`] = ` +Object { + "$ip": "5SpoCjYek1jtNwUC", + "$set": Object { + "testType": "5SpoCjYek1jtNwUC", + }, + "anonymous_id": "5SpoCjYek1jtNwUC", + "api_key": "5SpoCjYek1jtNwUC", + "distinct_id": "5SpoCjYek1jtNwUC", + "event": "$identify", + "type": "screen", + "user_id": "5SpoCjYek1jtNwUC", +} +`; + +exports[`Testing snapshot for Launchpad's identifyUser destination action: required fields 1`] = ` +Object { + "$set": Object { + "testType": "5SpoCjYek1jtNwUC", + }, + "api_key": "5SpoCjYek1jtNwUC", + "distinct_id": "user1234", + "event": "$identify", + "type": "screen", + "user_id": "user1234", +} +`; diff --git a/packages/destination-actions/src/destinations/launchpad/identifyUser/__tests__/index.test.ts b/packages/destination-actions/src/destinations/launchpad/identifyUser/__tests__/index.test.ts new file mode 100644 index 0000000000..d9cb02658d --- /dev/null +++ b/packages/destination-actions/src/destinations/launchpad/identifyUser/__tests__/index.test.ts @@ -0,0 +1,76 @@ +import nock from 'nock' +import { createTestIntegration } from '@segment/actions-core' +import Destination from '../../index' +import { SegmentEvent } from '@segment/actions-core' + +const launchpadAPISecret = 'lp-api-key' +const timestamp = '2023-01-28T15:21:15.449Z' + +const testDestination = createTestIntegration(Destination) + +const expectedTraits = { + email: 'steve@launchpad.pm', + name: 'Steve Jobs', + plan: '1', + group: 'Launchpad', + title: 'CEO' +} +const testIdentify: SegmentEvent = { + anonymousId: '947336ec-2294-4813-92e6-fe4a01efbf63', + integrations: {}, + messageId: 'ajs-next-11e27cb3f0a3e4e8074e9965ed19a151', + timestamp: timestamp, + traits: { + email: 'steve@launchpad.pm', + name: 'Steve Jobs', + plan: '1', + group: 'Launchpad', + title: 'CEO' + }, + type: 'identify', + userId: 'steve@launchpad.pm' +} + +describe('Launchpad.identifyUser', () => { + it('should convert the type and event name', async () => { + nock('https://data.launchpad.pm').post('/capture').reply(200, {}) + + const responses = await testDestination.testAction('identifyUser', { + event: testIdentify, + useDefaultMappings: true, + settings: { + apiSecret: launchpadAPISecret, + sourceName: 'example segment source name' + } + }) + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) + expect(responses[0].data).toMatchObject({}) + expect(responses[0].options.json).toMatchObject({ + event: '$identify', + type: 'screen', + $set: expect.objectContaining(expectedTraits) + }) + }) + + it('should send segment_source_name property if sourceName setting is defined', async () => { + nock('https://data.launchpad.pm').post('/capture').reply(200, {}) + + const responses = await testDestination.testAction('identifyUser', { + event: testIdentify, + useDefaultMappings: true, + settings: { + apiSecret: launchpadAPISecret, + sourceName: 'example segment source name' + } + }) + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) + expect(responses[0].data).toMatchObject({}) + expect(responses[0].options.json).toMatchObject({ + event: '$identify', + type: 'screen', + $set: expect.objectContaining(expectedTraits) + }) + }) +}) diff --git a/packages/destination-actions/src/destinations/launchpad/identifyUser/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/launchpad/identifyUser/__tests__/snapshot.test.ts new file mode 100644 index 0000000000..3ea86934b0 --- /dev/null +++ b/packages/destination-actions/src/destinations/launchpad/identifyUser/__tests__/snapshot.test.ts @@ -0,0 +1,76 @@ +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import { generateTestData } from '../../../../lib/test-data' +import destination from '../../index' +import nock from 'nock' + +const testDestination = createTestIntegration(destination) +const actionSlug = 'identifyUser' +const destinationSlug = 'Launchpad' +const seedName = `${destinationSlug}#${actionSlug}` + +describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination action:`, () => { + it('required fields', async () => { + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, true) + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + properties: eventData + }) + if (event && event.properties) event.properties.userId = 'user1234' + + const responses = await testDestination.testAction(actionSlug, { + event: event, + mapping: event.properties, + settings: settingsData, + auth: undefined + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + + expect(request.headers).toMatchSnapshot() + }) + + it('all fields', async () => { + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, false) + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + properties: eventData + }) + + const responses = await testDestination.testAction(actionSlug, { + event: event, + mapping: event.properties, + settings: settingsData, + auth: undefined + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + }) +}) diff --git a/packages/destination-actions/src/destinations/launchpad/identifyUser/generated-types.ts b/packages/destination-actions/src/destinations/launchpad/identifyUser/generated-types.ts new file mode 100644 index 0000000000..ea79c85680 --- /dev/null +++ b/packages/destination-actions/src/destinations/launchpad/identifyUser/generated-types.ts @@ -0,0 +1,22 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * The IP address of the user. This is only used for geolocation and won't be stored. + */ + ip?: string + /** + * A unique ID for a known user. This will be used as the Distinct ID. This field is required if the Anonymous ID field is empty + */ + userId?: string + /** + * A unique ID for an anonymous user. This will be used as the Distinct ID if the User ID field is empty. This field is required if the User ID field is empty + */ + anonymousId?: string + /** + * Properties that you want to set on the user profile and you would want to segment by later. + */ + traits: { + [k: string]: unknown + } +} diff --git a/packages/destination-actions/src/destinations/launchpad/identifyUser/index.ts b/packages/destination-actions/src/destinations/launchpad/identifyUser/index.ts new file mode 100644 index 0000000000..536430ae64 --- /dev/null +++ b/packages/destination-actions/src/destinations/launchpad/identifyUser/index.ts @@ -0,0 +1,92 @@ +import { ActionDefinition, omit } from '@segment/actions-core' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' + +import { getApiServerUrl, getConcatenatedName } from '../utils' + +const identifyUser: ActionDefinition = { + title: 'Identify User', + description: + '“Creates or updates a user profile, and adds or updates trait values on the user profile that you can use for segmentation within the Launchpad platform.', + defaultSubscription: 'type = "identify"', + fields: { + ip: { + label: 'IP Address', + type: 'string', + description: "The IP address of the user. This is only used for geolocation and won't be stored.", + default: { + '@path': '$.context.ip' + } + }, + userId: { + label: 'User ID', + type: 'string', + description: + 'A unique ID for a known user. This will be used as the Distinct ID. This field is required if the Anonymous ID field is empty', + default: { + '@path': '$.userId' + } + }, + anonymousId: { + label: 'Anonymous ID', + type: 'string', + description: + 'A unique ID for an anonymous user. This will be used as the Distinct ID if the User ID field is empty. This field is required if the User ID field is empty', + default: { + '@path': '$.anonymousId' + } + }, + traits: { + label: 'User Properties', + type: 'object', + required: true, + description: 'Properties that you want to set on the user profile and you would want to segment by later.', + default: { + '@path': '$.traits' + } + } + }, + + perform: async (request, { payload, settings }) => { + const apiServerUrl = getApiServerUrl(settings.apiRegion) + let traits + + if (payload.traits && Object.keys(payload.traits).length > 0) { + const concatenatedName = getConcatenatedName( + payload.traits.firstName, + payload.traits.lastName, + payload.traits.name + ) + traits = { + ...omit(payload.traits, ['created', 'email', 'firstName', 'lastName', 'name', 'username', 'phone']), + // to fit the Launchpad expectations, transform the special traits to Launchpad reserved property + $created: payload.traits.created, + $email: payload.traits.email, + $first_name: payload.traits.firstName, + $last_name: payload.traits.lastName, + $name: concatenatedName, + $username: payload.traits.username, + $phone: payload.traits.phone, + ...payload.traits + } + } + + const data = { + distinct_id: payload.userId ? payload.userId : payload.anonymousId, + user_id: payload.userId, + anonymous_id: payload.anonymousId, + $ip: payload.ip, + $set: traits ? traits : {}, + event: '$identify', + type: 'screen', + api_key: settings.apiSecret + } + const identifyResponse = request(`${apiServerUrl}capture`, { + method: 'post', + json: data + }) + return identifyResponse + } +} + +export default identifyUser diff --git a/packages/destination-actions/src/destinations/launchpad/index.ts b/packages/destination-actions/src/destinations/launchpad/index.ts new file mode 100644 index 0000000000..ef33a3defc --- /dev/null +++ b/packages/destination-actions/src/destinations/launchpad/index.ts @@ -0,0 +1,99 @@ +import type { DestinationDefinition } from '@segment/actions-core' +import { defaultValues } from '@segment/actions-core' +import type { Settings } from './generated-types' + +import trackEvent from './trackEvent' +import identifyUser from './identifyUser' +import groupIdentifyUser from './groupIdentifyUser' + +import { ApiRegions } from './utils' + +/** used in the quick setup */ +const presets: DestinationDefinition['presets'] = [ + { + name: 'Track Calls', + subscribe: 'type = "track" and event != "Order Completed"', + partnerAction: 'trackEvent', + mapping: defaultValues(trackEvent.fields) + }, + { + name: 'Page Calls', + subscribe: 'type = "page"', + partnerAction: 'trackEvent', + mapping: { + ...defaultValues(trackEvent.fields), + event: { + '@template': 'Viewed {{name}}' + } + } + }, + { + name: 'Screen Calls', + subscribe: 'type = "screen"', + partnerAction: 'trackEvent', + mapping: { + ...defaultValues(trackEvent.fields), + event: { + '@template': 'Viewed {{name}}' + } + } + }, + { + name: 'Identify Calls', + subscribe: 'type = "identify"', + partnerAction: 'identifyUser', + mapping: defaultValues(identifyUser.fields) + }, + { + name: 'Group Calls', + subscribe: 'type = "group"', + partnerAction: 'groupIdentifyUser', + mapping: defaultValues(groupIdentifyUser.fields) + } +] + +const destination: DestinationDefinition = { + name: 'Launchpad (Actions)', + slug: 'actions-launchpad', + mode: 'cloud', + authentication: { + scheme: 'custom', + fields: { + apiSecret: { + label: 'Secret Key', + description: 'Launchpad project secret. You can find that in the settings in your Launchpad.pm account.', + type: 'password', + required: true + }, + apiRegion: { + label: 'Data Residency', + description: 'Learn about [EU data residency](https://help.launchpad.pm).', + type: 'string', + choices: Object.values(ApiRegions).map((apiRegion) => ({ label: apiRegion, value: apiRegion })), + default: ApiRegions.US + }, + sourceName: { + label: 'Source Name', + description: + "This value, if it's not blank, will be sent as segment_source_name to Launchpad for every event/page/screen call.", + type: 'string' + } + }, + testAuthentication: (request, { settings }) => { + return request(`https://backend.launchpad.pm/api/data/validate-project-credentials/`, { + method: 'post', + body: JSON.stringify({ + api_secret: settings.apiSecret + }) + }) + } + }, + presets, + actions: { + trackEvent, + identifyUser, + groupIdentifyUser + } +} + +export default destination diff --git a/packages/destination-actions/src/destinations/launchpad/launchpad-types.ts b/packages/destination-actions/src/destinations/launchpad/launchpad-types.ts new file mode 100644 index 0000000000..f580ab4a1b --- /dev/null +++ b/packages/destination-actions/src/destinations/launchpad/launchpad-types.ts @@ -0,0 +1,29 @@ +export type LaunchpadEventProperties = { + anonymous_id?: string // 'anon-2134' + browser_version?: string // '9.0' + browser?: string // 'Mobile Safari' + current_url?: string // 'https?://segment.com/academy/' + device?: string // 'maguro' + group_id?: string // 'groupId123' + identified_id?: string // 'user1234' + messageId?: string // '859d3955-363f-590a-9aa4-b4f49b582437' + ip?: string | unknown // '192.168.1.1' + os_version?: string // '8.1.3' + os?: string // 'iPhone OS' + referrer?: string + user_id?: string + source: string // 'segment' + distinct_id: string | undefined // 'test_segment_user' + id?: string | null // this is just to maintain backwards compatibility with the classic segment integration, I'm not completely sure what the purpose of this was. + segment_source_name?: string // 'readme' + time?: string | number | undefined + properties?: [k: string] | unknown //event props + traits?: [k: string] | unknown + context?: [k: string] | unknown +} + +export type LaunchpadEvent = { + event?: string + properties?: LaunchpadEventProperties + api_key: string +} diff --git a/packages/destination-actions/src/destinations/launchpad/trackEvent/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/launchpad/trackEvent/__tests__/__snapshots__/snapshot.test.ts.snap new file mode 100644 index 0000000000..6cef94104d --- /dev/null +++ b/packages/destination-actions/src/destinations/launchpad/trackEvent/__tests__/__snapshots__/snapshot.test.ts.snap @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Testing snapshot for Launchpad's trackEvent destination action: all fields 1`] = ` +Object { + "api_key": "8I03tIp&]#vR", + "event": "8I03tIp&]#vR", + "properties": Object { + "anonymous_id": "8I03tIp&]#vR", + "context": Object { + "testType": "8I03tIp&]#vR", + }, + "distinct_id": "8I03tIp&]#vR", + "group_id": "8I03tIp&]#vR", + "messageId": "8I03tIp&]#vR", + "properties": Object { + "testType": "8I03tIp&]#vR", + }, + "segment_source_name": "8I03tIp&]#vR", + "time": "2021-02-01T00:00:00.000Z", + "traits": Object { + "testType": "8I03tIp&]#vR", + }, + "user_id": "8I03tIp&]#vR", + }, +} +`; + +exports[`Testing snapshot for Launchpad's trackEvent destination action: required fields 1`] = ` +Object { + "api_key": "8I03tIp&]#vR", + "event": "8I03tIp&]#vR", + "properties": Object { + "messageId": "8I03tIp&]#vR", + "segment_source_name": "8I03tIp&]#vR", + "time": "2021-02-01T00:00:00.000Z", + }, +} +`; diff --git a/packages/destination-actions/src/destinations/launchpad/trackEvent/__tests__/index.test.ts b/packages/destination-actions/src/destinations/launchpad/trackEvent/__tests__/index.test.ts new file mode 100644 index 0000000000..35d4d41828 --- /dev/null +++ b/packages/destination-actions/src/destinations/launchpad/trackEvent/__tests__/index.test.ts @@ -0,0 +1,63 @@ +import nock from 'nock' +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import Destination from '../../index' +import { ApiRegions } from '../../utils' + +const testDestination = createTestIntegration(Destination) + +const launchpadAPISecret = 'lp-api-key' +const timestamp = '2023-01-28T15:21:15.449Z' + +const mustHaveProps = { + distinct_id: 'user1234', + ip: '8.8.8.8', + properties: {}, + traits: {}, + user_id: 'user1234' +} + +describe('Launchpad.trackEvent', () => { + it('should always return distinct id', async () => { + const event = createTestEvent({ timestamp, event: 'Test Event' }) + + nock('https://data.launchpad.pm').post('/capture').reply(200, {}) + + const responses = await testDestination.testAction('trackEvent', { + event, + useDefaultMappings: true, + settings: { + apiSecret: launchpadAPISecret, + apiRegion: ApiRegions.EU, + sourceName: 'segment' + } + }) + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) + expect(responses[0].data).toMatchObject({}) + expect(responses[0].options.json).toMatchObject({ + event: 'Test Event', + properties: expect.objectContaining(mustHaveProps) + }) + }) + + it('should default to the EU endpoint if apiRegion setting is undefined', async () => { + const event = createTestEvent({ timestamp, event: 'Test Event' }) + + nock('https://data.launchpad.pm').post('/capture').reply(200, {}) + + const responses = await testDestination.testAction('trackEvent', { + event, + useDefaultMappings: true, + settings: { + apiSecret: launchpadAPISecret, + sourceName: 'segment' + } + }) + expect(responses[0].status).toBe(200) + expect(responses[0].data).toMatchObject({}) + expect(responses[0].options.json).toMatchObject({ + event: 'Test Event', + properties: expect.objectContaining(mustHaveProps) + }) + }) +}) diff --git a/packages/destination-actions/src/destinations/launchpad/trackEvent/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/launchpad/trackEvent/__tests__/snapshot.test.ts new file mode 100644 index 0000000000..3fbd0d4868 --- /dev/null +++ b/packages/destination-actions/src/destinations/launchpad/trackEvent/__tests__/snapshot.test.ts @@ -0,0 +1,75 @@ +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import { generateTestData } from '../../../../lib/test-data' +import destination from '../../index' +import nock from 'nock' + +const testDestination = createTestIntegration(destination) +const actionSlug = 'trackEvent' +const destinationSlug = 'Launchpad' +const seedName = `${destinationSlug}#${actionSlug}` + +describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination action:`, () => { + it('required fields', async () => { + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, true) + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + properties: eventData + }) + + const responses = await testDestination.testAction(actionSlug, { + event: event, + mapping: event.properties, + settings: settingsData, + auth: undefined + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + + expect(request.headers).toMatchSnapshot() + }) + + it('all fields', async () => { + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, false) + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + properties: eventData + }) + + const responses = await testDestination.testAction(actionSlug, { + event: event, + mapping: event.properties, + settings: settingsData, + auth: undefined + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + }) +}) diff --git a/packages/destination-actions/src/destinations/launchpad/trackEvent/generated-types.ts b/packages/destination-actions/src/destinations/launchpad/trackEvent/generated-types.ts new file mode 100644 index 0000000000..fefc33d628 --- /dev/null +++ b/packages/destination-actions/src/destinations/launchpad/trackEvent/generated-types.ts @@ -0,0 +1,50 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * The name of the action being performed. + */ + event: string + /** + * A unique ID for a known user. This will be used as the Distinct ID. This field is required if the Anonymous ID field is empty + */ + userId?: string + /** + * A unique ID for an anonymous user. This will be used as the Distinct ID if the User ID field is empty. This field is required if the User ID field is empty + */ + anonymousId?: string + /** + * The unique identifier of the group that performed this event. + */ + groupId?: string + /** + * A random id that is unique to an event. Launchpad uses $insert_id to deduplicate events. + */ + messageId: string + /** + * The timestamp of the event. Launchpad expects epoch timestamp in millisecond or second. Please note, Launchpad only accepts this field as the timestamp. If the field is empty, it will be set to the time Launchpad servers receive it. + */ + timestamp: string | number + /** + * An object of key-value pairs that represent additional data to be sent along with the event. + */ + properties?: { + [k: string]: unknown + } + /** + * An object of key-value pairs that represent additional data tied to the user. This is used for segmentation within the platform. + */ + traits?: { + [k: string]: unknown + } + /** + * An object of key-value pairs that provides useful context about the event. + */ + context?: { + [k: string]: unknown + } + /** + * Set as true to ensure Segment sends data to Launchpad in batches. + */ + enable_batching?: boolean +} diff --git a/packages/destination-actions/src/destinations/launchpad/trackEvent/index.ts b/packages/destination-actions/src/destinations/launchpad/trackEvent/index.ts new file mode 100644 index 0000000000..769a6dbdce --- /dev/null +++ b/packages/destination-actions/src/destinations/launchpad/trackEvent/index.ts @@ -0,0 +1,72 @@ +import { ActionDefinition, RequestClient } from '@segment/actions-core' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' +import { LaunchpadEvent } from '../launchpad-types' +import { getApiServerUrl } from '../utils' +import { LaunchpadEventProperties } from '../launchpad-types' +import { eventProperties } from './launchpad-properties' + +function getEventProperties(payload: Payload, settings: Settings): LaunchpadEventProperties { + const integration = payload.context?.integration as Record + return { + time: payload.timestamp, + ip: payload.context?.ip, + anonymous_id: payload.anonymousId, + distinct_id: payload.userId ? payload.userId : payload.anonymousId, + context: payload.context, + group_id: payload.groupId, + properties: payload.properties, + traits: payload.traits ? payload.traits : payload.context?.traits, + messageId: payload.messageId, + source: integration?.name != 'Segment' ? integration?.name : 'Segment', + user_id: payload.userId, + segment_source_name: settings.sourceName + } +} + +const getEventFromPayload = (payload: Payload, settings: Settings): LaunchpadEvent => { + const event: LaunchpadEvent = { + event: payload.event, + properties: { + ...getEventProperties(payload, settings) + }, + api_key: settings.apiSecret + } + return event +} + +const processData = async (request: RequestClient, settings: Settings, payload: Payload) => { + const event = getEventFromPayload(payload, settings) + const urlAddendum = 'capture' + + const requestURL: string = `${getApiServerUrl(settings.apiRegion)}` + urlAddendum + + return request(requestURL, { + method: 'post', + json: event + }) +} + +const trackEvent: ActionDefinition = { + title: 'Track Event', + description: 'Send an event to Launchpad. [Learn more about Events in Launchpad](https://help.launchpad.pm)', + defaultSubscription: 'type = "track"', + fields: { + event: { + label: 'Event Name', + type: 'string', + description: 'The name of the action being performed.', + required: true, + default: { + '@path': '$.event' + } + }, + ...eventProperties + }, + + perform: async (request, { settings, payload }) => { + return processData(request, settings, payload) + } +} + +export default trackEvent diff --git a/packages/destination-actions/src/destinations/launchpad/trackEvent/launchpad-properties.ts b/packages/destination-actions/src/destinations/launchpad/trackEvent/launchpad-properties.ts new file mode 100644 index 0000000000..1476ec9bb3 --- /dev/null +++ b/packages/destination-actions/src/destinations/launchpad/trackEvent/launchpad-properties.ts @@ -0,0 +1,89 @@ +import { InputField } from '@segment/actions-core' + +export const eventProperties: Record = { + userId: { + label: 'User ID', + type: 'string', + description: + 'A unique ID for a known user. This will be used as the Distinct ID. This field is required if the Anonymous ID field is empty', + default: { + '@path': '$.userId' + } + }, + anonymousId: { + label: 'Anonymous ID', + type: 'string', + description: + 'A unique ID for an anonymous user. This will be used as the Distinct ID if the User ID field is empty. This field is required if the User ID field is empty', + default: { + '@path': '$.anonymousId' + } + }, + groupId: { + label: 'Group ID', + type: 'string', + required: false, + description: 'The unique identifier of the group that performed this event.', + default: { + '@path': '$.context.groupId' + } + }, + messageId: { + label: 'Insert ID', + type: 'string', + required: true, + description: 'A random id that is unique to an event. Launchpad uses $insert_id to deduplicate events.', + default: { + '@path': '$.messageId' + } + }, + timestamp: { + label: 'Timestamp', + type: 'datetime', + required: true, + description: + 'The timestamp of the event. Launchpad expects epoch timestamp in millisecond or second. Please note, Launchpad only accepts this field as the timestamp. If the field is empty, it will be set to the time Launchpad servers receive it.', + default: { + '@path': '$.timestamp' + } + }, + properties: { + required: false, + label: 'Event Properties', + type: 'object', + description: 'An object of key-value pairs that represent additional data to be sent along with the event.', + default: { + '@path': '$.properties' + } + }, + traits: { + required: false, + label: 'User Properties', + type: 'object', + description: + 'An object of key-value pairs that represent additional data tied to the user. This is used for segmentation within the platform.', + default: { + '@if': { + exists: { '@path': '$.traits' }, + then: { '@path': '$.traits' }, + else: { '@path': '$.context.traits' } + } + } + }, + context: { + label: 'Event context', + required: false, + description: 'An object of key-value pairs that provides useful context about the event.', + type: 'object', + default: { + '@path': '$.context' + } + }, + enable_batching: { + required: false, + type: 'boolean', + label: 'Batch Data to Launchpad', + description: 'Set as true to ensure Segment sends data to Launchpad in batches.', + default: true + } +} diff --git a/packages/destination-actions/src/destinations/launchpad/utils.ts b/packages/destination-actions/src/destinations/launchpad/utils.ts new file mode 100644 index 0000000000..e88b4d572e --- /dev/null +++ b/packages/destination-actions/src/destinations/launchpad/utils.ts @@ -0,0 +1,20 @@ +export enum ApiRegions { + US = 'US 🇺🇸', + EU = 'EU 🇪🇺' +} + +// export enum StrictMode { +// ON = '1', +// OFF = '0' +// } + +export function getConcatenatedName(firstName: unknown, lastName: unknown, name: unknown): unknown { + return name ?? (firstName && lastName ? `${firstName} ${lastName}` : undefined) +} + +export function getApiServerUrl(apiRegion: string | unknown) { + if (apiRegion == ApiRegions.EU) { + return 'https://data.launchpad.pm/' + } + return 'https://data.launchpad.pm/' +} diff --git a/packages/destination-actions/src/destinations/livelike-cloud/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/livelike-cloud/__tests__/__snapshots__/snapshot.test.ts.snap new file mode 100644 index 0000000000..8e2f102dd0 --- /dev/null +++ b/packages/destination-actions/src/destinations/livelike-cloud/__tests__/__snapshots__/snapshot.test.ts.snap @@ -0,0 +1,32 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Testing snapshot for actions-livelike-cloud destination: trackEvent action - all fields 1`] = ` +Object { + "events": Array [ + Object { + "action_description": "n]C0wig#f82f0li1(A", + "action_key": "n]C0wig#f82f0li1(A", + "action_name": "n]C0wig#f82f0li1(A", + "livelike_profile_id": "n]C0wig#f82f0li1(A", + "properties": Object { + "testType": "n]C0wig#f82f0li1(A", + }, + "timestamp": "2021-02-01T00:00:00.000Z", + "user_id": "n]C0wig#f82f0li1(A", + }, + ], +} +`; + +exports[`Testing snapshot for actions-livelike-cloud destination: trackEvent action - required fields 1`] = ` +Object { + "events": Array [ + Object { + "action_key": "n]C0wig#f82f0li1(A", + "livelike_profile_id": "n]C0wig#f82f0li1(A", + "timestamp": "2021-02-01T00:00:00.000Z", + "user_id": "n]C0wig#f82f0li1(A", + }, + ], +} +`; diff --git a/packages/destination-actions/src/destinations/livelike-cloud/__tests__/index.test.ts b/packages/destination-actions/src/destinations/livelike-cloud/__tests__/index.test.ts new file mode 100644 index 0000000000..4a431d431e --- /dev/null +++ b/packages/destination-actions/src/destinations/livelike-cloud/__tests__/index.test.ts @@ -0,0 +1,26 @@ +import nock from 'nock' +import { createTestIntegration } from '@segment/actions-core' +import Definition from '../index' +import { apiBaseUrl } from '../properties' + +const testDestination = createTestIntegration(Definition) + +describe('Livelike', () => { + describe('testAuthentication', () => { + it('should throw an error in case of invalid inputs', async () => { + nock(apiBaseUrl).get('/applications/abc/validate-app/').matchHeader('authorization', `Bearer 123`).reply(401, {}) + + const authData = { clientId: 'abc', producerToken: '123' } + + await expect(testDestination.testAuthentication(authData)).rejects.toThrowError() + }) + + it('should validate authentication inputs', async () => { + nock(apiBaseUrl).get('/applications/abc/validate-app/').matchHeader('authorization', `Bearer 123`).reply(200, {}) + + const authData = { clientId: 'abc', producerToken: '123' } + + await expect(testDestination.testAuthentication(authData)).resolves.not.toThrowError() + }) + }) +}) diff --git a/packages/destination-actions/src/destinations/livelike-cloud/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/livelike-cloud/__tests__/snapshot.test.ts new file mode 100644 index 0000000000..4c635867dd --- /dev/null +++ b/packages/destination-actions/src/destinations/livelike-cloud/__tests__/snapshot.test.ts @@ -0,0 +1,77 @@ +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import { generateTestData } from '../../../lib/test-data' +import destination from '../index' +import nock from 'nock' + +const testDestination = createTestIntegration(destination) +const destinationSlug = 'actions-livelike-cloud' + +describe(`Testing snapshot for ${destinationSlug} destination:`, () => { + for (const actionSlug in destination.actions) { + it(`${actionSlug} action - required fields`, async () => { + const seedName = `${destinationSlug}#${actionSlug}` + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, true) + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + properties: eventData + }) + + const responses = await testDestination.testAction(actionSlug, { + event: event, + mapping: event.properties, + settings: settingsData, + auth: undefined + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + + expect(request.headers).toMatchSnapshot() + }) + + it(`${actionSlug} action - all fields`, async () => { + const seedName = `${destinationSlug}#${actionSlug}` + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, false) + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + properties: eventData + }) + + const responses = await testDestination.testAction(actionSlug, { + event: event, + mapping: event.properties, + settings: settingsData, + auth: undefined + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + }) + } +}) diff --git a/packages/destination-actions/src/destinations/livelike-cloud/generated-types.ts b/packages/destination-actions/src/destinations/livelike-cloud/generated-types.ts new file mode 100644 index 0000000000..5bfcf7430a --- /dev/null +++ b/packages/destination-actions/src/destinations/livelike-cloud/generated-types.ts @@ -0,0 +1,12 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Settings { + /** + * Your LiveLike Application Client ID. + */ + clientId: string + /** + * Your LiveLike Producer token. + */ + producerToken: string +} diff --git a/packages/destination-actions/src/destinations/livelike-cloud/index.ts b/packages/destination-actions/src/destinations/livelike-cloud/index.ts new file mode 100644 index 0000000000..4e79890d78 --- /dev/null +++ b/packages/destination-actions/src/destinations/livelike-cloud/index.ts @@ -0,0 +1,84 @@ +import { defaultValues, DestinationDefinition } from '@segment/actions-core' +import type { Settings } from './generated-types' +import { apiBaseUrl } from './properties' + +import trackEvent from './trackEvent' + +const presets: DestinationDefinition['presets'] = [ + { + name: 'Track User Actions', + subscribe: 'type = "track"', + partnerAction: 'trackEvent', + mapping: defaultValues(trackEvent.fields) + }, + { + name: 'Page Calls', + subscribe: 'type = "page"', + partnerAction: 'trackEvent', + mapping: { + ...defaultValues(trackEvent.fields), + action_name: { + '@if': { + exists: { '@path': '$.properties.action_name' }, + then: { '@path': '$.properties.action_name' }, + else: { '@path': '$.properties.title' } + } + } + } + }, + { + name: 'Screen Calls', + subscribe: 'type = "screen"', + partnerAction: 'trackEvent', + mapping: { + ...defaultValues(trackEvent.fields), + action_name: { + '@if': { + exists: { '@path': '$.properties.action_name' }, + then: { '@path': '$.properties.action_name' }, + else: { '@path': '$.properties.title' } + } + } + } + } +] + +const destination: DestinationDefinition = { + name: 'LiveLike', + slug: 'actions-livelike-cloud', + mode: 'cloud', + + authentication: { + scheme: 'custom', + fields: { + clientId: { + label: 'Client ID', + description: 'Your LiveLike Application Client ID.', + type: 'string', + required: true + }, + producerToken: { + label: 'Producer Token', + description: 'Your LiveLike Producer token.', + type: 'password', + required: true + } + }, + testAuthentication: async (request, { settings }) => { + return request(`${apiBaseUrl}/applications/${settings.clientId}/validate-app/`, { + method: 'get' + }) + } + }, + extendRequest: ({ settings }) => { + return { + headers: { Authorization: `Bearer ${settings.producerToken}` } + } + }, + presets, + actions: { + trackEvent + } +} + +export default destination diff --git a/packages/destination-actions/src/destinations/livelike-cloud/properties.ts b/packages/destination-actions/src/destinations/livelike-cloud/properties.ts new file mode 100644 index 0000000000..0004163db7 --- /dev/null +++ b/packages/destination-actions/src/destinations/livelike-cloud/properties.ts @@ -0,0 +1 @@ +export const apiBaseUrl = 'https://cf-blast-iconic.livelikecdn.com/api/v1' diff --git a/packages/destination-actions/src/destinations/livelike-cloud/trackEvent/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/livelike-cloud/trackEvent/__tests__/__snapshots__/snapshot.test.ts.snap new file mode 100644 index 0000000000..d0a2a6e6b4 --- /dev/null +++ b/packages/destination-actions/src/destinations/livelike-cloud/trackEvent/__tests__/__snapshots__/snapshot.test.ts.snap @@ -0,0 +1,32 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Testing snapshot for LivelikeCloud's trackEvent destination action: all fields 1`] = ` +Object { + "events": Array [ + Object { + "action_description": "&Lu]rW[@yx2%6aWnTgQ", + "action_key": "&Lu]rW[@yx2%6aWnTgQ", + "action_name": "&Lu]rW[@yx2%6aWnTgQ", + "livelike_profile_id": "&Lu]rW[@yx2%6aWnTgQ", + "properties": Object { + "testType": "&Lu]rW[@yx2%6aWnTgQ", + }, + "timestamp": "2021-02-01T00:00:00.000Z", + "user_id": "&Lu]rW[@yx2%6aWnTgQ", + }, + ], +} +`; + +exports[`Testing snapshot for LivelikeCloud's trackEvent destination action: required fields 1`] = ` +Object { + "events": Array [ + Object { + "action_key": "&Lu]rW[@yx2%6aWnTgQ", + "livelike_profile_id": "&Lu]rW[@yx2%6aWnTgQ", + "timestamp": "2021-02-01T00:00:00.000Z", + "user_id": "&Lu]rW[@yx2%6aWnTgQ", + }, + ], +} +`; diff --git a/packages/destination-actions/src/destinations/livelike-cloud/trackEvent/__tests__/index.test.ts b/packages/destination-actions/src/destinations/livelike-cloud/trackEvent/__tests__/index.test.ts new file mode 100644 index 0000000000..e94e6b8301 --- /dev/null +++ b/packages/destination-actions/src/destinations/livelike-cloud/trackEvent/__tests__/index.test.ts @@ -0,0 +1,250 @@ +import nock from 'nock' +import { createTestEvent, createTestIntegration, IntegrationError } from '@segment/actions-core' +import Destination from '../../index' +import { Payload } from '../generated-types' +import { apiBaseUrl } from '../../properties' + +const testDestination = createTestIntegration(Destination) +const LIVELIKE_CLIENT_ID = 'test-client-id' +const LIVELIKE_PRODUCER_TOKEN = 'test-producer-token' +const timestamp = '2021-08-17T15:21:15.449Z' +const livelike_profile_id = 'user123' + +const expectedEvent: Payload = { + action_key: 'test-event', + properties: {}, + timestamp: timestamp +} + +describe('LiveLike.trackEvent', () => { + it('should throw integration error when clientId and producerToken is not configured', async () => { + const event = createTestEvent({ + timestamp, + properties: { + action_key: 'test-event', + livelike_profile_id: livelike_profile_id + } + }) + + await expect( + testDestination.testAction('trackEvent', { + event, + useDefaultMappings: true + }) + ).rejects.toThrowError(new IntegrationError('Missing client ID or producer token.')) + }) + + it('should throw integration error when livelike_profile_id or user_id is not found or null', async () => { + const event = createTestEvent({ + //Need to set userId as null because `createTestEvent` and `testAction` adds a default userId which will fail the test everytime. + userId: null, + timestamp, + properties: { + action_key: 'test-event' + } + }) + + await expect( + testDestination.testAction('trackEvent', { + event, + useDefaultMappings: true, + settings: { + clientId: LIVELIKE_CLIENT_ID, + producerToken: LIVELIKE_PRODUCER_TOKEN + } + }) + ).rejects.toThrowError( + new IntegrationError('`livelike_profile_id` or `user_id` is required.', 'Missing required fields', 400) + ) + }) + + it('should throw integration error when action_key is not found', async () => { + const event = createTestEvent({ + timestamp, + properties: { + livelike_profile_id: livelike_profile_id + } + }) + + await expect( + testDestination.testAction('trackEvent', { + event, + useDefaultMappings: true, + settings: { + clientId: LIVELIKE_CLIENT_ID, + producerToken: LIVELIKE_PRODUCER_TOKEN + } + }) + ).rejects.toThrowError(new IntegrationError("The root value is missing the required field 'action_key'.")) + }) + + it('should validate action fields when userId is found and not livelike_profile_id ', async () => { + const event = createTestEvent({ + userId: livelike_profile_id, + timestamp, + properties: { + action_key: 'test-event' + } + }) + + nock(apiBaseUrl) + .post(`/applications/${LIVELIKE_CLIENT_ID}/segment-events/`) + .matchHeader('authorization', `Bearer ${LIVELIKE_PRODUCER_TOKEN}`) + .reply(202, {}) + + const responses = await testDestination.testAction('trackEvent', { + event, + useDefaultMappings: true, + settings: { + clientId: LIVELIKE_CLIENT_ID, + producerToken: LIVELIKE_PRODUCER_TOKEN + } + }) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(202) + expect(responses[0].data).toMatchObject({}) + expect(responses[0].options.json).toMatchObject({ + events: [ + { + ...expectedEvent, + user_id: livelike_profile_id + } + ] + }) + }) + + it('should validate action fields when livelike_profile_id is found and not user_id ', async () => { + const event = createTestEvent({ + timestamp, + properties: { + action_key: 'test-event', + livelike_profile_id: livelike_profile_id + } + }) + + nock(apiBaseUrl) + .post(`/applications/${LIVELIKE_CLIENT_ID}/segment-events/`) + .matchHeader('authorization', `Bearer ${LIVELIKE_PRODUCER_TOKEN}`) + .reply(202, {}) + + const responses = await testDestination.testAction('trackEvent', { + event, + useDefaultMappings: true, + settings: { + clientId: LIVELIKE_CLIENT_ID, + producerToken: LIVELIKE_PRODUCER_TOKEN + } + }) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(202) + expect(responses[0].data).toMatchObject({}) + expect(responses[0].options.json).toMatchObject({ + events: [ + { + ...expectedEvent, + livelike_profile_id: livelike_profile_id + } + ] + }) + }) + + it('should invoke performBatch for batches', async () => { + const events = [ + createTestEvent({ + timestamp, + properties: { + action_key: 'test-event', + livelike_profile_id: livelike_profile_id + } + }), + createTestEvent({ + timestamp, + properties: { + action_key: 'test-event', + livelike_profile_id: livelike_profile_id + } + }) + ] + + nock(apiBaseUrl) + .post(`/applications/${LIVELIKE_CLIENT_ID}/segment-events/`) + .matchHeader('authorization', `Bearer ${LIVELIKE_PRODUCER_TOKEN}`) + .reply(202, {}) + + const responses = await testDestination.testBatchAction('trackEvent', { + events, + useDefaultMappings: true, + settings: { + clientId: LIVELIKE_CLIENT_ID, + producerToken: LIVELIKE_PRODUCER_TOKEN + } + }) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(202) + expect(responses[0].data).toMatchObject({}) + expect(responses[0].options.json).toMatchObject({ + events: [ + { + ...expectedEvent, + livelike_profile_id: livelike_profile_id + }, + { + ...expectedEvent, + livelike_profile_id: livelike_profile_id + } + ] + }) + }) + + it('should validate action fields when event type is page or screen(presets) ', async () => { + const event = createTestEvent({ + type: 'page', + timestamp, + properties: { + name: 'Home Page', + action_key: 'test-event', + livelike_profile_id: livelike_profile_id + } + }) + + nock(apiBaseUrl) + .post(`/applications/${LIVELIKE_CLIENT_ID}/segment-events/`) + .matchHeader('authorization', `Bearer ${LIVELIKE_PRODUCER_TOKEN}`) + .reply(202, {}) + + const responses = await testDestination.testAction('trackEvent', { + event, + // Using the mapping of presets with event type 'page' and 'screen' + mapping: { + action_name: { + '@if': { + exists: { '@path': '$.properties.action_name' }, + then: { '@path': '$.properties.action_name' }, + else: { '@path': '$.properties.name' } + } + } + }, + useDefaultMappings: true, + settings: { + clientId: LIVELIKE_CLIENT_ID, + producerToken: LIVELIKE_PRODUCER_TOKEN + } + }) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(202) + expect(responses[0].data).toMatchObject({}) + expect(responses[0].options.json).toMatchObject({ + events: [ + { + ...expectedEvent, + livelike_profile_id: livelike_profile_id, + action_name: 'Home Page' + } + ] + }) + }) +}) diff --git a/packages/destination-actions/src/destinations/livelike-cloud/trackEvent/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/livelike-cloud/trackEvent/__tests__/snapshot.test.ts new file mode 100644 index 0000000000..ddadd44dbe --- /dev/null +++ b/packages/destination-actions/src/destinations/livelike-cloud/trackEvent/__tests__/snapshot.test.ts @@ -0,0 +1,75 @@ +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import { generateTestData } from '../../../../lib/test-data' +import destination from '../../index' +import nock from 'nock' + +const testDestination = createTestIntegration(destination) +const actionSlug = 'trackEvent' +const destinationSlug = 'LivelikeCloud' +const seedName = `${destinationSlug}#${actionSlug}` + +describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination action:`, () => { + it('required fields', async () => { + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, true) + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + properties: eventData + }) + + const responses = await testDestination.testAction(actionSlug, { + event: event, + mapping: event.properties, + settings: settingsData, + auth: undefined + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + + expect(request.headers).toMatchSnapshot() + }) + + it('all fields', async () => { + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, false) + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + properties: eventData + }) + + const responses = await testDestination.testAction(actionSlug, { + event: event, + mapping: event.properties, + settings: settingsData, + auth: undefined + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + }) +}) diff --git a/packages/destination-actions/src/destinations/livelike-cloud/trackEvent/generated-types.ts b/packages/destination-actions/src/destinations/livelike-cloud/trackEvent/generated-types.ts new file mode 100644 index 0000000000..c6cdeafdbc --- /dev/null +++ b/packages/destination-actions/src/destinations/livelike-cloud/trackEvent/generated-types.ts @@ -0,0 +1,34 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * The unique key of Action. LiveLike will uniquely identify any event by this key. + */ + action_key: string + /** + * The name of the action being performed. + */ + action_name?: string + /** + * The description of the Action. + */ + action_description?: string + /** + * A unique identifier for a user. At least one of `User ID` or `LiveLike User Profile ID` is mandatory. + */ + user_id?: string + /** + * The unique LiveLike user identifier. Atleast one of `LiveLike User Profile ID` or `User ID` is mandatory. + */ + livelike_profile_id?: string + /** + * The timestamp of the event. + */ + timestamp: string | number + /** + * Properties of the event. + */ + properties?: { + [k: string]: unknown + } +} diff --git a/packages/destination-actions/src/destinations/livelike-cloud/trackEvent/index.ts b/packages/destination-actions/src/destinations/livelike-cloud/trackEvent/index.ts new file mode 100644 index 0000000000..128af4a63d --- /dev/null +++ b/packages/destination-actions/src/destinations/livelike-cloud/trackEvent/index.ts @@ -0,0 +1,107 @@ +import { ActionDefinition, IntegrationError, RequestClient } from '@segment/actions-core' +import type { Settings } from '../generated-types' +import { apiBaseUrl } from '../properties' +import type { Payload } from './generated-types' + +const processData = async (request: RequestClient, settings: Settings, payloads: Payload[]) => { + const events = payloads.map((payload) => { + if (!payload.livelike_profile_id && !payload.user_id) { + throw new IntegrationError('`livelike_profile_id` or `user_id` is required.', 'Missing required fields', 400) + } + return payload + }) + + return request(`${apiBaseUrl}/applications/${settings.clientId}/segment-events/`, { + method: 'post', + json: { + events + } + }) +} + +const action: ActionDefinition = { + title: 'Track Event', + description: 'Send an event to LiveLike.', + defaultSubscription: 'type = "track"', + fields: { + action_key: { + label: 'Action Key', + description: 'The unique key of Action. LiveLike will uniquely identify any event by this key.', + type: 'string', + required: true, + default: { + '@path': '$.properties.action_key' + } + }, + action_name: { + label: 'Action Name', + description: 'The name of the action being performed.', + type: 'string', + default: { + '@if': { + exists: { '@path': '$.properties.action_name' }, + then: { '@path': '$.properties.action_name' }, + else: { '@path': '$.event' } + } + } + }, + action_description: { + label: 'Action Description', + description: 'The description of the Action.', + type: 'string', + default: { + '@path': '$.properties.action_description' + } + }, + user_id: { + label: 'User ID', + type: 'string', + description: + 'A unique identifier for a user. At least one of `User ID` or `LiveLike User Profile ID` is mandatory.', + default: { + '@path': '$.userId' + } + }, + livelike_profile_id: { + label: 'LiveLike User Profile ID', + description: + 'The unique LiveLike user identifier. Atleast one of `LiveLike User Profile ID` or `User ID` is mandatory.', + type: 'string', + default: { + '@path': '$.properties.livelike_profile_id' + } + }, + timestamp: { + label: 'Timestamp', + description: 'The timestamp of the event.', + type: 'datetime', + required: true, + default: { + '@path': '$.timestamp' + } + }, + properties: { + label: 'Event Properties', + description: 'Properties of the event.', + type: 'object', + default: { + '@path': '$.properties' + } + } + }, + perform: (request, { payload, settings }) => { + if (!settings.clientId || !settings.producerToken) { + throw new IntegrationError('Missing client ID or producer token.') + } + return processData(request, settings, [payload]) + }, + + performBatch: async (request, { settings, payload }) => { + if (!settings.clientId || !settings.producerToken) { + throw new IntegrationError('Missing client ID or producer token.') + } + return processData(request, settings, payload) + } +} + +export default action diff --git a/packages/destination-actions/src/destinations/mixpanel/trackEvent/functions.ts b/packages/destination-actions/src/destinations/mixpanel/trackEvent/functions.ts index 28f6b93e1d..54df5fdee9 100644 --- a/packages/destination-actions/src/destinations/mixpanel/trackEvent/functions.ts +++ b/packages/destination-actions/src/destinations/mixpanel/trackEvent/functions.ts @@ -1,4 +1,3 @@ - import { omit } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' @@ -9,61 +8,61 @@ import { getBrowser, getBrowserVersion, cheapGuid } from '../utils' const mixpanelReservedProperties = ['time', 'id', '$anon_id', 'distinct_id', '$group_id', '$insert_id', '$user_id'] export function getEventProperties(payload: Payload, settings: Settings): MixpanelEventProperties { - const datetime = payload.time - const time = datetime && dayjs.utc(datetime).isValid() ? dayjs.utc(datetime).valueOf() : Date.now() - const utm = payload.utm_properties || {} - let browser, browserVersion - if (payload.userAgent) { - browser = getBrowser(payload.userAgent, payload.device_manufacturer) - browserVersion = getBrowserVersion(payload.userAgent, payload.device_manufacturer) - } - const integration = payload.context?.integration as Record - return { - time: time, - ip: payload.ip, - id: payload.distinct_id, - $anon_id: payload.anonymous_id, - distinct_id: payload.distinct_id, - $app_build_number: payload.app_build, - $app_version_string: payload.app_version, - $app_namespace: payload.app_namespace, - $app_name: payload.app_name, - $browser: browser, - $browser_version: browserVersion, - $bluetooth_enabled: payload.bluetooth, - $cellular_enabled: payload.cellular, - $carrier: payload.carrier, - $current_url: payload.url, - $device: payload.device_name, - $device_id: payload.device_id, - $device_type: payload.device_type, - $device_name: payload.device_name, - $group_id: payload.group_id, - $identified_id: payload.user_id, - $insert_id: payload.insert_id ?? cheapGuid(), - $ios_ifa: payload.idfa, - $lib_version: payload.library_version, - $locale: payload.language, - $manufacturer: payload.device_manufacturer, - $model: payload.device_model, - $os: payload.os_name, - $os_version: payload.os_version, - $referrer: payload.referrer, - $screen_height: payload.screen_height, - $screen_width: payload.screen_width, - $screen_density: payload.screen_density, - $source: integration?.name == "Iterable" ? "Iterable" : 'segment', - $user_id: payload.user_id, - $wifi_enabled: payload.wifi, - mp_country_code: payload.country, - mp_lib: payload.library_name && `Segment Actions: ${ payload.library_name }`, - segment_source_name: settings.sourceName, - utm_campaign: utm.utm_campaign, - utm_content: utm.utm_content, - utm_medium: utm.utm_medium, - utm_source: utm.utm_source, - utm_term: utm.utm_term, - // Ignore Mixpanel reserved properties - ...omit(payload.event_properties, mixpanelReservedProperties) - } -} \ No newline at end of file + const datetime = payload.time + const time = datetime && dayjs.utc(datetime).isValid() ? dayjs.utc(datetime).valueOf() : Date.now() + const utm = payload.utm_properties || {} + let browser, browserVersion + if (payload.userAgent) { + browser = getBrowser(payload.userAgent) + browserVersion = getBrowserVersion(payload.userAgent) + } + const integration = payload.context?.integration as Record + return { + time: time, + ip: payload.ip, + id: payload.distinct_id, + $anon_id: payload.anonymous_id, + distinct_id: payload.distinct_id, + $app_build_number: payload.app_build, + $app_version_string: payload.app_version, + $app_namespace: payload.app_namespace, + $app_name: payload.app_name, + $browser: browser, + $browser_version: browserVersion, + $bluetooth_enabled: payload.bluetooth, + $cellular_enabled: payload.cellular, + $carrier: payload.carrier, + $current_url: payload.url, + $device: payload.device_name, + $device_id: payload.device_id, + $device_type: payload.device_type, + $device_name: payload.device_name, + $group_id: payload.group_id, + $identified_id: payload.user_id, + $insert_id: payload.insert_id ?? cheapGuid(), + $ios_ifa: payload.idfa, + $lib_version: payload.library_version, + $locale: payload.language, + $manufacturer: payload.device_manufacturer, + $model: payload.device_model, + $os: payload.os_name, + $os_version: payload.os_version, + $referrer: payload.referrer, + $screen_height: payload.screen_height, + $screen_width: payload.screen_width, + $screen_density: payload.screen_density, + $source: integration?.name == 'Iterable' ? 'Iterable' : 'segment', + $user_id: payload.user_id, + $wifi_enabled: payload.wifi, + mp_country_code: payload.country, + mp_lib: payload.library_name && `Segment Actions: ${payload.library_name}`, + segment_source_name: settings.sourceName, + utm_campaign: utm.utm_campaign, + utm_content: utm.utm_content, + utm_medium: utm.utm_medium, + utm_source: utm.utm_source, + utm_term: utm.utm_term, + // Ignore Mixpanel reserved properties + ...omit(payload.event_properties, mixpanelReservedProperties) + } +} diff --git a/packages/destination-actions/src/destinations/mixpanel/utils.ts b/packages/destination-actions/src/destinations/mixpanel/utils.ts index e21eb8ea23..53c8c2f28a 100644 --- a/packages/destination-actions/src/destinations/mixpanel/utils.ts +++ b/packages/destination-actions/src/destinations/mixpanel/utils.ts @@ -9,9 +9,7 @@ export enum StrictMode { } export function getConcatenatedName(firstName: unknown, lastName: unknown, name: unknown): unknown { - return ( - name ?? (firstName && lastName ? `${ firstName } ${ lastName }` : undefined) - ) + return name ?? (firstName && lastName ? `${firstName} ${lastName}` : undefined) } export function getApiServerUrl(apiRegion: string | undefined) { @@ -21,8 +19,7 @@ export function getApiServerUrl(apiRegion: string | undefined) { return 'https://api.mixpanel.com' } -export function getBrowser(userAgent: string, vendor: string | undefined): string { - vendor = vendor || '' // vendor is undefined for at least IE9 +export function getBrowser(userAgent: string): string { if (userAgent.includes(' OPR/')) { if (userAgent.includes('Mini')) { return 'Opera Mini' @@ -47,10 +44,7 @@ export function getBrowser(userAgent: string, vendor: string | undefined): strin return 'UC Browser' } else if (userAgent.includes('FxiOS')) { return 'Firefox iOS' - } else if (vendor.includes('Apple')) { - if (userAgent.includes('Mobile')) { - return 'Mobile Safari' - } + } else if (userAgent.includes('Safari')) { return 'Safari' } else if (userAgent.includes('Android')) { return 'Android Mobile' @@ -67,8 +61,8 @@ export function getBrowser(userAgent: string, vendor: string | undefined): strin } } -export function getBrowserVersion(userAgent: string, vendor: string | undefined) { - const browser = getBrowser(userAgent, vendor) +export function getBrowserVersion(userAgent: string) { + const browser = getBrowser(userAgent) const versionRegexs: { [browser: string]: RegExp } = { 'Internet Explorer Mobile': /rv:(\d+(\.\d+)?)/, 'Microsoft Edge': /Edge?\/(\d+(\.\d+)?)/, diff --git a/packages/destination-actions/src/destinations/pinterest-conversions/__tests__/index.test.ts b/packages/destination-actions/src/destinations/pinterest-conversions/__tests__/index.test.ts new file mode 100644 index 0000000000..4e892fb65c --- /dev/null +++ b/packages/destination-actions/src/destinations/pinterest-conversions/__tests__/index.test.ts @@ -0,0 +1,22 @@ +import nock from 'nock' +import { createTestIntegration } from '@segment/actions-core' +import Definition from '../index' +import { Settings } from '../generated-types' + +const testDestination = createTestIntegration(Definition) + +describe('Pinterest Conversions Api', () => { + describe('testAuthentication', () => { + it('should validate authentication inputs', async () => { + nock('https://your.destination.endpoint').get('*').reply(200, {}) + + // This should match your authentication.fields + const authData: Settings = { + ad_account_id: 'ad_account_id', + conversion_token: 'test_token' + } + + await expect(testDestination.testAuthentication(authData)).resolves.not.toThrowError() + }) + }) +}) diff --git a/packages/destination-actions/src/destinations/pinterest-conversions/generated-types.ts b/packages/destination-actions/src/destinations/pinterest-conversions/generated-types.ts new file mode 100644 index 0000000000..a641cb8a84 --- /dev/null +++ b/packages/destination-actions/src/destinations/pinterest-conversions/generated-types.ts @@ -0,0 +1,12 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Settings { + /** + * Unique identifier of an ad account. This can be found in the Pinterest UI by following the steps mentioned [here](https://developers.pinterest.com/docs/conversions/conversion-management/#Finding%20your%20%2Cad_account_id). + */ + ad_account_id: string + /** + * The conversion token for your Pinterest account. This can be found in the Pinterest UI by following the steps mentioned [here](https://developers.pinterest.com/docs/conversions/conversion-management/#Authenticating%20for%20the%20send%20conversion%20events%20endpoint). + */ + conversion_token: string +} diff --git a/packages/destination-actions/src/destinations/pinterest-conversions/index.ts b/packages/destination-actions/src/destinations/pinterest-conversions/index.ts new file mode 100644 index 0000000000..972b69da3a --- /dev/null +++ b/packages/destination-actions/src/destinations/pinterest-conversions/index.ts @@ -0,0 +1,41 @@ +import type { DestinationDefinition } from '@segment/actions-core' +import type { Settings } from './generated-types' + +import reportConversionEvent from './reportConversionEvent' + +const destination: DestinationDefinition = { + name: 'Pinterest Conversions API', + slug: 'actions-pinterest-conversions-api', + mode: 'cloud', + + authentication: { + scheme: 'custom', + fields: { + ad_account_id: { + label: 'Ad Account ID', + description: + 'Unique identifier of an ad account. This can be found in the Pinterest UI by following the steps mentioned [here](https://developers.pinterest.com/docs/conversions/conversion-management/#Finding%20your%20%2Cad_account_id).', + type: 'string', + required: true + }, + conversion_token: { + label: 'Conversion Token', + description: + 'The conversion token for your Pinterest account. This can be found in the Pinterest UI by following the steps mentioned [here](https://developers.pinterest.com/docs/conversions/conversion-management/#Authenticating%20for%20the%20send%20conversion%20events%20endpoint).', + type: 'password', + required: true + } + }, + testAuthentication: (_request) => { + // Return a request that tests/validates the user's credentials. + // If you do not have a way to validate the authentication fields safely, + // you can remove the `testAuthentication` function, though discouraged. + } + }, + + actions: { + reportConversionEvent + } +} + +export default destination diff --git a/packages/destination-actions/src/destinations/pinterest-conversions/reportConversionEvent/generated-types.ts b/packages/destination-actions/src/destinations/pinterest-conversions/reportConversionEvent/generated-types.ts new file mode 100644 index 0000000000..944d22b085 --- /dev/null +++ b/packages/destination-actions/src/destinations/pinterest-conversions/reportConversionEvent/generated-types.ts @@ -0,0 +1,3 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload {} diff --git a/packages/destination-actions/src/destinations/pinterest-conversions/reportConversionEvent/index.ts b/packages/destination-actions/src/destinations/pinterest-conversions/reportConversionEvent/index.ts new file mode 100644 index 0000000000..f5b733c5c2 --- /dev/null +++ b/packages/destination-actions/src/destinations/pinterest-conversions/reportConversionEvent/index.ts @@ -0,0 +1,18 @@ +import type { ActionDefinition } from '@segment/actions-core' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' + +const action: ActionDefinition = { + title: 'Report Conversion Event', + description: 'TODO', + fields: {}, + perform: () => { + // Make your partner api request here! + // return request('https://example.com', { + // method: 'post', + // json: data.payload + // }) + } +} + +export default action diff --git a/packages/destination-actions/src/destinations/qualtrics/upsertContactTransaction/__tests__/index.test.ts b/packages/destination-actions/src/destinations/qualtrics/upsertContactTransaction/__tests__/index.test.ts index 957a6d1a0b..0b3e8ab2a1 100644 --- a/packages/destination-actions/src/destinations/qualtrics/upsertContactTransaction/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/qualtrics/upsertContactTransaction/__tests__/index.test.ts @@ -1,6 +1,10 @@ import nock from 'nock' import { createTestEvent, createTestIntegration } from '@segment/actions-core' import Destination from '../../index' +import dayjs from '../../../../lib/dayjs' + +// Create a date object from the current time +const dateObj = dayjs() const testDestination = createTestIntegration(Destination) const SETTINGS = { @@ -11,7 +15,10 @@ const DIRECTORY_ID = 'POOL_XXXX' const CONTACT_ID = 'CID_XXXX' const MAILING_LIST_ID = 'CG_XXXX' const TRANSACTION_DATA = { Key: 'Value' } -const TRANSACTION_DATE = '2000-01-01 00:00:00' +// Convert the date object to formatted string +const TRANSACTION_DATE = dateObj.format('YYYY-MM-DD HH:mm:ss') +// Convert the date object to formatted string in UTC +const TRANSACTION_DATE_UTC = dateObj.utc().format('YYYY-MM-DD HH:mm:ss') const CONTACT_INFO = { firstName: 'Jane', lastName: 'Doe', @@ -77,7 +84,7 @@ describe('upsertContactTransaction', () => { expect(actualRequest[Object.keys(actualRequest)[0]]).toMatchObject({ contactId: CONTACT_ID, data: TRANSACTION_DATA, - transactionDate: TRANSACTION_DATE, + transactionDate: TRANSACTION_DATE_UTC, mailingListId: MAILING_LIST_ID }) }) @@ -140,7 +147,7 @@ describe('upsertContactTransaction', () => { expect(actualCreateTransactionRequest[Object.keys(actualCreateTransactionRequest)[0]]).toMatchObject({ contactId: 'CID_FOUND', data: TRANSACTION_DATA, - transactionDate: TRANSACTION_DATE, + transactionDate: TRANSACTION_DATE_UTC, mailingListId: MAILING_LIST_ID }) }) @@ -232,7 +239,7 @@ describe('upsertContactTransaction', () => { expect(actualCreateTransactionRequest[Object.keys(actualCreateTransactionRequest)[0]]).toMatchObject({ contactId: 'CID_CREATED', data: TRANSACTION_DATA, - transactionDate: TRANSACTION_DATE, + transactionDate: TRANSACTION_DATE_UTC, mailingListId: MAILING_LIST_ID }) }) diff --git a/packages/destination-actions/src/destinations/ripe/group/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/ripe/group/__tests__/__snapshots__/snapshot.test.ts.snap index ce05424896..d8a7474c6b 100644 --- a/packages/destination-actions/src/destinations/ripe/group/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/ripe/group/__tests__/__snapshots__/snapshot.test.ts.snap @@ -4,7 +4,7 @@ exports[`Testing snapshot for Ripe's group destination action: all fields 1`] = Object { "anonymousId": "DY&TUQqmpg2I", "groupId": "DY&TUQqmpg2I", - "messageId": "2159444d-cdb6-52df-84de-11853deccb7e", + "messageId": Any, "timestamp": Any, "traits": Object { "testType": "DY&TUQqmpg2I", @@ -17,6 +17,7 @@ exports[`Testing snapshot for Ripe's group destination action: required fields 1 Object { "anonymousId": "anonId1234", "groupId": "DY&TUQqmpg2I", + "messageId": Any, "timestamp": Any, "traits": Object {}, "userId": "user1234", diff --git a/packages/destination-actions/src/destinations/ripe/group/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/ripe/group/__tests__/snapshot.test.ts index 0a2ac58441..dcd9a44f62 100644 --- a/packages/destination-actions/src/destinations/ripe/group/__tests__/snapshot.test.ts +++ b/packages/destination-actions/src/destinations/ripe/group/__tests__/snapshot.test.ts @@ -35,6 +35,7 @@ describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination ac try { const json = JSON.parse(rawBody) expect(json).toMatchSnapshot({ + messageId: expect.any(String), timestamp: expect.any(String) }) return @@ -70,6 +71,7 @@ describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination ac try { const json = JSON.parse(rawBody) expect(json).toMatchSnapshot({ + messageId: expect.any(String), timestamp: expect.any(String) }) return diff --git a/packages/destination-actions/src/destinations/ripe/group/index.ts b/packages/destination-actions/src/destinations/ripe/group/index.ts index 1ad321a733..d589ea34d6 100644 --- a/packages/destination-actions/src/destinations/ripe/group/index.ts +++ b/packages/destination-actions/src/destinations/ripe/group/index.ts @@ -49,7 +49,7 @@ const action: ActionDefinition = { required: false, description: 'The Segment messageId', label: 'MessageId', - default: { '@path': '$messageId' } + default: { '@path': '$.messageId' } } }, perform: (request, { payload, settings }) => { diff --git a/packages/destination-actions/src/destinations/ripe/identify/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/ripe/identify/__tests__/__snapshots__/snapshot.test.ts.snap index 02a7fc983f..a2b7ec5b44 100644 --- a/packages/destination-actions/src/destinations/ripe/identify/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/ripe/identify/__tests__/__snapshots__/snapshot.test.ts.snap @@ -6,7 +6,7 @@ Object { "context": Object { "groupId": "ojywl7GMKZU*opR%8", }, - "messageId": "7dbfec72-3264-59dd-8983-99027cd3e162", + "messageId": Any, "timestamp": Any, "traits": Object { "testType": "ojywl7GMKZU*opR%8", @@ -19,6 +19,7 @@ exports[`Testing snapshot for Ripe's identify destination action: required field Object { "anonymousId": "anonId1234", "context": Object {}, + "messageId": Any, "timestamp": Any, "traits": Object {}, "userId": "user1234", diff --git a/packages/destination-actions/src/destinations/ripe/identify/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/ripe/identify/__tests__/snapshot.test.ts index 7fe776509f..28adf14208 100644 --- a/packages/destination-actions/src/destinations/ripe/identify/__tests__/snapshot.test.ts +++ b/packages/destination-actions/src/destinations/ripe/identify/__tests__/snapshot.test.ts @@ -35,6 +35,7 @@ describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination ac try { const json = JSON.parse(rawBody) expect(json).toMatchSnapshot({ + messageId: expect.any(String), timestamp: expect.any(String) }) return @@ -70,6 +71,7 @@ describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination ac try { const json = JSON.parse(rawBody) expect(json).toMatchSnapshot({ + messageId: expect.any(String), timestamp: expect.any(String) }) return diff --git a/packages/destination-actions/src/destinations/ripe/identify/index.ts b/packages/destination-actions/src/destinations/ripe/identify/index.ts index f4138a4a2b..f307401057 100644 --- a/packages/destination-actions/src/destinations/ripe/identify/index.ts +++ b/packages/destination-actions/src/destinations/ripe/identify/index.ts @@ -49,7 +49,7 @@ const action: ActionDefinition = { required: false, description: 'The Segment messageId', label: 'MessageId', - default: { '@path': '$messageId' } + default: { '@path': '$.messageId' } } }, perform: (request, { payload, settings }) => { diff --git a/packages/destination-actions/src/destinations/ripe/page/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/ripe/page/__tests__/__snapshots__/snapshot.test.ts.snap index 24fe7c548c..38e143f818 100644 --- a/packages/destination-actions/src/destinations/ripe/page/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/ripe/page/__tests__/__snapshots__/snapshot.test.ts.snap @@ -13,7 +13,7 @@ Object { "url": "http://fujav.cx/ef", }, }, - "messageId": "22c96ec8-da18-5ccf-90bb-f92dfd44a98e", + "messageId": Any, "name": "Gm^2vj@raF9m", "properties": Object { "testType": "Gm^2vj@raF9m", @@ -33,6 +33,7 @@ Object { "url": "https://segment.com/academy/", }, }, + "messageId": Any, "properties": Object {}, "timestamp": Any, "userId": "user1234", diff --git a/packages/destination-actions/src/destinations/ripe/page/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/ripe/page/__tests__/snapshot.test.ts index c68680b31d..f685d0f363 100644 --- a/packages/destination-actions/src/destinations/ripe/page/__tests__/snapshot.test.ts +++ b/packages/destination-actions/src/destinations/ripe/page/__tests__/snapshot.test.ts @@ -35,6 +35,7 @@ describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination ac try { const json = JSON.parse(rawBody) expect(json).toMatchSnapshot({ + messageId: expect.any(String), timestamp: expect.any(String) }) return @@ -71,6 +72,7 @@ describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination ac try { const json = JSON.parse(rawBody) expect(json).toMatchSnapshot({ + messageId: expect.any(String), timestamp: expect.any(String) }) return diff --git a/packages/destination-actions/src/destinations/ripe/page/index.ts b/packages/destination-actions/src/destinations/ripe/page/index.ts index 536541fdf7..736d6288bd 100644 --- a/packages/destination-actions/src/destinations/ripe/page/index.ts +++ b/packages/destination-actions/src/destinations/ripe/page/index.ts @@ -124,7 +124,7 @@ const action: ActionDefinition = { required: false, description: 'The Segment messageId', label: 'MessageId', - default: { '@path': '$messageId' } + default: { '@path': '$.messageId' } } }, perform: (request, { payload, settings }) => { diff --git a/packages/destination-actions/src/destinations/ripe/track/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/ripe/track/__tests__/__snapshots__/snapshot.test.ts.snap index b00d044064..edc1a6a5b0 100644 --- a/packages/destination-actions/src/destinations/ripe/track/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/ripe/track/__tests__/__snapshots__/snapshot.test.ts.snap @@ -7,7 +7,7 @@ Object { "groupId": "f6t8rQsKdU^N", }, "event": "f6t8rQsKdU^N", - "messageId": "2b7e8d4e-2a59-53b3-b51a-8830d6a1ceea", + "messageId": Any, "name": "f6t8rQsKdU^N", "properties": Object { "testType": "f6t8rQsKdU^N", @@ -22,6 +22,7 @@ Object { "anonymousId": "anonId1234", "context": Object {}, "event": "f6t8rQsKdU^N", + "messageId": Any, "name": "f6t8rQsKdU^N", "properties": Object { "event": "f6t8rQsKdU^N", diff --git a/packages/destination-actions/src/destinations/ripe/track/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/ripe/track/__tests__/snapshot.test.ts index 1b268d0f9b..8cf3703059 100644 --- a/packages/destination-actions/src/destinations/ripe/track/__tests__/snapshot.test.ts +++ b/packages/destination-actions/src/destinations/ripe/track/__tests__/snapshot.test.ts @@ -35,6 +35,7 @@ describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination ac try { const json = JSON.parse(rawBody) expect(json).toMatchSnapshot({ + messageId: expect.any(String), timestamp: expect.any(String) }) return @@ -70,6 +71,7 @@ describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination ac try { const json = JSON.parse(rawBody) expect(json).toMatchSnapshot({ + messageId: expect.any(String), timestamp: expect.any(String) }) return diff --git a/packages/destination-actions/src/destinations/ripe/track/index.ts b/packages/destination-actions/src/destinations/ripe/track/index.ts index 6493cff797..ed73ca7579 100644 --- a/packages/destination-actions/src/destinations/ripe/track/index.ts +++ b/packages/destination-actions/src/destinations/ripe/track/index.ts @@ -56,7 +56,7 @@ const action: ActionDefinition = { required: false, description: 'The Segment messageId', label: 'MessageId', - default: { '@path': '$messageId' } + default: { '@path': '$.messageId' } } }, perform: (request, { payload, settings }) => { diff --git a/packages/destination-actions/src/destinations/saleswings/__tests__/index.test.ts b/packages/destination-actions/src/destinations/saleswings/__tests__/index.test.ts index 13a035dc99..2891e64a3c 100644 --- a/packages/destination-actions/src/destinations/saleswings/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/saleswings/__tests__/index.test.ts @@ -1,21 +1,26 @@ import nock from 'nock' import { createTestIntegration } from '@segment/actions-core' import Definition from '../index' -import { apiBaseUrl } from '../api' +import { getAccountUrl } from '../api' const testDestination = createTestIntegration(Definition) describe('Saleswings', () => { + const env = 'helium' describe('testAuthentication', () => { it('should validate authentication inputs', async () => { - nock(apiBaseUrl).get('/project/account').matchHeader('authorization', 'Bearer myApiKey').reply(200, {}) - await expect(testDestination.testAuthentication({ apiKey: 'myApiKey' })).resolves.not.toThrowError() + nock(getAccountUrl(env)).get('').matchHeader('authorization', 'Bearer myApiKey').reply(200, {}) + await expect( + testDestination.testAuthentication({ apiKey: 'myApiKey', environment: env }) + ).resolves.not.toThrowError() }) it('should reject invalid API key', async () => { - nock(apiBaseUrl).get('/project/account').matchHeader('authorization', 'Bearer myApiKey').reply(200, {}) - nock(apiBaseUrl).get('/project/account').reply(401, {}) - await expect(testDestination.testAuthentication({ apiKey: 'invalidApiKey' })).rejects.toThrowError() + nock(getAccountUrl(env)).get('').matchHeader('authorization', 'Bearer myApiKey').reply(200, {}) + nock(getAccountUrl(env)).get('').reply(401, {}) + await expect( + testDestination.testAuthentication({ apiKey: 'invalidApiKey', environment: env }) + ).rejects.toThrowError() }) }) }) diff --git a/packages/destination-actions/src/destinations/saleswings/api.ts b/packages/destination-actions/src/destinations/saleswings/api.ts index c8f6703ee2..82f7f11106 100644 --- a/packages/destination-actions/src/destinations/saleswings/api.ts +++ b/packages/destination-actions/src/destinations/saleswings/api.ts @@ -1,46 +1,7 @@ -export const apiBaseUrl = 'https://helium.saleswings.pro/api/core' +export type EventType = 'track' | 'identify' | 'page' | 'screen' -export type Event = TrackingEvent | PageVisitEvent - -export type EventBatch = { - events: Event[] -} - -export class TrackingEvent { - leadRefs: LeadRef[] - kind: string - data: string - url?: string - referrerUrl?: string - userAgent?: string - timestamp: number - values: ValueMap - readonly type: string = 'tracking' - - public constructor(fields?: Partial) { - Object.assign(this, fields) - } -} - -export class PageVisitEvent { - leadRefs: LeadRef[] - url: string - referrerUrl?: string - userAgent?: string - timestamp: number - readonly type: string = 'page-visit' - - public constructor(fields?: Partial) { - Object.assign(this, fields) - } -} - -export type Value = string | number | boolean -export type ValueMap = { [k: string]: Value } - -export type LeadRefType = 'email' | 'client-id' - -export type LeadRef = { - type: LeadRefType - value: string -} +export const submitEventUrl = (env: string, eventType: EventType): string => + `https://${env}.saleswings.pro/api/segment/events/${eventType}` +export const submitEventBatchUrl = (env: string, eventType: EventType): string => + `https://${env}.saleswings.pro/api/segment/events/${eventType}/batches` +export const getAccountUrl = (env: string): string => `https://${env}.saleswings.pro/api/core/project/account` diff --git a/packages/destination-actions/src/destinations/saleswings/common.ts b/packages/destination-actions/src/destinations/saleswings/common.ts index 6c2fd8235f..1a9f26a898 100644 --- a/packages/destination-actions/src/destinations/saleswings/common.ts +++ b/packages/destination-actions/src/destinations/saleswings/common.ts @@ -1,38 +1,23 @@ import { RequestFn } from '@segment/actions-core' -import { apiBaseUrl, Event, EventBatch } from './api' +import { submitEventUrl, submitEventBatchUrl, EventType } from './api' import { Settings } from './generated-types' -export function perform(convertEvent: (payload: Payload) => Event | undefined): RequestFn { +export function perform(eventType: EventType): RequestFn { return (request, data) => { - const event = convertEvent(data.payload) - if (!event) return - return request(`${apiBaseUrl}/events`, { + return request(submitEventUrl(data.settings.environment, eventType), { method: 'post', - json: event, + json: data.payload, headers: { Authorization: `Bearer ${data.settings.apiKey}` } }) } } -export function performBatch( - convertEvent: (payload: Payload) => Event | undefined -): RequestFn { +export function performBatch(eventType: EventType): RequestFn { return (request, data) => { - const batch = convertEventBatch(data.payload, convertEvent) - if (!batch) return - return request(`${apiBaseUrl}/events/batches`, { + return request(submitEventBatchUrl(data.settings.environment, eventType), { method: 'post', - json: batch, + json: data.payload, headers: { Authorization: `Bearer ${data.settings.apiKey}` } }) } } - -function convertEventBatch( - payloads: Payload[], - convertEvent: (payload: Payload) => Event | undefined -): EventBatch | undefined { - const events = payloads.map(convertEvent).filter((evt) => evt) as Event[] - if (events.length == 0) return undefined - return { events } -} diff --git a/packages/destination-actions/src/destinations/saleswings/fields.ts b/packages/destination-actions/src/destinations/saleswings/fields.ts index 932e5652c7..6e556340ea 100644 --- a/packages/destination-actions/src/destinations/saleswings/fields.ts +++ b/packages/destination-actions/src/destinations/saleswings/fields.ts @@ -1,7 +1,7 @@ import { InputField } from '@segment/actions-core' import { Directive } from '@segment/actions-core/src/destination-kit/types' -export const userId: InputField = { +export const userID: InputField = { label: 'Segment User ID', description: 'Permanent identifier of a Segment user the event is attributed to.', type: 'string', @@ -11,7 +11,7 @@ export const userId: InputField = { } } -export const anonymousId: InputField = { +export const anonymousID: InputField = { label: 'Segment Anonymous User ID', description: 'A pseudo-unique substitute for a Segment user ID the event is attributed to.', type: 'string', diff --git a/packages/destination-actions/src/destinations/saleswings/generated-types.ts b/packages/destination-actions/src/destinations/saleswings/generated-types.ts index f0eb97ee46..9448251b8b 100644 --- a/packages/destination-actions/src/destinations/saleswings/generated-types.ts +++ b/packages/destination-actions/src/destinations/saleswings/generated-types.ts @@ -5,4 +5,8 @@ export interface Settings { * Segment.io API key for your SalesWings project. */ apiKey: string + /** + * SalesWings environment this destination is connected with. + */ + environment: string } diff --git a/packages/destination-actions/src/destinations/saleswings/index.ts b/packages/destination-actions/src/destinations/saleswings/index.ts index e85287f3f3..ee168071d3 100644 --- a/packages/destination-actions/src/destinations/saleswings/index.ts +++ b/packages/destination-actions/src/destinations/saleswings/index.ts @@ -1,5 +1,5 @@ import { defaultValues, DestinationDefinition } from '@segment/actions-core' -import { apiBaseUrl } from './api' +import { getAccountUrl } from './api' import type { Settings } from './generated-types' import submitTrackEvent from './submitTrackEvent' @@ -20,13 +20,23 @@ const destination: DestinationDefinition = { description: 'Segment.io API key for your SalesWings project.', type: 'password', required: true + }, + environment: { + label: 'Environment', + description: 'SalesWings environment this destination is connected with.', + type: 'string', + choices: [ + { value: 'helium', label: 'Helium (live environment)' }, + { value: 'ozone', label: 'Ozone (test environment)' } + ], + required: true, + default: 'helium' } }, testAuthentication: async (request, { settings }) => { - const resp = await request(`${apiBaseUrl}/project/account`, { + return request(getAccountUrl('helium'), { headers: { Authorization: `Bearer ${settings.apiKey}` } }) - return resp.status == 200 } }, diff --git a/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/__tests__/__snapshots__/snapshot.test.ts.snap index 68160c6e62..8be8985ce0 100644 --- a/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/__tests__/__snapshots__/snapshot.test.ts.snap @@ -1,55 +1,31 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Testing snapshot for 's submitIdentifyEvent destination action: all fields 1`] = ` +exports[`Testing snapshot for saleswings's submitIdentifyEvent destination action: all fields 1`] = ` Object { + "anonymousID": "anonId1234", "data": "peter@example.com", + "email": "peter@example.com", "kind": "Identify", - "leadRefs": Array [ - Object { - "type": "client-id", - "value": "user1234", - }, - Object { - "type": "client-id", - "value": "anonId1234", - }, - Object { - "type": "email", - "value": "peter@example.com", - }, - ], - "timestamp": 1671371213652, - "type": "tracking", - "url": "Cge8DWzjce", + "timestamp": "2022-12-18T13:46:53.652Z", + "url": "MrDZodCq([l&%Xby8@jL", "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1", + "userID": "user1234", "values": Object { "email": "peter@example.com", }, } `; -exports[`Testing snapshot for 's submitIdentifyEvent destination action: required fields 1`] = ` +exports[`Testing snapshot for saleswings's submitIdentifyEvent destination action: required fields 1`] = ` Object { + "anonymousID": "anonId1234", "data": "peter@example.com", + "email": "peter@example.com", "kind": "Identify", - "leadRefs": Array [ - Object { - "type": "client-id", - "value": "user1234", - }, - Object { - "type": "client-id", - "value": "anonId1234", - }, - Object { - "type": "email", - "value": "peter@example.com", - }, - ], - "timestamp": 1671371213632, - "type": "tracking", + "timestamp": "2022-12-18T13:46:53.632Z", "url": "https://segment.com/academy/", "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1", + "userID": "user1234", "values": Object { "email": "peter@example.com", }, diff --git a/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/__tests__/index.test.ts b/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/__tests__/index.test.ts index e77259a25b..c98c5200cb 100644 --- a/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/__tests__/index.test.ts @@ -1,5 +1,5 @@ import { createTestEvent } from '@segment/actions-core' -import { expectedTs, testAction, testBatchAction, userAgent } from '../../testing' +import { testAction, testBatchAction, userAgent } from '../../testing' const actionName = 'submitIdentifyEvent' @@ -24,20 +24,18 @@ describe('SalesWings', () => { }) const request = await testAction(actionName, event) expect(request).toMatchObject({ - type: 'tracking', - leadRefs: [ - { type: 'client-id', value: event.userId }, - { type: 'client-id', value: event.anonymousId }, - { type: 'email', value: event.traits?.email } - ], + userID: event.userId, + anonymousID: event.anonymousId, + email: event.traits?.email, kind: 'Identify', data: 'peter@example.com', url: 'https://example.com', referrerUrl: 'https://example.com/other', userAgent, - timestamp: expectedTs(event.timestamp), + timestamp: event.timestamp, values: { name: 'Peter Gibbons', + email: 'peter@example.com', plan: 'premium', logins: 5 } @@ -53,35 +51,12 @@ describe('SalesWings', () => { }) const request = await testAction(actionName, event) expect(request).toMatchObject({ - type: 'tracking', - leadRefs: [ - { type: 'client-id', value: event.userId }, - { type: 'client-id', value: event.anonymousId }, - { type: 'email', value: event.traits?.email } - ], - kind: 'Identify', - data: 'peter@example.com', - timestamp: expectedTs(event.timestamp), - values: {} - }) - }) - - it('should not skip an event without ids', async () => { - const event = createTestEvent({ - type: 'identify', - traits: { - email: 'peter@example.com' - }, - userId: undefined, - anonymousId: undefined - }) - const request = await testAction(actionName, event) - expect(request).toMatchObject({ - type: 'tracking', - leadRefs: [{ type: 'email', value: event.traits?.email }], + userID: event.userId, + anonymousID: event.anonymousId, + email: event.traits?.email, kind: 'Identify', data: 'peter@example.com', - timestamp: expectedTs(event.timestamp), + timestamp: event.timestamp, values: {} }) }) @@ -102,34 +77,24 @@ describe('SalesWings', () => { }) ] const request = await testBatchAction(actionName, events) - expect(request).toMatchObject({ - events: [ - { - type: 'tracking', - leadRefs: [ - { type: 'client-id', value: events[0].userId }, - { type: 'client-id', value: events[0].anonymousId }, - { type: 'email', value: events[0].traits?.email } - ], - kind: 'Identify', - data: 'peter@example.com', - timestamp: expectedTs(events[0].timestamp), - values: {} - }, - { - type: 'tracking', - leadRefs: [ - { type: 'client-id', value: events[1].userId }, - { type: 'client-id', value: events[1].anonymousId }, - { type: 'email', value: events[1].traits?.email } - ], - kind: 'Identify', - data: 'frank@example.com', - timestamp: expectedTs(events[1].timestamp), - values: {} - } - ] - }) + expect(request).toMatchObject([ + { + userID: events[0].userId, + anonymousID: events[0].anonymousId, + email: events[0].traits?.email, + kind: 'Identify', + data: 'peter@example.com', + timestamp: events[0].timestamp + }, + { + userID: events[1].userId, + anonymousID: events[1].anonymousId, + email: events[1].traits?.email, + kind: 'Identify', + data: 'frank@example.com', + timestamp: events[1].timestamp + } + ]) }) }) }) diff --git a/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/__tests__/snapshot.test.ts index 362ef386b8..dbb557084a 100644 --- a/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/__tests__/snapshot.test.ts +++ b/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/__tests__/snapshot.test.ts @@ -5,7 +5,7 @@ import nock from 'nock' const testDestination = createTestIntegration(destination) const actionSlug = 'submitIdentifyEvent' -const destinationSlug = '' +const destinationSlug = 'saleswings' const seedName = `${destinationSlug}#${actionSlug}` describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination action:`, () => { @@ -28,7 +28,7 @@ describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination ac const responses = await testDestination.testAction(actionSlug, { event: event, - settings: settingsData, + settings: { ...settingsData, environment: 'helium' }, useDefaultMappings: true }) @@ -65,7 +65,7 @@ describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination ac const responses = await testDestination.testAction(actionSlug, { event: event, - settings: settingsData, + settings: { ...settingsData, environment: 'helium' }, useDefaultMappings: true }) diff --git a/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/generated-types.ts b/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/generated-types.ts index 822f72a4f6..c21bb1294b 100644 --- a/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/generated-types.ts +++ b/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/generated-types.ts @@ -12,11 +12,11 @@ export interface Payload { /** * Permanent identifier of a Segment user the event is attributed to. */ - userId?: string + userID?: string /** * A pseudo-unique substitute for a Segment user ID the event is attributed to. */ - anonymousId?: string + anonymousID?: string /** * Identified email of the Segment User. */ diff --git a/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/index.ts b/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/index.ts index cbbe40bff2..147a703053 100644 --- a/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/index.ts +++ b/packages/destination-actions/src/destinations/saleswings/submitIdentifyEvent/index.ts @@ -1,24 +1,9 @@ import type { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { Event, TrackingEvent } from '../api' -import { userId, anonymousId, email, url, referrerUrl, userAgent, timestamp, kind, data, values } from '../fields' -import { convertLeadRefs, convertValues, convertTimestamp } from '../converter' +import { userID, anonymousID, email, url, referrerUrl, userAgent, timestamp, kind, data, values } from '../fields' import { perform, performBatch } from '../common' -const convertEvent = (payload: Payload): Event | undefined => { - return new TrackingEvent({ - leadRefs: convertLeadRefs(payload), - kind: payload.kind, - data: payload.data, - url: payload.url, - referrerUrl: payload.referrerUrl, - userAgent: payload.userAgent, - timestamp: convertTimestamp(payload.timestamp), - values: convertValues(payload.values) - }) -} - const action: ActionDefinition = { title: 'Submit Identify Event', description: @@ -27,8 +12,8 @@ const action: ActionDefinition = { fields: { kind: kind('Identify'), data: data({ '@path': '$.traits.email' }), - userId, - anonymousId, + userID, + anonymousID, email: { ...email, required: true }, url, referrerUrl, @@ -36,8 +21,8 @@ const action: ActionDefinition = { timestamp, values: values({ '@path': '$.traits' }) }, - perform: perform(convertEvent), - performBatch: performBatch(convertEvent) + perform: perform('identify'), + performBatch: performBatch('identify') } export default action diff --git a/packages/destination-actions/src/destinations/saleswings/submitPageEvent/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/saleswings/submitPageEvent/__tests__/__snapshots__/snapshot.test.ts.snap index cfd485649b..94adf8612a 100644 --- a/packages/destination-actions/src/destinations/saleswings/submitPageEvent/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/saleswings/submitPageEvent/__tests__/__snapshots__/snapshot.test.ts.snap @@ -1,39 +1,21 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Testing snapshot for 's submitPageEvent destination action: all fields 1`] = ` +exports[`Testing snapshot for saleswings's submitPageEvent destination action: all fields 1`] = ` Object { - "leadRefs": Array [ - Object { - "type": "client-id", - "value": "user1234", - }, - Object { - "type": "client-id", - "value": "anonId1234", - }, - ], - "timestamp": 1671371213652, - "type": "page-visit", - "url": "[isj*b()C", + "anonymousID": "anonId1234", + "timestamp": "2022-12-18T13:46:53.652Z", + "url": "BYNuExz1#lNm)rdjv", "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1", + "userID": "user1234", } `; -exports[`Testing snapshot for 's submitPageEvent destination action: required fields 1`] = ` +exports[`Testing snapshot for saleswings's submitPageEvent destination action: required fields 1`] = ` Object { - "leadRefs": Array [ - Object { - "type": "client-id", - "value": "user1234", - }, - Object { - "type": "client-id", - "value": "anonId1234", - }, - ], - "timestamp": 1671371213632, - "type": "page-visit", - "url": "[isj*b()C", + "anonymousID": "anonId1234", + "timestamp": "2022-12-18T13:46:53.632Z", + "url": "BYNuExz1#lNm)rdjv", "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1", + "userID": "user1234", } `; diff --git a/packages/destination-actions/src/destinations/saleswings/submitPageEvent/__tests__/index.test.ts b/packages/destination-actions/src/destinations/saleswings/submitPageEvent/__tests__/index.test.ts index e045b3d1c3..2a7115cacb 100644 --- a/packages/destination-actions/src/destinations/saleswings/submitPageEvent/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/saleswings/submitPageEvent/__tests__/index.test.ts @@ -1,5 +1,5 @@ import { createTestEvent } from '@segment/actions-core' -import { expectedTs, testAction, testActionWithSkippedEvent, testBatchAction, userAgent } from '../../testing' +import { testAction, testBatchAction, userAgent } from '../../testing' const actionName = 'submitPageEvent' @@ -18,15 +18,12 @@ describe('SalesWings', () => { }) const request = await testAction(actionName, event) expect(request).toMatchObject({ - type: 'page-visit', - leadRefs: [ - { type: 'client-id', value: event.userId }, - { type: 'client-id', value: event.anonymousId } - ], + userID: event.userId, + anonymousID: event.anonymousId, url: 'https://example.com', referrerUrl: 'https://example.com/other', userAgent, - timestamp: expectedTs(event.timestamp) + timestamp: event.timestamp }) }) @@ -39,13 +36,10 @@ describe('SalesWings', () => { }) const request = await testAction(actionName, event) expect(request).toMatchObject({ - type: 'page-visit', - leadRefs: [ - { type: 'client-id', value: event.userId }, - { type: 'client-id', value: event.anonymousId } - ], + userID: event.userId, + anonymousID: event.anonymousId, url: 'https://example.com', - timestamp: expectedTs(event.timestamp) + timestamp: event.timestamp }) }) @@ -59,10 +53,9 @@ describe('SalesWings', () => { }) const request = await testAction(actionName, event) expect(request).toMatchObject({ - type: 'page-visit', - leadRefs: [{ type: 'client-id', value: event.userId }], + userID: event.userId, url: 'https://example.com', - timestamp: expectedTs(event.timestamp) + timestamp: event.timestamp }) }) @@ -76,25 +69,12 @@ describe('SalesWings', () => { }) const request = await testAction(actionName, event) expect(request).toMatchObject({ - type: 'page-visit', - leadRefs: [{ type: 'client-id', value: event.anonymousId }], + anonymousID: event.anonymousId, url: 'https://example.com', - timestamp: expectedTs(event.timestamp) + timestamp: event.timestamp }) }) - it('should skip an event without any ids', async () => { - const event = createTestEvent({ - type: 'page', - properties: { - url: 'https://example.com' - }, - anonymousId: undefined, - userId: undefined - }) - await testActionWithSkippedEvent(actionName, event) - }) - it('should submit event batch', async () => { const events = [ createTestEvent({ @@ -111,28 +91,20 @@ describe('SalesWings', () => { }) ] const request = await testBatchAction(actionName, events) - expect(request).toMatchObject({ - events: [ - { - type: 'page-visit', - leadRefs: [ - { type: 'client-id', value: events[0].userId }, - { type: 'client-id', value: events[0].anonymousId } - ], - url: 'https://example.com/01', - timestamp: expectedTs(events[0].timestamp) - }, - { - type: 'page-visit', - leadRefs: [ - { type: 'client-id', value: events[0].userId }, - { type: 'client-id', value: events[0].anonymousId } - ], - url: 'https://example.com/02', - timestamp: expectedTs(events[1].timestamp) - } - ] - }) + expect(request).toMatchObject([ + { + userID: events[0].userId, + anonymousID: events[0].anonymousId, + url: 'https://example.com/01', + timestamp: events[0].timestamp + }, + { + userID: events[1].userId, + anonymousID: events[1].anonymousId, + url: 'https://example.com/02', + timestamp: events[1].timestamp + } + ]) }) }) }) diff --git a/packages/destination-actions/src/destinations/saleswings/submitPageEvent/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/saleswings/submitPageEvent/__tests__/snapshot.test.ts index 372728cbba..b4b744da3e 100644 --- a/packages/destination-actions/src/destinations/saleswings/submitPageEvent/__tests__/snapshot.test.ts +++ b/packages/destination-actions/src/destinations/saleswings/submitPageEvent/__tests__/snapshot.test.ts @@ -5,7 +5,7 @@ import nock from 'nock' const testDestination = createTestIntegration(destination) const actionSlug = 'submitPageEvent' -const destinationSlug = '' +const destinationSlug = 'saleswings' const seedName = `${destinationSlug}#${actionSlug}` describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination action:`, () => { @@ -25,7 +25,7 @@ describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination ac const responses = await testDestination.testAction(actionSlug, { event: event, - settings: settingsData, + settings: { ...settingsData, environment: 'helium' }, useDefaultMappings: true }) @@ -59,7 +59,7 @@ describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination ac const responses = await testDestination.testAction(actionSlug, { event: event, - settings: settingsData, + settings: { ...settingsData, environment: 'helium' }, useDefaultMappings: true }) diff --git a/packages/destination-actions/src/destinations/saleswings/submitPageEvent/generated-types.ts b/packages/destination-actions/src/destinations/saleswings/submitPageEvent/generated-types.ts index 8584bdebe2..2c4adebd76 100644 --- a/packages/destination-actions/src/destinations/saleswings/submitPageEvent/generated-types.ts +++ b/packages/destination-actions/src/destinations/saleswings/submitPageEvent/generated-types.ts @@ -4,11 +4,11 @@ export interface Payload { /** * Permanent identifier of a Segment user the event is attributed to. */ - userId?: string + userID?: string /** * A pseudo-unique substitute for a Segment user ID the event is attributed to. */ - anonymousId?: string + anonymousID?: string /** * URL associated with the event. */ diff --git a/packages/destination-actions/src/destinations/saleswings/submitPageEvent/index.ts b/packages/destination-actions/src/destinations/saleswings/submitPageEvent/index.ts index 3b9d0c134a..1d69b86e30 100644 --- a/packages/destination-actions/src/destinations/saleswings/submitPageEvent/index.ts +++ b/packages/destination-actions/src/destinations/saleswings/submitPageEvent/index.ts @@ -1,38 +1,24 @@ import type { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { Event, PageVisitEvent } from '../api' -import { userId, anonymousId, url, referrerUrl, userAgent, timestamp } from '../fields' -import { convertLeadRefs, convertTimestamp } from '../converter' +import { userID, anonymousID, url, referrerUrl, userAgent, timestamp } from '../fields' import { perform, performBatch } from '../common' -const convertEvent = (payload: Payload): Event | undefined => { - const leadRefs = convertLeadRefs(payload) - if (leadRefs.length == 0) return undefined - return new PageVisitEvent({ - leadRefs, - url: payload.url, - referrerUrl: payload.referrerUrl, - userAgent: payload.userAgent, - timestamp: convertTimestamp(payload.timestamp) - }) -} - const action: ActionDefinition = { title: 'Submit Page Event', description: 'Send your Segment Page events to SalesWings to use them for tagging, scoring and prioritising your leads.', defaultSubscription: 'type = "page"', fields: { - userId, - anonymousId, + userID, + anonymousID, url: { ...url, required: true }, referrerUrl, userAgent, timestamp }, - perform: perform(convertEvent), - performBatch: performBatch(convertEvent) + perform: perform('page'), + performBatch: performBatch('page') } export default action diff --git a/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/__tests__/__snapshots__/snapshot.test.ts.snap index 9b92654bcd..57325609a2 100644 --- a/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/__tests__/__snapshots__/snapshot.test.ts.snap @@ -1,62 +1,44 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Testing snapshot for 's submitScreenEvent destination action: all fields 1`] = ` +exports[`Testing snapshot for saleswings's submitScreenEvent destination action: all fields 1`] = ` Object { + "anonymousID": "anonId1234", "data": "Home", + "email": "itimo@zimul.ad", "kind": "Screen", - "leadRefs": Array [ - Object { - "type": "client-id", - "value": "user1234", - }, - Object { - "type": "client-id", - "value": "anonId1234", - }, - Object { - "type": "email", - "value": "sajvoji@monarwi.pk", - }, - ], - "timestamp": 1671371213652, - "type": "tracking", - "url": "kwF&9l73A", + "timestamp": "2022-12-18T13:46:53.652Z", + "url": "]qLj6Vft5pTqg", "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1", + "userID": "user1234", "values": Object { - "anonymousId": "kwF&9l73A", - "data": "kwF&9l73A", - "email": "sajvoji@monarwi.pk", - "kind": "kwF&9l73A", - "referrerUrl": "kwF&9l73A", + "anonymousID": "]qLj6Vft5pTqg", + "data": "]qLj6Vft5pTqg", + "email": "itimo@zimul.ad", + "kind": "]qLj6Vft5pTqg", + "referrerUrl": "]qLj6Vft5pTqg", "timestamp": "2021-02-01T00:00:00.000Z", - "url": "kwF&9l73A", - "userAgent": "kwF&9l73A", - "userId": "kwF&9l73A", + "url": "]qLj6Vft5pTqg", + "userAgent": "]qLj6Vft5pTqg", + "userID": "]qLj6Vft5pTqg", + "values": Object { + "testType": "]qLj6Vft5pTqg", + }, }, } `; -exports[`Testing snapshot for 's submitScreenEvent destination action: required fields 1`] = ` +exports[`Testing snapshot for saleswings's submitScreenEvent destination action: required fields 1`] = ` Object { + "anonymousID": "anonId1234", "data": "Home", "kind": "Screen", - "leadRefs": Array [ - Object { - "type": "client-id", - "value": "user1234", - }, - Object { - "type": "client-id", - "value": "anonId1234", - }, - ], - "timestamp": 1671371213632, - "type": "tracking", + "timestamp": "2022-12-18T13:46:53.632Z", "url": "https://segment.com/academy/", "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1", + "userID": "user1234", "values": Object { - "data": "kwF&9l73A", - "kind": "kwF&9l73A", + "data": "]qLj6Vft5pTqg", + "kind": "]qLj6Vft5pTqg", }, } `; diff --git a/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/__tests__/index.test.ts b/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/__tests__/index.test.ts index 2d440e8465..4b1ee7b438 100644 --- a/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/__tests__/index.test.ts @@ -1,5 +1,5 @@ import { createTestEvent } from '@segment/actions-core' -import { expectedTs, testAction, testBatchAction, userAgent } from '../../testing' +import { testAction, testBatchAction, userAgent } from '../../testing' const actionName = 'submitScreenEvent' @@ -22,17 +22,14 @@ describe('SalesWings', () => { }) const request = await testAction(actionName, event) expect(request).toMatchObject({ - type: 'tracking', - leadRefs: [ - { type: 'client-id', value: event.userId }, - { type: 'client-id', value: event.anonymousId } - ], + userID: event.userId, + anonymousID: event.anonymousId, kind: 'Screen', data: 'Home', url: 'https://example.com', referrerUrl: 'https://example.com/other', userAgent, - timestamp: expectedTs(event.timestamp), + timestamp: event.timestamp, values: { 'Feed Type': 'private' } @@ -46,15 +43,11 @@ describe('SalesWings', () => { }) const request = await testAction(actionName, event) expect(request).toMatchObject({ - type: 'tracking', - leadRefs: [ - { type: 'client-id', value: event.userId }, - { type: 'client-id', value: event.anonymousId } - ], + userID: event.userId, + anonymousID: event.anonymousId, kind: 'Screen', data: 'Home', - timestamp: expectedTs(event.timestamp), - values: {} + timestamp: event.timestamp }) }) @@ -66,11 +59,10 @@ describe('SalesWings', () => { }) const request = await testAction(actionName, event) expect(request).toMatchObject({ - type: 'tracking', - leadRefs: [{ type: 'client-id', value: event.userId }], + userID: event.userId, kind: 'Screen', data: 'Home', - timestamp: expectedTs(event.timestamp), + timestamp: event.timestamp, values: {} }) }) @@ -83,11 +75,10 @@ describe('SalesWings', () => { }) const request = await testAction(actionName, event) expect(request).toMatchObject({ - type: 'tracking', - leadRefs: [{ type: 'client-id', value: event.anonymousId }], + anonymousID: event.anonymousId, kind: 'Screen', data: 'Home', - timestamp: expectedTs(event.timestamp), + timestamp: event.timestamp, values: {} }) }) @@ -104,32 +95,24 @@ describe('SalesWings', () => { }) ] const request = await testBatchAction(actionName, events) - expect(request).toMatchObject({ - events: [ - { - type: 'tracking', - leadRefs: [ - { type: 'client-id', value: events[0].userId }, - { type: 'client-id', value: events[0].anonymousId } - ], - kind: 'Screen', - data: 'Home', - timestamp: expectedTs(events[0].timestamp), - values: {} - }, - { - type: 'tracking', - leadRefs: [ - { type: 'client-id', value: events[1].userId }, - { type: 'client-id', value: events[1].anonymousId } - ], - kind: 'Screen', - data: 'Orders', - timestamp: expectedTs(events[1].timestamp), - values: {} - } - ] - }) + expect(request).toMatchObject([ + { + userID: events[0].userId, + anonymousID: events[0].anonymousId, + kind: 'Screen', + data: 'Home', + timestamp: events[0].timestamp, + values: {} + }, + { + userID: events[1].userId, + anonymousID: events[1].anonymousId, + kind: 'Screen', + data: 'Orders', + timestamp: events[1].timestamp, + values: {} + } + ]) }) }) }) diff --git a/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/__tests__/snapshot.test.ts index dd59035a05..ec4138ed8d 100644 --- a/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/__tests__/snapshot.test.ts +++ b/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/__tests__/snapshot.test.ts @@ -5,7 +5,7 @@ import nock from 'nock' const testDestination = createTestIntegration(destination) const actionSlug = 'submitScreenEvent' -const destinationSlug = '' +const destinationSlug = 'saleswings' const seedName = `${destinationSlug}#${actionSlug}` describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination action:`, () => { @@ -26,7 +26,7 @@ describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination ac const responses = await testDestination.testAction(actionSlug, { event: event, - settings: settingsData, + settings: { ...settingsData, environment: 'helium' }, useDefaultMappings: true }) @@ -61,7 +61,7 @@ describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination ac const responses = await testDestination.testAction(actionSlug, { event: event, - settings: settingsData, + settings: { ...settingsData, environment: 'helium' }, useDefaultMappings: true }) diff --git a/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/generated-types.ts b/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/generated-types.ts index c06058783e..f794ba160c 100644 --- a/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/generated-types.ts +++ b/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/generated-types.ts @@ -12,11 +12,11 @@ export interface Payload { /** * Permanent identifier of a Segment user the event is attributed to. */ - userId?: string + userID?: string /** * A pseudo-unique substitute for a Segment user ID the event is attributed to. */ - anonymousId?: string + anonymousID?: string /** * Identified email of the Segment User. */ diff --git a/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/index.ts b/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/index.ts index d51d109d66..3b8385acbd 100644 --- a/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/index.ts +++ b/packages/destination-actions/src/destinations/saleswings/submitScreenEvent/index.ts @@ -1,26 +1,9 @@ import type { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { Event, TrackingEvent } from '../api' -import { userId, anonymousId, email, url, referrerUrl, userAgent, timestamp, kind, data, values } from '../fields' -import { convertLeadRefs, convertValues, convertTimestamp } from '../converter' +import { userID, anonymousID, email, url, referrerUrl, userAgent, timestamp, kind, data, values } from '../fields' import { perform, performBatch } from '../common' -const convertEvent = (payload: Payload): Event | undefined => { - const leadRefs = convertLeadRefs(payload) - if (leadRefs.length == 0) return undefined - return new TrackingEvent({ - leadRefs, - kind: payload.kind, - data: payload.data, - url: payload.url, - referrerUrl: payload.referrerUrl, - userAgent: payload.userAgent, - timestamp: convertTimestamp(payload.timestamp), - values: convertValues(payload.values) - }) -} - const action: ActionDefinition = { title: 'Submit Screen Event', description: @@ -29,8 +12,8 @@ const action: ActionDefinition = { fields: { kind: kind('Screen'), data: data({ '@path': '$.name' }), - userId, - anonymousId, + userID, + anonymousID, email, url, referrerUrl, @@ -38,8 +21,8 @@ const action: ActionDefinition = { timestamp, values: values({ '@path': '$.properties' }) }, - perform: perform(convertEvent), - performBatch: performBatch(convertEvent) + perform: perform('screen'), + performBatch: performBatch('screen') } export default action diff --git a/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/__tests__/__snapshots__/snapshot.test.ts.snap index e891e21a75..24d7601433 100644 --- a/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/__tests__/__snapshots__/snapshot.test.ts.snap @@ -1,62 +1,44 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Testing snapshot for 's submitTrackEvent destination action: all fields 1`] = ` +exports[`Testing snapshot for saleswings's submitTrackEvent destination action: all fields 1`] = ` Object { + "anonymousID": "anonId1234", "data": "Test Event", + "email": "acorob@komma.net", "kind": "Track", - "leadRefs": Array [ - Object { - "type": "client-id", - "value": "user1234", - }, - Object { - "type": "client-id", - "value": "anonId1234", - }, - Object { - "type": "email", - "value": "tukifvus@domemci.li", - }, - ], - "timestamp": 1671371213652, - "type": "tracking", - "url": "K[uAM", + "timestamp": "2022-12-18T13:46:53.652Z", + "url": "HL3Gau", "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1", + "userID": "user1234", "values": Object { - "anonymousId": "K[uAM", - "data": "K[uAM", - "email": "tukifvus@domemci.li", - "kind": "K[uAM", - "referrerUrl": "K[uAM", + "anonymousID": "HL3Gau", + "data": "HL3Gau", + "email": "acorob@komma.net", + "kind": "HL3Gau", + "referrerUrl": "HL3Gau", "timestamp": "2021-02-01T00:00:00.000Z", - "url": "K[uAM", - "userAgent": "K[uAM", - "userId": "K[uAM", + "url": "HL3Gau", + "userAgent": "HL3Gau", + "userID": "HL3Gau", + "values": Object { + "testType": "HL3Gau", + }, }, } `; -exports[`Testing snapshot for 's submitTrackEvent destination action: required fields 1`] = ` +exports[`Testing snapshot for saleswings's submitTrackEvent destination action: required fields 1`] = ` Object { + "anonymousID": "anonId1234", "data": "Test Event", "kind": "Track", - "leadRefs": Array [ - Object { - "type": "client-id", - "value": "user1234", - }, - Object { - "type": "client-id", - "value": "anonId1234", - }, - ], - "timestamp": 1671371213632, - "type": "tracking", + "timestamp": "2022-12-18T13:46:53.632Z", "url": "https://segment.com/academy/", "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1", + "userID": "user1234", "values": Object { - "data": "K[uAM", - "kind": "K[uAM", + "data": "HL3Gau", + "kind": "HL3Gau", }, } `; diff --git a/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/__tests__/index.test.ts b/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/__tests__/index.test.ts index 02a7b6dc1d..615fedf3b8 100644 --- a/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/__tests__/index.test.ts @@ -1,12 +1,5 @@ import { createTestEvent } from '@segment/actions-core' -import { - expectedTs, - testAction, - testActionWithSkippedEvent, - testBatchAction, - testBatchActionSkippedEvents, - userAgent -} from '../../testing' +import { testAction, testBatchAction, userAgent } from '../../testing' const actionName = 'submitTrackEvent' @@ -30,17 +23,14 @@ describe('SalesWings', () => { }) const request = await testAction(actionName, event) expect(request).toMatchObject({ - type: 'tracking', - leadRefs: [ - { type: 'client-id', value: event.userId }, - { type: 'client-id', value: event.anonymousId } - ], + userID: event.userId, + anonymousID: event.anonymousId, kind: 'Track', data: 'User Registered', url: 'https://example.com', referrerUrl: 'https://example.com/other', userAgent, - timestamp: expectedTs(event.timestamp), + timestamp: event.timestamp, values: { plan: 'Pro Annual', accountType: 'Facebook' @@ -55,26 +45,12 @@ describe('SalesWings', () => { }) const request = await testAction(actionName, event) expect(request).toMatchObject({ - type: 'tracking', - leadRefs: [ - { type: 'client-id', value: event.userId }, - { type: 'client-id', value: event.anonymousId } - ], + userID: event.userId, + anonymousID: event.anonymousId, kind: 'Track', data: 'User Registered', - timestamp: expectedTs(event.timestamp), - values: {} - }) - }) - - it('should skip event without any id', async () => { - const event = createTestEvent({ - type: 'track', - event: 'User Registered', - userId: undefined, - anonymousId: undefined + timestamp: event.timestamp }) - await testActionWithSkippedEvent(actionName, event) }) it('should submit event on Track event with email in properties', async () => { @@ -87,15 +63,12 @@ describe('SalesWings', () => { }) const request = await testAction(actionName, event) expect(request).toMatchObject({ - type: 'tracking', - leadRefs: [ - { type: 'client-id', value: event.userId }, - { type: 'client-id', value: event.anonymousId }, - { type: 'email', value: 'peter@example.com' } - ], + userID: event.userId, + anonymousID: event.anonymousId, + email: 'peter@example.com', kind: 'Track', data: 'User Registered', - timestamp: expectedTs(event.timestamp), + timestamp: event.timestamp, values: {} }) }) @@ -112,11 +85,10 @@ describe('SalesWings', () => { }) const request = await testAction(actionName, event) expect(request).toMatchObject({ - type: 'tracking', - leadRefs: [{ type: 'email', value: 'peter@example.com' }], + email: 'peter@example.com', kind: 'Track', data: 'User Registered', - timestamp: expectedTs(event.timestamp), + timestamp: event.timestamp, values: {} }) }) @@ -129,11 +101,10 @@ describe('SalesWings', () => { }) const request = await testAction(actionName, event) expect(request).toMatchObject({ - type: 'tracking', - leadRefs: [{ type: 'client-id', value: event.userId }], + userID: event.userId, kind: 'Track', data: 'User Registered', - timestamp: expectedTs(event.timestamp), + timestamp: event.timestamp, values: {} }) }) @@ -146,11 +117,10 @@ describe('SalesWings', () => { }) const request = await testAction(actionName, event) expect(request).toMatchObject({ - type: 'tracking', - leadRefs: [{ type: 'client-id', value: event.anonymousId }], + anonymousID: event.anonymousId, kind: 'Track', data: 'User Registered', - timestamp: expectedTs(event.timestamp), + timestamp: event.timestamp, values: {} }) }) @@ -167,102 +137,22 @@ describe('SalesWings', () => { }) ] const request = await testBatchAction(actionName, events) - expect(request).toMatchObject({ - events: [ - { - type: 'tracking', - leadRefs: [ - { type: 'client-id', value: events[0].userId }, - { type: 'client-id', value: events[0].anonymousId } - ], - kind: 'Track', - data: 'User Registered', - timestamp: expectedTs(events[0].timestamp), - values: {} - }, - { - type: 'tracking', - leadRefs: [ - { type: 'client-id', value: events[1].userId }, - { type: 'client-id', value: events[1].anonymousId } - ], - kind: 'Track', - data: 'Order Completed', - timestamp: expectedTs(events[0].timestamp), - values: {} - } - ] - }) - }) - - it('should not include skippable events into a batch', async () => { - const events = [ - createTestEvent({ - type: 'track', - event: 'User Registered' - }), - createTestEvent({ - type: 'track', - event: 'Order Purchased' - }), - createTestEvent({ - type: 'track', - event: 'Cart Abandonned', - userId: undefined, - anonymousId: undefined - }) - ] - const request = await testBatchAction(actionName, events) - expect(request).toMatchObject({ - events: [ - { - type: 'tracking', - leadRefs: [ - { type: 'client-id', value: events[0].userId }, - { type: 'client-id', value: events[0].anonymousId } - ], - kind: 'Track', - data: 'User Registered', - timestamp: expectedTs(events[0].timestamp), - values: {} - }, - { - type: 'tracking', - leadRefs: [ - { type: 'client-id', value: events[1].userId }, - { type: 'client-id', value: events[1].anonymousId } - ], - kind: 'Track', - data: 'Order Purchased', - timestamp: expectedTs(events[0].timestamp), - values: {} - } - ] - }) - }) - - it('should not submit a batch if all the events are skippable', async () => { - const events = [ - createTestEvent({ - type: 'track', - event: 'User Registered', - userId: undefined, - anonymousId: undefined - }), - createTestEvent({ - type: 'track', - event: 'Order Purchased', - userId: undefined, - anonymousId: undefined - }), - createTestEvent({ - type: 'track', - event: 'Cart Abandonned', - userId: undefined, - anonymousId: undefined - }) - ] - await testBatchActionSkippedEvents(actionName, events) + expect(request).toMatchObject([ + { + userID: events[0].userId, + anonymousID: events[0].anonymousId, + kind: 'Track', + data: 'User Registered', + timestamp: events[0].timestamp + }, + { + userID: events[1].userId, + anonymousID: events[1].anonymousId, + kind: 'Track', + data: 'Order Completed', + timestamp: events[1].timestamp + } + ]) }) }) }) diff --git a/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/__tests__/snapshot.test.ts index 11acbfc86b..4330fa2755 100644 --- a/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/__tests__/snapshot.test.ts +++ b/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/__tests__/snapshot.test.ts @@ -5,7 +5,7 @@ import nock from 'nock' const testDestination = createTestIntegration(destination) const actionSlug = 'submitTrackEvent' -const destinationSlug = '' +const destinationSlug = 'saleswings' const seedName = `${destinationSlug}#${actionSlug}` describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination action:`, () => { @@ -25,7 +25,7 @@ describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination ac const responses = await testDestination.testAction(actionSlug, { event: event, - settings: settingsData, + settings: { ...settingsData, environment: 'helium' }, useDefaultMappings: true }) @@ -59,7 +59,7 @@ describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination ac const responses = await testDestination.testAction(actionSlug, { event: event, - settings: settingsData, + settings: { ...settingsData, environment: 'helium' }, useDefaultMappings: true }) diff --git a/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/generated-types.ts b/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/generated-types.ts index c06058783e..f794ba160c 100644 --- a/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/generated-types.ts +++ b/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/generated-types.ts @@ -12,11 +12,11 @@ export interface Payload { /** * Permanent identifier of a Segment user the event is attributed to. */ - userId?: string + userID?: string /** * A pseudo-unique substitute for a Segment user ID the event is attributed to. */ - anonymousId?: string + anonymousID?: string /** * Identified email of the Segment User. */ diff --git a/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/index.ts b/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/index.ts index 6779715421..b6b66c8f82 100644 --- a/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/index.ts +++ b/packages/destination-actions/src/destinations/saleswings/submitTrackEvent/index.ts @@ -1,26 +1,9 @@ import type { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { Event, TrackingEvent } from '../api' -import { userId, anonymousId, email, url, referrerUrl, userAgent, timestamp, kind, data, values } from '../fields' -import { convertLeadRefs, convertValues, convertTimestamp } from '../converter' +import { userID, anonymousID, email, url, referrerUrl, userAgent, timestamp, kind, data, values } from '../fields' import { perform, performBatch } from '../common' -const convertEvent = (payload: Payload): Event | undefined => { - const leadRefs = convertLeadRefs(payload) - if (leadRefs.length == 0) return undefined - return new TrackingEvent({ - leadRefs, - kind: payload.kind, - data: payload.data, - url: payload.url, - referrerUrl: payload.referrerUrl, - userAgent: payload.userAgent, - timestamp: convertTimestamp(payload.timestamp), - values: convertValues(payload.values) - }) -} - const action: ActionDefinition = { title: 'Submit Track Event', description: @@ -29,8 +12,8 @@ const action: ActionDefinition = { fields: { kind: kind('Track'), data: data({ '@path': '$.event' }), - userId, - anonymousId, + userID, + anonymousID, email, url, referrerUrl, @@ -38,8 +21,8 @@ const action: ActionDefinition = { timestamp, values: values({ '@path': '$.properties' }) }, - perform: perform(convertEvent), - performBatch: performBatch(convertEvent) + perform: perform('track'), + performBatch: performBatch('track') } export default action diff --git a/packages/destination-actions/src/destinations/saleswings/testing.ts b/packages/destination-actions/src/destinations/saleswings/testing.ts index d6e1991f9b..f4d70572ba 100644 --- a/packages/destination-actions/src/destinations/saleswings/testing.ts +++ b/packages/destination-actions/src/destinations/saleswings/testing.ts @@ -1,14 +1,16 @@ import { createTestIntegration, SegmentEvent } from '@segment/actions-core' import nock from 'nock' -import { apiBaseUrl } from './api' +import { EventType, submitEventBatchUrl, submitEventUrl } from './api' import destination from './index' const testDestination = createTestIntegration(destination) -export const settings = { apiKey: 'TEST_API_KEY' } +export const settings = { apiKey: 'TEST_API_KEY', environment: 'helium' } export const testAction = async (actionName: string, event: SegmentEvent): Promise => { - nock(apiBaseUrl).post('/events').reply(200, {}) + nock(submitEventUrl(settings.environment, eventType(event))) + .post('') + .reply(200, {}) const input = { event, settings, useDefaultMappings: true } const responses = await testDestination.testAction(actionName, input) expect(responses.length).toBe(1) @@ -19,7 +21,9 @@ export const testAction = async (actionName: string, event: SegmentEvent): Promi } export const testBatchAction = async (actionName: string, events: SegmentEvent[]): Promise => { - nock(apiBaseUrl).post('/events/batches').reply(200, {}) + nock(submitEventBatchUrl(settings.environment, eventType(events[0]))) + .post('') + .reply(200, {}) const input = { events, settings, useDefaultMappings: true } const responses = await testDestination.testBatchAction(actionName, input) expect(responses.length).toBe(1) @@ -29,21 +33,11 @@ export const testBatchAction = async (actionName: string, events: SegmentEvent[] return JSON.parse(rawBody) } -export const testActionWithSkippedEvent = async (actionName: string, event: SegmentEvent): Promise => { - const responses = await testDestination.testAction(actionName, { event, settings, useDefaultMappings: true }) - expect(responses.length).toBe(0) -} - -export const testBatchActionSkippedEvents = async (actionName: string, events: SegmentEvent[]): Promise => { - const responses = await testDestination.testBatchAction(actionName, { events, settings, useDefaultMappings: true }) - expect(responses.length).toBe(0) -} - -export const expectedTs = (segmentEventTs: string | Date | undefined): number => { - if (segmentEventTs === undefined) throw new Error('Unexpected state: test event created without a timestamp') - else if (typeof segmentEventTs === 'string') return Date.parse(segmentEventTs) - else return segmentEventTs.valueOf() -} - export const userAgent = '"Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1' + +const eventType = (event: SegmentEvent): EventType => { + if (event.type == 'track' || event.type == 'identify' || event.type == 'screen' || event.type == 'page') + return event.type + throw new Error(`Not supported event type for tests: ${event.type}`) +} diff --git a/packages/destination-actions/src/destinations/segment-profiles/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/segment-profiles/__tests__/__snapshots__/snapshot.test.ts.snap index 0b38499722..d59cc6a161 100644 --- a/packages/destination-actions/src/destinations/segment-profiles/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/segment-profiles/__tests__/__snapshots__/snapshot.test.ts.snap @@ -4,6 +4,9 @@ exports[`Testing snapshot for actions-segment-profiles destination: sendGroup ac Object { "anonymousId": "cZE8HyAL0!BF#)WQb^", "groupId": "cZE8HyAL0!BF#)WQb^", + "integrations": Object { + "All": false, + }, "traits": Object { "testType": "cZE8HyAL0!BF#)WQb^", }, @@ -15,6 +18,9 @@ exports[`Testing snapshot for actions-segment-profiles destination: sendGroup ac Object { "anonymousId": "cZE8HyAL0!BF#)WQb^", "groupId": "cZE8HyAL0!BF#)WQb^", + "integrations": Object { + "All": false, + }, "traits": Object {}, "userId": "cZE8HyAL0!BF#)WQb^", } @@ -24,6 +30,9 @@ exports[`Testing snapshot for actions-segment-profiles destination: sendIdentify Object { "anonymousId": "hIC1OAmWa[Q!&d%o", "groupId": "hIC1OAmWa[Q!&d%o", + "integrations": Object { + "All": false, + }, "traits": Object { "testType": "hIC1OAmWa[Q!&d%o", }, @@ -35,6 +44,9 @@ exports[`Testing snapshot for actions-segment-profiles destination: sendIdentify Object { "anonymousId": "hIC1OAmWa[Q!&d%o", "groupId": "hIC1OAmWa[Q!&d%o", + "integrations": Object { + "All": false, + }, "traits": Object {}, "userId": "hIC1OAmWa[Q!&d%o", } diff --git a/packages/destination-actions/src/destinations/segment-profiles/index.ts b/packages/destination-actions/src/destinations/segment-profiles/index.ts index c6dceaf6e8..3e0c72f234 100644 --- a/packages/destination-actions/src/destinations/segment-profiles/index.ts +++ b/packages/destination-actions/src/destinations/segment-profiles/index.ts @@ -17,7 +17,7 @@ const destination: DestinationDefinition = { label: 'Segment Public API Token', description: 'The Segment Public API requires that you have an authentication token before you send requests. [This document](https://docs.segmentapis.com/tag/Getting-Started#section/Get-an-API-token) explains how to setup a token.', - type: 'string', + type: 'password', required: true }, endpoint: { diff --git a/packages/destination-actions/src/destinations/segment-profiles/segment-properties.ts b/packages/destination-actions/src/destinations/segment-profiles/segment-properties.ts index d47b717aad..f8247d9bda 100644 --- a/packages/destination-actions/src/destinations/segment-profiles/segment-properties.ts +++ b/packages/destination-actions/src/destinations/segment-profiles/segment-properties.ts @@ -28,9 +28,9 @@ export const traits: InputField = { } export const engage_space: InputField = { - label: 'Engage Space', + label: 'Profile Space', description: - 'The Engage Space to use for creating a record. *Note: This field shows list of internal sources associated with your Engaged Spaces. Changes made to the Engage Space name in **Settings** will not reflect in this list unless the source associated with the Engage Space is renamed explicitly.*', + 'The Profile Space to use for creating a record. *Note: This field shows list of internal sources associated with your Engaged Spaces. Changes made to the Engage Space name in **Settings** will not reflect in this list unless the source associated with the Engage Space is renamed explicitly.*', type: 'string', required: true, dynamic: true diff --git a/packages/destination-actions/src/destinations/segment-profiles/sendGroup/__tests__/__snapshots__/index.test.ts.snap b/packages/destination-actions/src/destinations/segment-profiles/sendGroup/__tests__/__snapshots__/index.test.ts.snap index b472420c08..98dd2246f3 100644 --- a/packages/destination-actions/src/destinations/segment-profiles/sendGroup/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/destination-actions/src/destinations/segment-profiles/sendGroup/__tests__/__snapshots__/index.test.ts.snap @@ -17,6 +17,9 @@ exports[`SegmentProfiles.sendGroup Should send an group event to Segment 2`] = ` Object { "anonymousId": "arky4h2sh7k", "groupId": "test-group-ks2i7e", + "integrations": Object { + "All": false, + }, "traits": Object { "industry": "Technology", "name": "Example Corp", diff --git a/packages/destination-actions/src/destinations/segment-profiles/sendGroup/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/segment-profiles/sendGroup/__tests__/__snapshots__/snapshot.test.ts.snap index e518bad79d..093f9ffd0b 100644 --- a/packages/destination-actions/src/destinations/segment-profiles/sendGroup/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/segment-profiles/sendGroup/__tests__/__snapshots__/snapshot.test.ts.snap @@ -4,6 +4,9 @@ exports[`Testing snapshot for SegmentProfiles's sendGroup destination action: al Object { "anonymousId": "tKaa(2A", "groupId": "tKaa(2A", + "integrations": Object { + "All": false, + }, "traits": Object { "testType": "tKaa(2A", }, @@ -15,6 +18,9 @@ exports[`Testing snapshot for SegmentProfiles's sendGroup destination action: re Object { "anonymousId": "tKaa(2A", "groupId": "tKaa(2A", + "integrations": Object { + "All": false, + }, "traits": Object {}, "userId": "tKaa(2A", } diff --git a/packages/destination-actions/src/destinations/segment-profiles/sendGroup/generated-types.ts b/packages/destination-actions/src/destinations/segment-profiles/sendGroup/generated-types.ts index 866973e0f5..0aaba82be9 100644 --- a/packages/destination-actions/src/destinations/segment-profiles/sendGroup/generated-types.ts +++ b/packages/destination-actions/src/destinations/segment-profiles/sendGroup/generated-types.ts @@ -2,7 +2,7 @@ export interface Payload { /** - * The Engage Space to use for creating a record. *Note: This field shows list of internal sources associated with your Engaged Spaces. Changes made to the Engage Space name in **Settings** will not reflect in this list unless the source associated with the Engage Space is renamed explicitly.* + * The Profile Space to use for creating a record. *Note: This field shows list of internal sources associated with your Engaged Spaces. Changes made to the Engage Space name in **Settings** will not reflect in this list unless the source associated with the Engage Space is renamed explicitly.* */ engage_space: string /** diff --git a/packages/destination-actions/src/destinations/segment-profiles/sendGroup/index.ts b/packages/destination-actions/src/destinations/segment-profiles/sendGroup/index.ts index 8160b0ed89..e200b6fde6 100644 --- a/packages/destination-actions/src/destinations/segment-profiles/sendGroup/index.ts +++ b/packages/destination-actions/src/destinations/segment-profiles/sendGroup/index.ts @@ -35,6 +35,11 @@ const action: ActionDefinition = { groupId: payload?.group_id, traits: { ...payload?.traits + }, + integrations: { + // Setting 'integrations.All' to false will ensure that we don't send events + // to any destinations which is connected to the Segment Profiles space. + All: false } } diff --git a/packages/destination-actions/src/destinations/segment-profiles/sendIdentify/__tests__/__snapshots__/index.test.ts.snap b/packages/destination-actions/src/destinations/segment-profiles/sendIdentify/__tests__/__snapshots__/index.test.ts.snap index 44110df300..d094a50037 100644 --- a/packages/destination-actions/src/destinations/segment-profiles/sendIdentify/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/destination-actions/src/destinations/segment-profiles/sendIdentify/__tests__/__snapshots__/index.test.ts.snap @@ -17,6 +17,9 @@ exports[`Segment.sendIdentify Should send an identify event to Segment 2`] = ` Object { "anonymousId": "arky4h2sh7k", "groupId": undefined, + "integrations": Object { + "All": false, + }, "traits": Object { "email": "test-user@test-company.com", "name": "Test User", diff --git a/packages/destination-actions/src/destinations/segment-profiles/sendIdentify/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/segment-profiles/sendIdentify/__tests__/__snapshots__/snapshot.test.ts.snap index 1cdda7d384..d0706d1f1e 100644 --- a/packages/destination-actions/src/destinations/segment-profiles/sendIdentify/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/segment-profiles/sendIdentify/__tests__/__snapshots__/snapshot.test.ts.snap @@ -4,6 +4,9 @@ exports[`Testing snapshot for SegmentProfiles's sendIdentify destination action: Object { "anonymousId": "mV[ZQcEVgZO$MX", "groupId": "mV[ZQcEVgZO$MX", + "integrations": Object { + "All": false, + }, "traits": Object { "testType": "mV[ZQcEVgZO$MX", }, @@ -15,6 +18,9 @@ exports[`Testing snapshot for SegmentProfiles's sendIdentify destination action: Object { "anonymousId": "mV[ZQcEVgZO$MX", "groupId": "mV[ZQcEVgZO$MX", + "integrations": Object { + "All": false, + }, "traits": Object {}, "userId": "mV[ZQcEVgZO$MX", } diff --git a/packages/destination-actions/src/destinations/segment-profiles/sendIdentify/generated-types.ts b/packages/destination-actions/src/destinations/segment-profiles/sendIdentify/generated-types.ts index 5657723b32..27d3bc99ca 100644 --- a/packages/destination-actions/src/destinations/segment-profiles/sendIdentify/generated-types.ts +++ b/packages/destination-actions/src/destinations/segment-profiles/sendIdentify/generated-types.ts @@ -2,7 +2,7 @@ export interface Payload { /** - * The Engage Space to use for creating a record. *Note: This field shows list of internal sources associated with your Engaged Spaces. Changes made to the Engage Space name in **Settings** will not reflect in this list unless the source associated with the Engage Space is renamed explicitly.* + * The Profile Space to use for creating a record. *Note: This field shows list of internal sources associated with your Engaged Spaces. Changes made to the Engage Space name in **Settings** will not reflect in this list unless the source associated with the Engage Space is renamed explicitly.* */ engage_space: string /** diff --git a/packages/destination-actions/src/destinations/segment-profiles/sendIdentify/index.ts b/packages/destination-actions/src/destinations/segment-profiles/sendIdentify/index.ts index 5b9c763a4c..e9c05aa25c 100644 --- a/packages/destination-actions/src/destinations/segment-profiles/sendIdentify/index.ts +++ b/packages/destination-actions/src/destinations/segment-profiles/sendIdentify/index.ts @@ -36,6 +36,11 @@ const action: ActionDefinition = { groupId: payload?.group_id, traits: { ...payload?.traits + }, + integrations: { + // Setting 'integrations.All' to false will ensure that we don't send events + // to any destinations which is connected to the Segment Profiles space. + All: false } } diff --git a/packages/destination-actions/src/destinations/voucherify/__tests__/customEvent.test.ts b/packages/destination-actions/src/destinations/voucherify/__tests__/customEvent.test.ts new file mode 100644 index 0000000000..40dc230885 --- /dev/null +++ b/packages/destination-actions/src/destinations/voucherify/__tests__/customEvent.test.ts @@ -0,0 +1,148 @@ +import nock from 'nock' +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import Voucherify from '../index' +import { Settings } from '../generated-types' + +const testDestination = createTestIntegration(Voucherify) + +const settings: Settings = { + apiKey: 'voucherifyApiKey', + secretKey: 'voucherifySecretKey', + customURL: 'https://us1.api.voucherify.io/segmentio' +} + +describe('Voucherify', () => { + describe('addCustomEvent', () => { + it('should throw error if the custom URL is invalid.', async () => { + const wrongSettings: Settings = { + apiKey: 'voucherifyApiKey', + secretKey: 'voucherifySecretKey', + customURL: 'wrongURL.com' + } + nock(wrongSettings.customURL).post('/event-processing').reply(200) + + const testEvent = createTestEvent({ + event: 'Test Track Event', + type: 'track', + properties: { + source_id: 'test_customer_1' + } + }) + + await expect( + testDestination.testAction('addCustomEvent', { + event: testEvent, + settings: wrongSettings, + mapping: { + event: { + '@path': '$.event' + }, + type: { + '@path': '$.type' + }, + source_id: { + '@path': '$.properties.source_id' + } + } + }) + ).rejects.toThrowError( + `The Custom URL: ${wrongSettings.customURL} is invalid. It probably lacks the HTTP/HTTPS protocol or has an incorrect format.` + ) + }) + + it('should work with the default mapping', async () => { + nock(settings.customURL).post('/event-processing').reply(200) + + const testEvent = createTestEvent({ + event: 'Test Track Event', + type: 'track', + properties: { + source_id: 'test_customer_1' + } + }) + + await expect( + testDestination.testAction('addCustomEvent', { + event: testEvent, + settings, + useDefaultMappings: true + }) + ).rejects.not.toThrowError("The root value is missing the required field 'source_id'.") + }) + + it('should throw an error if the source_id and email are not specified', async () => { + nock(settings.customURL).post('/event-processing').reply(200) + + const testEvent = createTestEvent({ + event: 'Test Track Event', + type: 'track', + properties: { + testProp: 'property' + } + }) + + await expect( + testDestination.testAction('addCustomEvent', { + event: testEvent, + settings, + mapping: { + event: { + '@path': '$.event' + }, + type: { + '@path': '$.type' + } + } + }) + ).rejects.toThrowError("The root value is missing the required field 'source_id'.") + }) + + it('should throw an error if the type is not specified', async () => { + nock(settings.customURL).post('/event-processing').reply(200) + + const testEvent = createTestEvent({ + event: 'Test Track Event', + properties: { + source_id: 'test_customer_1' + } + }) + await expect( + testDestination.testAction('addCustomEvent', { + event: testEvent, + settings, + mapping: { + event: { + '@path': '$.event' + }, + source_id: { + '@path': '$.properties.source_id' + } + } + }) + ).rejects.toThrowError("The root value is missing the required field 'type'.") + }) + + it('should work if the email is supplied instead of the source_id', async () => { + nock(settings.customURL).post('/event-processing').reply(200) + + const testEvent = createTestEvent({ + event: 'Test Track Event', + type: 'track', + properties: { + email: 'test@voucherify.io' + } + }) + await expect( + testDestination.testAction('addCustomEvent', { + event: testEvent, + settings, + mapping: { + source_id: { + '@path': '$.properties.email' + } + } + }) + ).rejects.not.toThrowError("The root value is missing the required field 'source_id'.") + }) + }) +}) diff --git a/packages/destination-actions/src/destinations/voucherify/__tests__/customer.test.ts b/packages/destination-actions/src/destinations/voucherify/__tests__/customer.test.ts new file mode 100644 index 0000000000..35c4db226a --- /dev/null +++ b/packages/destination-actions/src/destinations/voucherify/__tests__/customer.test.ts @@ -0,0 +1,55 @@ +import nock from 'nock' +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import Voucherify from '../index' +import { Settings } from '../generated-types' + +const testDestination = createTestIntegration(Voucherify) + +const settings: Settings = { + apiKey: 'voucherifyApiKey', + secretKey: 'voucherifySecretKey', + customURL: 'https://us1.api.voucherify.io/segmentio' +} + +describe('Voucherify', () => { + describe('upsertCustomer', () => { + it('should throw error when source_id is not specified', async () => { + nock(settings.customURL).post('/customer-processing').reply(200) + const testEvent = createTestEvent({ + traits: { + name: 'Test' + }, + type: 'identify' + }) + + await expect( + testDestination.testAction('upsertCustomer', { + event: testEvent, + settings + }) + ).rejects.toThrowError("The root value is missing the required field 'source_id'.") + }) + }) + + describe('assignCustomerToGroup', () => { + it('should throw error when group_id is not specified', async () => { + nock(settings.customURL).post('/group-processing').reply(200) + const testEvent = createTestEvent({ + traits: { + name: 'Test' + }, + type: 'group', + properties: { + source_id: 'test_customer_1' + } + }) + + await expect( + testDestination.testAction('assignCustomerToGroup', { + event: testEvent, + settings + }) + ).rejects.toThrowError("The root value is missing the required field 'group_id'.") + }) + }) +}) diff --git a/packages/destination-actions/src/destinations/voucherify/addCustomEvent/generated-types.ts b/packages/destination-actions/src/destinations/voucherify/addCustomEvent/generated-types.ts new file mode 100644 index 0000000000..777ba0772a --- /dev/null +++ b/packages/destination-actions/src/destinations/voucherify/addCustomEvent/generated-types.ts @@ -0,0 +1,26 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * The source_id which identifies the [customer](https://docs.voucherify.io/reference/the-customer-object) in Voucherify. + */ + source_id: string + /** + * The email that identifies the [customer](https://docs.voucherify.io/reference/the-customer-object) in Voucherify. + */ + email?: string + /** + * The name of the event that will be saved as a [custom event](https://docs.voucherify.io/reference/the-custom-event-object) in Voucherify. + */ + event?: string + /** + * Additional data that will be stored in the [custom event](https://docs.voucherify.io/reference/the-custom-event-object) metadata in Voucherify. + */ + metadata?: { + [k: string]: unknown + } + /** + * Type of the event. It can be track, page or screen. + */ + type: string +} diff --git a/packages/destination-actions/src/destinations/voucherify/addCustomEvent/index.ts b/packages/destination-actions/src/destinations/voucherify/addCustomEvent/index.ts new file mode 100644 index 0000000000..653f5d8b42 --- /dev/null +++ b/packages/destination-actions/src/destinations/voucherify/addCustomEvent/index.ts @@ -0,0 +1,81 @@ +import { ActionDefinition } from '@segment/actions-core' +import type { Settings } from '../generated-types' +import { getVoucherifyEndpointURL } from '../url-provider' +import type { Payload } from './generated-types' + +const action: ActionDefinition = { + title: 'Add custom event', + description: + 'Send the Track, Page or Screen event that will be saved as a [custom event](https://docs.voucherify.io/reference/the-custom-event-object) in Voucherify.', + defaultSubscription: 'type = "track" or type = "page" or type = "screen"', + fields: { + source_id: { + label: 'Source ID', + description: + 'The source_id which identifies the [customer](https://docs.voucherify.io/reference/the-customer-object) in Voucherify.', + type: 'string', + required: true, + default: { + '@if': { + exists: { '@path': '$.userId' }, + then: { '@path': '$.userId' }, + else: { '@path': '$.anonymousId' } + } + } + }, + email: { + label: 'Email Address', + description: + 'The email that identifies the [customer](https://docs.voucherify.io/reference/the-customer-object) in Voucherify.', + type: 'string', + default: { + '@if': { + exists: { '@path': '$.properties.email' }, + then: { '@path': '$.properties.email' }, + else: { '@path': '$.context.traits' } + } + } + }, + event: { + label: 'Event Name', + description: + 'The name of the event that will be saved as a [custom event](https://docs.voucherify.io/reference/the-custom-event-object) in Voucherify.', + type: 'string', + default: { + '@if': { + exists: { '@path': '$.event' }, + then: { '@path': '$.event' }, + else: { '@path': '$.name' } + } + } + }, + metadata: { + label: 'Track Event Metadata', + description: + 'Additional data that will be stored in the [custom event](https://docs.voucherify.io/reference/the-custom-event-object) metadata in Voucherify.', + type: 'object', + default: { + '@path': '$.properties' + } + }, + type: { + label: 'Event Type', + description: 'Type of the event. It can be track, page or screen.', + type: 'string', + required: true, + default: { + '@path': '$.type' + } + } + }, + + perform: (request, { settings, payload }) => { + const voucherifyRequestURL = getVoucherifyEndpointURL(settings, 'event') + return request(voucherifyRequestURL, { + method: 'post', + json: payload + }) + } +} + +export default action diff --git a/packages/destination-actions/src/destinations/voucherify/assignCustomerToGroup/generated-types.ts b/packages/destination-actions/src/destinations/voucherify/assignCustomerToGroup/generated-types.ts new file mode 100644 index 0000000000..8aea6c2c05 --- /dev/null +++ b/packages/destination-actions/src/destinations/voucherify/assignCustomerToGroup/generated-types.ts @@ -0,0 +1,26 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * The source_id which identifies the [customer](https://docs.voucherify.io/reference/the-customer-object) in Voucherify. + */ + source_id: string + /** + * The email that identifies the [customer](https://docs.voucherify.io/reference/the-customer-object) in Voucherify. + */ + email?: string + /** + * The ID used to uniquely identify a group to which customer belongs. + */ + group_id: string + /** + * Traits of the group that will be created in customer [metadata](https://www.voucherify.io/glossary/metadata-custom-attributes). + */ + traits?: { + [k: string]: unknown + } + /** + * Type of the event [The Segment Spec](https://segment.com/docs/connections/spec/). + */ + type: string +} diff --git a/packages/destination-actions/src/destinations/voucherify/assignCustomerToGroup/index.ts b/packages/destination-actions/src/destinations/voucherify/assignCustomerToGroup/index.ts new file mode 100644 index 0000000000..59fc86093b --- /dev/null +++ b/packages/destination-actions/src/destinations/voucherify/assignCustomerToGroup/index.ts @@ -0,0 +1,72 @@ +import { ActionDefinition } from '@segment/actions-core' +import { Settings } from '../generated-types' +import { Payload } from './generated-types' +import { getVoucherifyEndpointURL } from '../url-provider' + +const action: ActionDefinition = { + title: 'Assign customer to group', + description: 'Assign a specific group and its traits to the customer.', + defaultSubscription: 'type = "group"', + fields: { + source_id: { + label: 'Source ID', + description: + 'The source_id which identifies the [customer](https://docs.voucherify.io/reference/the-customer-object) in Voucherify.', + type: 'string', + required: true, + default: { + '@if': { + exists: { '@path': '$.userId' }, + then: { '@path': '$.userId' }, + else: { '@path': '$.anonymousId' } + } + } + }, + email: { + label: 'Email Address', + description: + 'The email that identifies the [customer](https://docs.voucherify.io/reference/the-customer-object) in Voucherify.', + type: 'string', + default: { + '@path': '$.email' + } + }, + group_id: { + label: 'Group ID', + description: 'The ID used to uniquely identify a group to which customer belongs.', + type: 'string', + required: true, + default: { + '@path': '$.groupId' + } + }, + + traits: { + label: 'Group traits', + description: + 'Traits of the group that will be created in customer [metadata](https://www.voucherify.io/glossary/metadata-custom-attributes).', + type: 'object', + default: { + '@path': '$.traits' + } + }, + type: { + label: 'Event Type', + description: 'Type of the event [The Segment Spec](https://segment.com/docs/connections/spec/).', + type: 'string', + required: true, + default: { + '@path': '$.type' + } + } + }, + perform: (request, { settings, payload }) => { + const voucherifyRequestURL = getVoucherifyEndpointURL(settings, 'group') + return request(voucherifyRequestURL, { + method: 'post', + json: payload + }) + } +} + +export default action diff --git a/packages/destination-actions/src/destinations/voucherify/generated-types.ts b/packages/destination-actions/src/destinations/voucherify/generated-types.ts new file mode 100644 index 0000000000..5bc275d145 --- /dev/null +++ b/packages/destination-actions/src/destinations/voucherify/generated-types.ts @@ -0,0 +1,16 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Settings { + /** + * Application ID can be found in [Voucherify dashboard](https://docs.voucherify.io/docs/authentication). + */ + apiKey: string + /** + * Secret Key can be found in [Voucherify dashboard](https://docs.voucherify.io/docs/authentication). + */ + secretKey: string + /** + * Check your API region in [Voucherify dashboard](https://app.voucherify.io/#/login) -> Project settings -> API endpoint. For example: `https://us1.api.voucherify.io` -> `https://us1.segmentio.voucherify.io`. It also works for dedicated URLs. + */ + customURL: string +} diff --git a/packages/destination-actions/src/destinations/voucherify/index.ts b/packages/destination-actions/src/destinations/voucherify/index.ts new file mode 100644 index 0000000000..c615c3d6cf --- /dev/null +++ b/packages/destination-actions/src/destinations/voucherify/index.ts @@ -0,0 +1,116 @@ +/* eslint-disable no-useless-escape */ +import { DestinationDefinition } from '@segment/actions-core' +import { Settings } from './generated-types' +import upsertCustomer from './upsertCustomer' +import { defaultValues } from '@segment/actions-core' +import addCustomEvent from './addCustomEvent' +import assignCustomerToGroup from './assignCustomerToGroup' +import { validateURL } from './url-validator' + +const destination: DestinationDefinition = { + name: 'Voucherify (Actions)', + slug: 'voucherify-actions', + mode: 'cloud', + + authentication: { + scheme: 'basic', + fields: { + apiKey: { + label: 'Application ID', + description: + 'Application ID can be found in [Voucherify dashboard](https://docs.voucherify.io/docs/authentication).', + type: 'string', + required: true + }, + secretKey: { + label: 'Secret Key', + description: + 'Secret Key can be found in [Voucherify dashboard](https://docs.voucherify.io/docs/authentication).', + type: 'password', + required: true + }, + customURL: { + label: 'Custom Voucherify URL', + description: + 'Check your API region in [Voucherify dashboard](https://app.voucherify.io/#/login) -> Project settings -> API endpoint. For example: `https://us1.api.voucherify.io` -> `https://us1.segmentio.voucherify.io`. It also works for dedicated URLs.', + type: 'string', + required: true + } + }, + testAuthentication: async (request, { settings }) => { + const testAuthenticationEndpoint = `${settings.customURL}/test-authentication` + const authenticationResponse = await request(testAuthenticationEndpoint, { + headers: { + authorization: `Basic ${Buffer.from(settings.apiKey).toString('base64')}`, + 'secret-key': Buffer.from(settings.secretKey).toString('base64') + } + }) + if (authenticationResponse.status === 401) { + throw new Error(authenticationResponse.status + ': ' + authenticationResponse.statusText) + } + } + }, + extendRequest({ settings }) { + if (!settings.apiKey || !settings.secretKey) { + throw new Error('The request is missing Application ID or Secret Key.') + } + + validateURL(settings) + + return { + headers: { + authorization: `Basic ${Buffer.from(settings.apiKey).toString('base64')}`, + 'secret-key': Buffer.from(settings.secretKey).toString('base64') + } + } + }, + actions: { + upsertCustomer, + addCustomEvent, + assignCustomerToGroup + }, + presets: [ + { + name: 'Add Custom Event (Track Event)', + subscribe: 'type = "track"', + partnerAction: 'addCustomEvent', + mapping: defaultValues(addCustomEvent.fields) + }, + { + name: 'Add Custom Event (Page Event)', + subscribe: 'type = "page"', + partnerAction: 'addCustomEvent', + mapping: { + ...defaultValues(addCustomEvent.fields), + event: { + '@template': 'Viewed {{name}} Page' + } + } + }, + { + name: 'Add Custom Event (Screen Event)', + subscribe: 'type = "screen"', + partnerAction: 'addCustomEvent', + mapping: { + ...defaultValues(addCustomEvent.fields), + event: { + '@template': 'Viewed {{name}} Screen' + } + } + }, + { + name: 'Create Or Update Customer', + subscribe: 'type = "identify"', + partnerAction: 'upsertCustomer', + mapping: defaultValues(upsertCustomer.fields) + }, + { + name: 'Assign Customer To Group', + subscribe: 'type = "group"', + partnerAction: 'assignCustomerToGroup', + mapping: defaultValues(assignCustomerToGroup.fields) + } + ] +} + +export default destination diff --git a/packages/destination-actions/src/destinations/voucherify/upsertCustomer/generated-types.ts b/packages/destination-actions/src/destinations/voucherify/upsertCustomer/generated-types.ts new file mode 100644 index 0000000000..e191c8abb5 --- /dev/null +++ b/packages/destination-actions/src/destinations/voucherify/upsertCustomer/generated-types.ts @@ -0,0 +1,34 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * The source_id which identifies the [customer](https://docs.voucherify.io/reference/the-customer-object) in Voucherify. + */ + source_id: string + /** + * The email that identifies the [customer](https://docs.voucherify.io/reference/the-customer-object) in Voucherify. + */ + email?: string + /** + * Additional [customer](https://docs.voucherify.io/reference/the-customer-object) attributes, such as email, name, description, phone, address, birthdate, metadata. When updating a customer, attributes are either added or updated in the customer object. + */ + traits?: { + firstName?: string + lastName?: string + name?: string + description?: string + address?: { + [k: string]: unknown + } + phone?: string + birthdate?: string + metadata?: { + [k: string]: unknown + } + [k: string]: unknown + } + /** + * Type of the event [The Segment Spec](https://segment.com/docs/connections/spec/). + */ + type: string +} diff --git a/packages/destination-actions/src/destinations/voucherify/upsertCustomer/index.ts b/packages/destination-actions/src/destinations/voucherify/upsertCustomer/index.ts new file mode 100644 index 0000000000..1954943cfe --- /dev/null +++ b/packages/destination-actions/src/destinations/voucherify/upsertCustomer/index.ts @@ -0,0 +1,107 @@ +import { ActionDefinition } from '@segment/actions-core' +import { Settings } from '../generated-types' +import { getVoucherifyEndpointURL } from '../url-provider' +import { Payload } from './generated-types' + +const action: ActionDefinition = { + title: ' Upsert Customer', + description: + 'Send the [identify event](https://segment.com/docs/connections/spec/identify/) to create or update the [customer](https://docs.voucherify.io/reference/the-customer-object)', + defaultSubscription: 'type = "identify"', + fields: { + source_id: { + label: 'Source ID', + description: + 'The source_id which identifies the [customer](https://docs.voucherify.io/reference/the-customer-object) in Voucherify.', + type: 'string', + required: true, + default: { + '@if': { + exists: { '@path': '$.userId' }, + then: { '@path': '$.userId' }, + else: { '@path': '$.anonymousId' } + } + } + }, + email: { + label: 'Email Address', + description: + 'The email that identifies the [customer](https://docs.voucherify.io/reference/the-customer-object) in Voucherify.', + type: 'string', + default: { + '@path': '$.traits.email' + } + }, + traits: { + label: 'Customer Attributes', + description: + 'Additional [customer](https://docs.voucherify.io/reference/the-customer-object) attributes, such as email, name, description, phone, address, birthdate, metadata. When updating a customer, attributes are either added or updated in the customer object.', + additionalProperties: true, + defaultObjectUI: 'keyvalue', + type: 'object', + properties: { + firstName: { + label: 'First Name', + type: 'string' + }, + lastName: { + label: 'Last Name', + type: 'string' + }, + name: { + label: 'Name', + type: 'string' + }, + description: { + label: 'Description', + type: 'string' + }, + address: { + label: 'Address', + type: 'object' + }, + phone: { + label: 'Phone', + type: 'string' + }, + birthdate: { + label: 'Birthdate', + type: 'string' + }, + metadata: { + label: 'Metadata', + type: 'object' + } + }, + default: { + firstName: { '@path': '$.traits.first_name' }, + lastName: { '@path': '$.traits.last_name' }, + name: { '@path': '$.traits.name' }, + description: { '@path': '$.traits.description' }, + address: { '@path': '$.traits.address' }, + phone: { '@path': '$.traits.phone' }, + birthdate: { '@path': '$.traits.birthdate' }, + metadata: { '@path': '$.traits.metadata' } + } + }, + + type: { + label: 'Event Type', + description: 'Type of the event [The Segment Spec](https://segment.com/docs/connections/spec/).', + type: 'string', + required: true, + default: { + '@path': '$.type' + } + } + }, + perform: (request, { settings, payload }) => { + const voucherifyRequestURL = getVoucherifyEndpointURL(settings, 'customer') + return request(voucherifyRequestURL, { + method: 'post', + json: payload + }) + } +} + +export default action diff --git a/packages/destination-actions/src/destinations/voucherify/url-provider.ts b/packages/destination-actions/src/destinations/voucherify/url-provider.ts new file mode 100644 index 0000000000..d23ec4ef46 --- /dev/null +++ b/packages/destination-actions/src/destinations/voucherify/url-provider.ts @@ -0,0 +1,9 @@ +import type { Settings } from './generated-types' + +export const getVoucherifyEndpointURL = (settings: Settings, eventType: string) => { + if (!settings.customURL) { + throw new Error('URL not provided.') + } + + return `${settings.customURL}/segmentio/${eventType}-processing` +} diff --git a/packages/destination-actions/src/destinations/voucherify/url-validator.ts b/packages/destination-actions/src/destinations/voucherify/url-validator.ts new file mode 100644 index 0000000000..c942a604dc --- /dev/null +++ b/packages/destination-actions/src/destinations/voucherify/url-validator.ts @@ -0,0 +1,17 @@ +/* eslint-disable no-useless-escape */ +import type { Settings } from './generated-types' + +export const validateURL = (settings: Settings) => { + if (settings.customURL) { + const urlRegEx = + /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)$/ + const regExMatcher = new RegExp(urlRegEx) + const isCustomURLValid = typeof settings.customURL === 'string' && regExMatcher.test(settings.customURL) + + if (!isCustomURLValid) { + throw new Error( + `The Custom URL: ${settings.customURL} is invalid. It probably lacks the HTTP/HTTPS protocol or has an incorrect format.` + ) + } + } +} diff --git a/packages/destination-actions/src/destinations/vwo/identifyUser/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/vwo/identifyUser/__tests__/__snapshots__/snapshot.test.ts.snap index b29d54ce04..ce4b6bff67 100644 --- a/packages/destination-actions/src/destinations/vwo/identifyUser/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/vwo/identifyUser/__tests__/__snapshots__/snapshot.test.ts.snap @@ -8,7 +8,7 @@ Object { "props": Object { "$visitor": Object { "props": Object { - "segment_testType": "sl#5^du3V5b#Q5V", + "segment.testType": "sl#5^du3V5b#Q5V", }, }, "isCustomEvent": true, @@ -27,7 +27,7 @@ Object { "visId": "sl#5^du3V5b#Q5V", "visitor": Object { "props": Object { - "segment_testType": "sl#5^du3V5b#Q5V", + "segment.testType": "sl#5^du3V5b#Q5V", }, }, }, @@ -42,7 +42,7 @@ Object { "props": Object { "$visitor": Object { "props": Object { - "segment_testType": "sl#5^du3V5b#Q5V", + "segment.testType": "sl#5^du3V5b#Q5V", }, }, "isCustomEvent": true, @@ -61,7 +61,7 @@ Object { "visId": "sl#5^du3V5b#Q5V", "visitor": Object { "props": Object { - "segment_testType": "sl#5^du3V5b#Q5V", + "segment.testType": "sl#5^du3V5b#Q5V", }, }, }, diff --git a/packages/destination-actions/src/destinations/vwo/identifyUser/__tests__/index.test.ts b/packages/destination-actions/src/destinations/vwo/identifyUser/__tests__/index.test.ts index 0ea1350d21..3418ff2b7d 100644 --- a/packages/destination-actions/src/destinations/vwo/identifyUser/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/vwo/identifyUser/__tests__/index.test.ts @@ -35,7 +35,7 @@ describe('VWO.identifyUser', () => { props: { $visitor: { props: { - segment_textProperty: 'Hello' + 'segment.textProperty': 'Hello' } }, vwoMeta: { @@ -50,7 +50,7 @@ describe('VWO.identifyUser', () => { sessionId, visitor: { props: { - segment_textProperty: 'Hello' + 'segment.textProperty': 'Hello' } } } diff --git a/packages/destination-actions/src/destinations/vwo/identifyUser/generated-types.ts b/packages/destination-actions/src/destinations/vwo/identifyUser/generated-types.ts index 9fcf54dbd3..8db3a77a68 100644 --- a/packages/destination-actions/src/destinations/vwo/identifyUser/generated-types.ts +++ b/packages/destination-actions/src/destinations/vwo/identifyUser/generated-types.ts @@ -2,7 +2,7 @@ export interface Payload { /** - * A JSON object containing additional attributes that will be associated with the event. + * Visitor's attributes to be mapped */ attributes: { [k: string]: unknown @@ -12,21 +12,21 @@ export interface Payload { */ vwoUuid: string /** - * Page Context + * Contains context information regarding a webpage */ page: { [k: string]: unknown } /** - * IP Address + * IP address of the user */ ip?: string /** - * User Agent + * User-Agent of the user */ userAgent: string /** - * Timestamp + * Timestamp on the event */ timestamp: string } diff --git a/packages/destination-actions/src/destinations/vwo/identifyUser/index.ts b/packages/destination-actions/src/destinations/vwo/identifyUser/index.ts index ee21b1bafd..22505559a8 100644 --- a/packages/destination-actions/src/destinations/vwo/identifyUser/index.ts +++ b/packages/destination-actions/src/destinations/vwo/identifyUser/index.ts @@ -5,11 +5,11 @@ import { formatPayload, formatAttributes } from '../utility' const action: ActionDefinition = { title: 'Identify User', - description: "Maps segment's visitor traits to VWO visitors' attributes", + description: "Maps Segment's visitor traits to the visitor attributes in VWO", defaultSubscription: 'type = "identify"', fields: { attributes: { - description: 'A JSON object containing additional attributes that will be associated with the event.', + description: `Visitor's attributes to be mapped`, label: 'attributes', required: true, type: 'object', @@ -27,7 +27,7 @@ const action: ActionDefinition = { } }, page: { - description: 'Page Context', + description: 'Contains context information regarding a webpage', label: 'Page', required: true, type: 'object', @@ -36,7 +36,7 @@ const action: ActionDefinition = { } }, ip: { - description: 'IP Address', + description: 'IP address of the user', label: 'IP Address', required: false, type: 'string', @@ -45,7 +45,7 @@ const action: ActionDefinition = { } }, userAgent: { - description: 'User Agent', + description: 'User-Agent of the user', label: 'User Agent', required: true, type: 'string', @@ -54,7 +54,7 @@ const action: ActionDefinition = { } }, timestamp: { - description: 'Timestamp', + description: 'Timestamp on the event', label: 'Timestamp', required: true, type: 'string', diff --git a/packages/destination-actions/src/destinations/vwo/index.ts b/packages/destination-actions/src/destinations/vwo/index.ts index 7665158f49..2bf75bbc06 100644 --- a/packages/destination-actions/src/destinations/vwo/index.ts +++ b/packages/destination-actions/src/destinations/vwo/index.ts @@ -34,7 +34,7 @@ const destination: DestinationDefinition = { scheme: 'custom', fields: { vwoAccountId: { - label: 'Your VWO account ID, used for fetching your VWO async smart code. ', + label: 'Your VWO account ID', description: 'Enter your VWO Account ID', type: 'number', required: true diff --git a/packages/destination-actions/src/destinations/vwo/pageVisit/generated-types.ts b/packages/destination-actions/src/destinations/vwo/pageVisit/generated-types.ts index 4bf23681c1..48f0cef0db 100644 --- a/packages/destination-actions/src/destinations/vwo/pageVisit/generated-types.ts +++ b/packages/destination-actions/src/destinations/vwo/pageVisit/generated-types.ts @@ -2,7 +2,7 @@ export interface Payload { /** - * The URL of the page + * URL of the webpage */ url: string /** @@ -10,21 +10,21 @@ export interface Payload { */ vwoUuid: string /** - * Page Context + * Contains context information regarding a webpage */ page: { [k: string]: unknown } /** - * IP Address + * IP address of the user */ ip?: string /** - * User Agent + * User-Agent of the user */ userAgent: string /** - * Timestamp + * Timestamp on the event */ timestamp: string } diff --git a/packages/destination-actions/src/destinations/vwo/pageVisit/index.ts b/packages/destination-actions/src/destinations/vwo/pageVisit/index.ts index 1ebc21c1b5..c9359adfd4 100644 --- a/packages/destination-actions/src/destinations/vwo/pageVisit/index.ts +++ b/packages/destination-actions/src/destinations/vwo/pageVisit/index.ts @@ -5,10 +5,10 @@ import { formatPayload } from '../utility' const action: ActionDefinition = { title: 'Page Visit', - description: 'Sends page visit information to VWO', + description: `Sends Segment's page event to VWO`, fields: { url: { - description: 'The URL of the page', + description: 'URL of the webpage', label: 'Page URL', required: true, type: 'string', @@ -26,7 +26,7 @@ const action: ActionDefinition = { } }, page: { - description: 'Page Context', + description: 'Contains context information regarding a webpage', label: 'Page', required: true, type: 'object', @@ -35,7 +35,7 @@ const action: ActionDefinition = { } }, ip: { - description: 'IP Address', + description: 'IP address of the user', label: 'IP Address', required: false, type: 'string', @@ -44,7 +44,7 @@ const action: ActionDefinition = { } }, userAgent: { - description: 'User Agent', + description: 'User-Agent of the user', label: 'User Agent', required: true, type: 'string', @@ -53,7 +53,7 @@ const action: ActionDefinition = { } }, timestamp: { - description: 'Timestamp', + description: 'Timestamp on the event', label: 'Timestamp', required: true, type: 'string', diff --git a/packages/destination-actions/src/destinations/vwo/trackEvent/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/vwo/trackEvent/__tests__/__snapshots__/snapshot.test.ts.snap index 3f96d663e6..5ccbbaf398 100644 --- a/packages/destination-actions/src/destinations/vwo/trackEvent/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/vwo/trackEvent/__tests__/__snapshots__/snapshot.test.ts.snap @@ -4,7 +4,7 @@ exports[`Testing snapshot for actions-vwo-cloud's trackEvent destination action: Object { "d": Object { "event": Object { - "name": "segment_yw0zltLcbQ", + "name": "segment.yw0zltLcbQ", "props": Object { "isCustomEvent": true, "page": Object { @@ -30,7 +30,7 @@ exports[`Testing snapshot for actions-vwo-cloud's trackEvent destination action: Object { "d": Object { "event": Object { - "name": "segment_yw0zltLcbQ", + "name": "segment.yw0zltLcbQ", "props": Object { "isCustomEvent": true, "page": Object { diff --git a/packages/destination-actions/src/destinations/vwo/trackEvent/generated-types.ts b/packages/destination-actions/src/destinations/vwo/trackEvent/generated-types.ts index af1f740e91..255463966f 100644 --- a/packages/destination-actions/src/destinations/vwo/trackEvent/generated-types.ts +++ b/packages/destination-actions/src/destinations/vwo/trackEvent/generated-types.ts @@ -2,11 +2,11 @@ export interface Payload { /** - * The name of the event. + * Name of the event */ name: string /** - * A JSON object containing additional properties that will be associated with the event. + * JSON object containing additional properties that will be associated with the event. */ properties?: { [k: string]: unknown @@ -16,21 +16,21 @@ export interface Payload { */ vwoUuid: string /** - * Page Context + * Contains context information regarding a webpage */ page: { [k: string]: unknown } /** - * IP Address + * IP address of the user */ ip?: string /** - * User Agent + * User-Agent of the user */ userAgent: string /** - * Timestamp + * Timestamp on the event */ timestamp: string } diff --git a/packages/destination-actions/src/destinations/vwo/trackEvent/index.ts b/packages/destination-actions/src/destinations/vwo/trackEvent/index.ts index 054cf0e277..6b900fd0e6 100644 --- a/packages/destination-actions/src/destinations/vwo/trackEvent/index.ts +++ b/packages/destination-actions/src/destinations/vwo/trackEvent/index.ts @@ -5,11 +5,11 @@ import { formatPayload, sanitiseEventName } from '../utility' const action: ActionDefinition = { title: 'Track Event', - description: 'Sends track events to VWO', + description: `Sends Segment's track event to VWO`, defaultSubscription: 'type = "track"', fields: { name: { - description: 'The name of the event.', + description: 'Name of the event', label: 'Name', required: true, type: 'string', @@ -18,7 +18,7 @@ const action: ActionDefinition = { } }, properties: { - description: 'A JSON object containing additional properties that will be associated with the event.', + description: 'JSON object containing additional properties that will be associated with the event.', label: 'Properties', required: false, type: 'object', @@ -36,7 +36,7 @@ const action: ActionDefinition = { } }, page: { - description: 'Page Context', + description: 'Contains context information regarding a webpage', label: 'Page', required: true, type: 'object', @@ -45,7 +45,7 @@ const action: ActionDefinition = { } }, ip: { - description: 'IP Address', + description: 'IP address of the user', label: 'IP Address', required: false, type: 'string', @@ -54,7 +54,7 @@ const action: ActionDefinition = { } }, userAgent: { - description: 'User Agent', + description: 'User-Agent of the user', label: 'User Agent', required: true, type: 'string', @@ -63,7 +63,7 @@ const action: ActionDefinition = { } }, timestamp: { - description: 'Timestamp', + description: 'Timestamp on the event', label: 'Timestamp', required: true, type: 'string', diff --git a/packages/destination-actions/src/destinations/vwo/utility.ts b/packages/destination-actions/src/destinations/vwo/utility.ts index d373975b35..d568f2a5d7 100644 --- a/packages/destination-actions/src/destinations/vwo/utility.ts +++ b/packages/destination-actions/src/destinations/vwo/utility.ts @@ -45,13 +45,13 @@ export function formatPayload(name: string, payload: commonPayload, isCustomEven } export function sanitiseEventName(name: string) { - return 'segment_' + name + return 'segment.' + name } export function formatAttributes(attributes: { [k: string]: unknown } | undefined) { const formattedAttributes: { [k: string]: unknown } = {} for (const key in attributes) { - formattedAttributes[`segment_${key}`] = attributes[key] + formattedAttributes[`segment.${key}`] = attributes[key] } return formattedAttributes } diff --git a/packages/destination-actions/src/destinations/webhook/__test__/webhook.test.ts b/packages/destination-actions/src/destinations/webhook/__test__/webhook.test.ts index fdc94baa60..baa7696bb9 100644 --- a/packages/destination-actions/src/destinations/webhook/__test__/webhook.test.ts +++ b/packages/destination-actions/src/destinations/webhook/__test__/webhook.test.ts @@ -2,6 +2,7 @@ import nock from 'nock' import { createTestEvent, createTestIntegration } from '@segment/actions-core' import Webhook from '../index' import { createHmac, timingSafeEqual } from 'crypto' +import { SegmentEvent } from '@segment/actions-core' const testDestination = createTestIntegration(Webhook) @@ -95,5 +96,56 @@ describe('Webhook', () => { expect(responses.length).toBe(1) expect(responses[0].status).toBe(200) }) + + it('supports request signing with batched events', async () => { + const url = 'https://example.com' + + const events: SegmentEvent[] = [ + createTestEvent({ + properties: { cool: false } + }), + createTestEvent({ + properties: { cool: true } + }) + ] + + const payload = JSON.stringify(events.map(({ properties }) => properties)) + console.log(payload) + const sharedSecret = 'abc123' + nock(url) + .post('/', payload) + .reply(async function (_uri, body: any) { + // Normally you should use the raw body but nock automatically + // deserializes it (and doesn't allow us to access the raw request + // body) so we re-serialize the body here so that we can demonstrate + // signture validation + + // Validate the signature + const expectSignature = this.req.headers['x-signature'][0] + const actualSignature = createHmac('sha1', sharedSecret).update(JSON.stringify(body[0])).digest('hex') + + // Use constant-time comparison to avoid timing attacks + if ( + expectSignature.length !== actualSignature.length || + !timingSafeEqual(Buffer.from(actualSignature, 'hex'), Buffer.from(expectSignature, 'hex')) + ) { + return [400, 'Invalid signature123'] + } + + return [200, 'OK'] + }) + + const responses = await testDestination.testBatchAction('send', { + events, + mapping: { + url, + data: { '@path': '$.properties' } + }, + settings: { sharedSecret }, + useDefaultMappings: true + }) + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) + }) }) }) diff --git a/packages/destination-actions/src/destinations/webhook/index.ts b/packages/destination-actions/src/destinations/webhook/index.ts index abb0d63e98..60abc69ca6 100644 --- a/packages/destination-actions/src/destinations/webhook/index.ts +++ b/packages/destination-actions/src/destinations/webhook/index.ts @@ -20,10 +20,9 @@ const destination: DestinationDefinition = { } }, extendRequest: ({ settings, payload }) => { - if (settings.sharedSecret && payload?.data) { - const digest = createHmac('sha1', settings.sharedSecret) - .update(JSON.stringify(payload.data), 'utf8') - .digest('hex') + const payloadData = payload.length ? payload[0]['data'] : payload['data'] + if (settings.sharedSecret && payloadData) { + const digest = createHmac('sha1', settings.sharedSecret).update(JSON.stringify(payloadData), 'utf8').digest('hex') return { headers: { 'X-Signature': digest } } } return {} diff --git a/packages/destination-subscriptions/package.json b/packages/destination-subscriptions/package.json index 65b340ef6c..44145b8c7b 100644 --- a/packages/destination-subscriptions/package.json +++ b/packages/destination-subscriptions/package.json @@ -1,6 +1,6 @@ { "name": "@segment/destination-subscriptions", - "version": "3.15.0", + "version": "3.16.0", "description": "Validate event payload using subscription AST", "license": "MIT", "repository": { @@ -17,7 +17,7 @@ "test": "jest", "typecheck": "tsc -p tsconfig.build.json --noEmit", "prepare": "yarn build", - "size": "size-limit" + "size": "bash scripts/size.sh" }, "main": "dist/cjs/index.js", "module": "dist/esm/index.js", diff --git a/packages/destination-subscriptions/scripts/size.sh b/packages/destination-subscriptions/scripts/size.sh new file mode 100755 index 0000000000..8cd3f77bee --- /dev/null +++ b/packages/destination-subscriptions/scripts/size.sh @@ -0,0 +1,8 @@ +#!/bin/bash +NODE_VERSION="$(node --version)"; +NODE_VERSION_MAJOR="${NODE_VERSION:1}"; # strip the "v" prefix +NODE_VERSION_MAJOR="${NODE_VERSION_MAJOR%%.*}"; #get everything before the first dot +if [ "$NODE_VERSION_MAJOR" -ge "18" ]; then +NODE_OPTIONS=--openssl-legacy-provider size-limit +else size-limit +fi diff --git a/scripts/test-browser.sh b/scripts/test-browser.sh new file mode 100755 index 0000000000..d9e07195fb --- /dev/null +++ b/scripts/test-browser.sh @@ -0,0 +1,8 @@ +#!/bin/bash +NODE_VERSION="$(node --version)"; +NODE_VERSION_MAJOR="${NODE_VERSION:1}"; # strip the "v" prefix +NODE_VERSION_MAJOR="${NODE_VERSION_MAJOR%%.*}"; #get everything before the first dot +if [ "$NODE_VERSION_MAJOR" -ge "18" ]; then +lerna run build:karma --stream && NODE_OPTIONS=--openssl-legacy-provider karma start; +else lerna run build:karma --stream && karma start; +fi From 0247715ab2772ddfb4d5b1b65acb83752b1b4153 Mon Sep 17 00:00:00 2001 From: Noah Cooper Date: Sat, 18 Feb 2023 07:33:14 -0500 Subject: [PATCH 03/10] main (#9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Publish - @segment/actions-cli-internal@3.123.2 - @segment/actions-cli@3.123.2 - @segment/action-destinations@3.127.2 * Subscribe to more events (Redo without required type) (#986) * Add subscriptions to track event * Generate types and new snapshot * Add test * Improve labels and descriptions * Generated types * Change description * Generate types * Remove required * Generate types * Update Ripe web destination (#968) * Update Ripe web destination - Add new endpoint setting for testing purposes - Remove alias call - Update misleading anonymousId descriptions * Update erroneous default paths * Set anonymousId in identify call * Heap 34916 - add session_id + update segment library for tracking purposes (#787) * Fix events payload * Use the single event not the bulk * Fix tests * Fix should not override * remove console log and update SEGMENT_LIB var * update constant value * update browser tests as well * Adding Group support for customerio -Rename identifier field names (#973) * Initial commit for objects * Added Test cases * Adding Tests validation for the payload * committing generate type file * Adding group support from identify * Fixing conflicts * Adding traits to attributes property for createUpdateObject action * renaming id and type_id to object_id and object_type_id Co-authored-by: kishoredevarasettyn * SalesWings (Actions) Destination (#945) * Generated integration from scaffold * Fix action name * Implement SalesWings destination actions * Send user agent, rearrange fields * Bugfixes * Remove debug logging * First tests * Auth tests & track event tests * Page event tests * Identify event tests * Screen event tests * Event batch test * More event batch tests * Change API key description * Commit generated types * Minor cleanup * Fix square brackets in field description UI * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Add mandatory `.` to description Co-authored-by: Marín Alcaraz * Hardcoding timestamps for snapshot tests * Extract email from properties of Track event * Add action description * Add default subscription to action * Add destination present * Merge URL fields * Dedicated actions per event type * Cleanup * Update field descriptions * Update geenrated types Co-authored-by: Yevgeny Terov <73266004+yevsw@users.noreply.github.com> Co-authored-by: Marín Alcaraz Co-authored-by: Yevgeny Terov * Changing default subscription to group for group call (#995) * Initial commit for objects * Added Test cases * Adding Tests validation for the payload * committing generate type file * Adding group support from identify * Fixing conflicts * Adding traits to attributes property for createUpdateObject action * setting default subscription as group for createUpdateObject and addressing other review comments * correcting merge overrides Co-authored-by: kishoredevarasettyn * Add anonymous id as a user property (#981) * Update setting description in Google Ads Conversions (#983) * HGI-237 | Updated Description for Braze Cohorts Fields (#992) * udpated description for braze cohorts * updated description for braze cohorts * updated description in mapping fields * updated description * updated event_properties to hidden * made event_properties unhidden Co-authored-by: Gaurav Kochar * Increase CI timeout to 15 minutes and 10 minutes respectively (#985) * Increase CI timeout to 15 minutes * Bump browser tests to 10 minutes Co-authored-by: Nolan Chan * Pipedrive actions PE-20 (#996) * fix for pipedrive pe-20 issue * removing default visible_to * Register Saleswing Action (#999) Co-authored-by: Nolan Chan * Publish - @segment/browser-destinations@3.72.0 - @segment/actions-cli-internal@3.124.0 - @segment/actions-cli@3.124.0 - @segment/action-destinations@3.128.0 * ACT-362 Brackets Support (#993) * add support for brackets inside js keys in get method * add double quotes * explanatory text + new link * safari support * remove invalid bracket test since it is now supported * use class based regex to avoid parseError * actually convert the regex correctly * cleanup * split tests by functionality * Refactor .get test (#1000) * Heap Fix for empty event name (#1004) * fix for pe-52 * fixing breaking tests * Publish - @segment/actions-shared@1.32.0 - @segment/browser-destinations@3.73.0 - @segment/actions-cli-internal@3.125.0 - @segment/actions-cli@3.125.0 - @segment/actions-core@3.50.0 - @segment/action-destinations@3.129.0 * fix scaffolding for oauth (#1008) * [CHANNELS-329] Add WhatsApp support for Twilio Engage (#987) * feat: added whatsapp support * fix: added missing dependencies * refactor: minor cleanup * fix: moved dependency to package level * fix: uri encoding for get traits * fix: using same auth scheme for sms & whatsapp whatsApp thankfully allows using apiKeySid & apiSecret instead of accountSid & authToken * fix: reverted changed package version * feat: allow bypassing contentVariables reconciliation * Publish - @segment/actions-cli-internal@3.126.0 - @segment/actions-cli@3.126.0 - @segment/action-destinations@3.130.0 * Add browser destination tests with saucelabs (#994) * Node 18 Upgrade (#991) * packages * ci + nvm * lock for 18 * fix webpack hashing issue * node types to 18 * Update node version for browser-tests and snyk * Fix tests * Try to fix browser tests * Fix pipedrive unit tests * Fix domain in snapshot tests * Fix yarn subscriptions * Update README --------- Co-authored-by: Dan Lasky * Use node18 for browser tests destinations (#1014) * Contribution pe 53 (#1007) * updating contributing guidelines * adding extra instructions for post deployment changes * spelling corrections * spelling corrections * Update CONTRIBUTING.md Co-authored-by: SyedWasiHaider * Update CONTRIBUTING.md Co-authored-by: SyedWasiHaider --------- Co-authored-by: SyedWasiHaider * Qualtrics upsert transaction (#963) * adding upsert contact transacion destination * fixing snapshots * Updating perform function for upsertTransaction * Adding dynamic fields for directoryId. Updating field descriptions. Update upsertTransaction defaultSubscription * updating types * Update qualtrics destination name and descriptons on actions --------- Co-authored-by: Carl Lee * fixing a couple of issues with new Ironclad destination (#1002) * fixing a couple of issues with new Ironclad destination * adding updated generated types * fixing broken test * [salesforce] - Verify the `instanceUrl` is a valid Salesforce domain (#997) * Regex and WIP unit tests * Unit tests working * Updates regex and unit tests * Updates other unit tests * Saving package.json * Adds a couple more unit tests * Removes package.json from commits * Removes package.json from commits * Imports request client using absolute path instead of relative path * Enforce https * Publish - @segment/actions-shared@1.33.0 - @segment/browser-destinations-integration-tests@0.1.0 - @segment/browser-destinations@3.74.0 - @segment/actions-cli-internal@3.127.0 - @segment/actions-cli@3.127.0 - @segment/actions-core@3.51.0 - @segment/action-destinations@3.131.0 - @segment/destination-subscriptions@3.15.0 * Fix CommandBar browser destination initialization when CommandBar has already been loaded through other means (#1009) Co-authored-by: Thomas Kainrad * remove flow that attempts to create a JIRA ticket (#1021) * Twilio Studio as a Segment Action Destination (#1023) * Twilio Studio as a Segment Action Destination * Replaced phone number with userid in the cache key * Addressed review comments * DOTORG-839: Blackbaud Raiser's Edge NXT Destination (#998) * DOTORG-839: Create or Update Individual Constituent Action (#1) * DOTORG-839 Added OAuth2 settings for Blackbaud (#2) * Move bbApiSubscriptionKey to settings * Only aggregate integrationErrors * Update Online Presence label * Update directory structure * Add types * Abstract API calls * Add dateStringToFuzzyDate * Add types * Don't retry 401s * Don't catch errors on constituent search or creation * Concatenate integrationErrors * Add throwHttpErrors * Set default for lookup_id to userId * Pass constituentId to updateConstituent * Remove try/catch * Use camelCase traits * Add filterObjectListByMatchFields * Check if primary property is defined * DOTORG-839 Added authentication test (#3) * Don't match on country * Use datetime type * Strip non-numeric characters from phone when matching * Don't match on undefined boolean fields * Update generated-types.ts * Fix linting errors * Move fixtures out of tests directory * Update constituentData * Update default lookup_id mapping * Update testAuthentication * Remove UNEXPECTED_RECORD_COUNT error * Update tests --------- Co-authored-by: twilio-hwong <91703194+twilio-hwong@users.noreply.github.com> * Google Analytics 4 Web Destination (#1012) * addPaymentInfo Action * ga4 types, properties, and functions * set config fields action poc * set config fields action poc * GA4 all action created * Remove unit test cases files * Added defaultSubscription tag in viewItemList and generateLead Action * added register code for GA4 in broweser-destinations * add customEvent action * set config action & custom event action * Delete snapshot.test.ts * Delete index.test.ts * clean up & add mappings for preset * updated types and removed picklist * Added test cases for GA4 actions * Added test cases * added custom event unit test + cleaned up merge issues and commented code * fixed typo * add back ripe and commandbar to index file * added comment on non using variable * Apply suggestions from code review Co-authored-by: Neek Sandhu * revert set configuration field actions * added updateUser function, and updated event to payload * reverted back gtag function and datalayer setup * add gtag type and remove comment * update yarn.lock file * add viewItemList & disable linter for args * remove gtag.js type dependency * update unit tests * added events to defaultSubscriptions * update index.js --------- Co-authored-by: Varadarajan V Co-authored-by: Ankit Gupta Co-authored-by: Neek Sandhu * livelike-cloud action destination (PE-41) (#1020) * created livelike-cloud action destination with one trackEvent action and three presets and added unit tests * Update packages/destination-actions/src/destinations/livelike-cloud/trackEvent/index.ts Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> --------- Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> * Qualtrics - Fixed timezone issue in unit test (#1026) * Fixed timezone issue in unit test * refactored to use dayjs from lib * generate types (#1028) * PE-47 - Merge Algolia Insights (#1027) * Algolia insights integration (#975) * feat: initial commit after scaffold * feat: clarify insights API and impliment as distinct actions * test: write test for conversion destination * test: write test for click and view destination * test: write auth schema test * create productClick presets * add presets to destination * create all event presets * remove default imports for actions * add testAuthentication method for algolia-insights --------- Co-authored-by: Beatrice Parfait * fixing snapshots and replacing userId and anonId with userToken * fixing timestamps in tests --------- Co-authored-by: Wesley Walser Co-authored-by: Beatrice Parfait * VWO Cloud Mode Description changes (#1015) * Description changes and changes in utility * Tests altered for new format * Update CODEOWNERS (#1029) * [HEAP-38485] Move to the integrations endpoint (#1016) * [HEAP-32036] First trackEvent implementation (#8) * [HEAP-32037] Send identify and add user properties requests (#9) * [HEAP-32037] Add user properties and migrate anonymous users * Hash anonymous user ID * Use message ID as idempotency key * Throw if parameters are missing * [HEAP-32884] backport filtering event properties (#10) * [HEAP-32884] backport filtering event properties - flat properties from the request payload before sending them to heap * address comments: - remove the embeded object under the util.ts, move it to flattenObj - remove unnecessary tests on identifyUser - import and use the embeded object and flatten object in trackEvent and identifyUser test not part of comments: - rename the file from flattenObj to flat * PR comments * Update packages/destination-actions/src/destinations/heap/trackEvent/index.ts Co-authored-by: Marín Alcaraz * Update packages/destination-actions/src/destinations/heap/trackEvent/index.ts Co-authored-by: Marín Alcaraz * Update packages/destination-actions/src/destinations/heap/trackEvent/index.ts Co-authored-by: Marín Alcaraz * Update packages/destination-actions/src/destinations/heap/trackEvent/index.ts Co-authored-by: Marín Alcaraz * remove unused file * save changes * save * fix library * remove accidental file * update tests * remove session id * PR comments * fix the tests * add user properties if more that one identifier is present * Revert "add user properties if more that one identifier is present" This reverts commit bb4fb94684be54c933e9375ae1fc877e876fbbd1. --------- Co-authored-by: Gediminas Rapolavicius Co-authored-by: Yiyang Li <93153941+yiyangli-heap@users.noreply.github.com> Co-authored-by: Marín Alcaraz Co-authored-by: murphpdx * Launchpad Segment Integration (#1010) * Launchpad Segment Integration We are now adding the ability to send events from segment into Launchpad.pm - the mission control for Product teams at scale. We are adding the following: * trackEvent * groupIdentifyUser * identifyUser We have added unit tests and have tested this manually. It requires the following: * apiRegion - EU by default. We have added the US as means of future extensibility. * apiSecret - given by Launchpad.pm while onboarding. * sourceName - to be added. * updating snapshots * Changes implemented following call with Joe Track() [x] Explicitly indicate which fields are required or optional [x] Look for traits and if not context.traits [x] Need description for the Action [x] Make Timestamp field required [x] Make messageId required. It will always be there. [x] getEventProperties: This line looks incorrect: id: payload.event, [x] source mapping looks incorrect: source: integration?.name == 'Iterable' ? 'Iterable' : 'segment', Identify() [x] Description should be updated. [x] identify() calls are often fired when a trait is collected, or when a userId is collected. [x] Maybe use wording: “Creates or updates a user profile, and adds or updates trait values on the user profile…” [x] Refactor the perform() function group() [x] Group Key - Amend description to better explain that group key is a way to connect multiple organizations together [x] groupId field - Should be updated [x] Consider adding anonymousId as a field [x] Handle when there are no traits. * changes * tests passing, removed the check on user_id, anonymous_id since none are required * fixing the types as well * fixing test --------- Co-authored-by: joe-ayoub-segment <45374896+joe-ayoub-segment@users.noreply.github.com> * [STRATCONN-1950] Init new destination `Pinterest Conversions API` (#1030) * Init new destination `Pinterest Conversions API` * Update index.ts * Update index.ts * Update index.ts --------- Co-authored-by: rvadera12 <89420099+rvadera12@users.noreply.github.com> * Try to only pass in NODE_OPTIONS for node18+ (#1032) * Try using conditional for scripts * Remove NODE_OPTIONS from else * Echo the nodeversion * Try setting the node major version and testing that * Use correct variable in condition * Try a different approach * Use bash instead * Try to fix script * Add other remaining scripts * Changing name of new Algolia Insights Integration (#1038) * Register new action destinations (#1036) * Register new action destinations: Launchpad, Livelike Cloud, Twilio Studio, BlackBaud Raisers Edge Nxt, Pinterest Conversions Api * Fix path * add action to pinterest conversions api * add action to pinterest conversions api * add description to pinterest action * Register algolia insights (Actions) --------- Co-authored-by: rvadera12 * Publish - @segment/actions-shared@1.34.0 - @segment/browser-destinations@3.75.0 - @segment/actions-cli-internal@3.128.0 - @segment/actions-cli@3.128.0 - @segment/actions-core@3.52.0 - @segment/action-destinations@3.132.0 - @segment/destination-subscriptions@3.16.0 * Use correct defaultPath for messageId (#1041) * Google ads v11 to v12 (#1018) * gooogle conversion v12 upgrade * changes * bug fix for test cases * gooogle conversion v12 upgrade * changes * bug fix for test cases * flag name changes * flag name change in test cases * review changes * revert ga4-types file change * revert ga4-types file change * revert ga4-types file change --------- Co-authored-by: manoj kumar * [STRATCONN-1779]Add datadog stats for google ads api version (#1046) * adds stats for api version * adds missing statscontext parameter to getCustomVariables * refactor params * Voucherify-Segment.io Integration using action-destinations (#970) * initial destination configuration * identify customer action * add trackEvent * Generate screenEvent and pageEvent * Change the structure of track, page and screen events. * Delete test folder for now * Change the type definition * Delete timestamp * create group event * removed created_at property * hit to localhost address * Add unit tests * Some fixes * Change the URLs in perform method * Update URLs in tests * Delete unused testing authentication fn * Add snapshots * Add ability to pass a custom URL * Set type to required in page and screen events * Delete snapshots * Replace api endpoint (with regions) with custom URL * if there's no userId then use the anonymousId * removed space * added type property to rest of events * changed name of property 'name' to 'event' * Update index.ts * Update generated-types.ts * Delete unnecessary test - 'should throw an error when the name is not provided using page event' * Delete the 'voucherify' prefix * Slightly change the descriptions * generated types * Separate URL functions into separate files Change the file names to be more descriptive. * Reduce the getVoucherifyEndpointURL function * delete * Update the descriptions - Also deleted the 'event' prop from screen/page events and now the 'name' in screen/page event is no longer required. * Commit the generated types * Reduce the number of events to three. Track Custom Event, Identify Customer, Add group to customer metadata * Add customer attributes to traits in upsertCustomer action * Update generated-types.ts * Add testAuthentication * Update names of actions in unit tests * add firstName and lastName * Add email to custom event processing * Delete email from upsertCustomer (leave it only in traits) * Add email to description * Add email to customer processing * Update testAuthentication * update addCustomEvent * Update packages/destination-actions/src/destinations/voucherify/upsertCustomer/index.ts Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> * Fix issue with mapping the user attributes and improve Presets * generate types * change email properties * generated types * custom url information * generated types * Update the desc of Custom URL * minor changes from last PR * Update index.ts --------- Co-authored-by: Patryk Smolarz Co-authored-by: Patryk Smolarz <77458595+patricioo1@users.noreply.github.com> Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> * HGI 372 - Fix Segment Profiles Destination (#1047) * Added `All: false` in Tracking API event * Changed PAPI Token field from `string` to `password` * Fixed minor typos * HGI-368 | Fixed the missing x-signature in request header when batching is enabled (#1043) * worked on HGI-368 Fix * added a unit test case for same fix --------- Co-authored-by: Gaurav Kochar * Mixpanel destination: use user-agent for browser data (#1035) * remove device manufacturer - use user-agent * userAgent is only sent on web * New SalesWings API urls (#1039) * Use new SalesWings API urls * Do not check response status in testAuthentication * Fix testAuthentication * Remove unused file * Mixpanel - identify null id fix (#1033) * fix engage for null user_id * use distinct_id with default for /engage * formatting fix * revert some more auto formatting * formatting * fix test * Remove distinct_id mapping and use null-coalescing * Descriptions changed for VWO Web Destination (#1006) * Descriptions changed * Revert Changes for cloud mode * Test altered for new format * fixing Mixpanel broken test (#1055) --------- Co-authored-by: Nick Aguilar Co-authored-by: Stella Chung Co-authored-by: Simon Co-authored-by: A Murphy Co-authored-by: kishoredevarasettyn <97026912+kishoredevarasettyn@users.noreply.github.com> Co-authored-by: kishoredevarasettyn Co-authored-by: Denis Egorushkin <98813888+denis-egorushkin-sw@users.noreply.github.com> Co-authored-by: Yevgeny Terov <73266004+yevsw@users.noreply.github.com> Co-authored-by: Marín Alcaraz Co-authored-by: Yevgeny Terov Co-authored-by: maryamsharif <99763167+maryamsharif@users.noreply.github.com> Co-authored-by: Innovative-GauravKochar <117165746+Innovative-GauravKochar@users.noreply.github.com> Co-authored-by: Gaurav Kochar Co-authored-by: Nolan Chan Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> Co-authored-by: Nolan Chan Co-authored-by: Dan Co-authored-by: Seth Silesky <5115498+silesky@users.noreply.github.com> Co-authored-by: rhall-twilio <103517471+rhall-twilio@users.noreply.github.com> Co-authored-by: alfrimpong <119889384+alfrimpong@users.noreply.github.com> Co-authored-by: SyedWasiHaider Co-authored-by: Dan Lasky Co-authored-by: drakauskas <119876674+drakauskas@users.noreply.github.com> Co-authored-by: Carl Lee Co-authored-by: Wasi Haider Co-authored-by: Thomas Kainrad <7394822+tkainrad@users.noreply.github.com> Co-authored-by: Thomas Kainrad Co-authored-by: aradhakrishnan-twilio <116877054+aradhakrishnan-twilio@users.noreply.github.com> Co-authored-by: twilio-hwong <91703194+twilio-hwong@users.noreply.github.com> Co-authored-by: rvadera12 <89420099+rvadera12@users.noreply.github.com> Co-authored-by: Varadarajan V Co-authored-by: Ankit Gupta Co-authored-by: Neek Sandhu Co-authored-by: Abhishek Kansal Co-authored-by: Sayan Das <109198085+sayan-das-in@users.noreply.github.com> Co-authored-by: Wesley Walser Co-authored-by: Beatrice Parfait Co-authored-by: Prathamesh Tamanekar Co-authored-by: Gediminas Rapolavicius Co-authored-by: Yiyang Li <93153941+yiyangli-heap@users.noreply.github.com> Co-authored-by: murphpdx Co-authored-by: Stefan Sabev Co-authored-by: Logan Luque <98849774+LLuque-twilio@users.noreply.github.com> Co-authored-by: rvadera12 Co-authored-by: Logan Luque Co-authored-by: immanojkumar <117071418+immanojkumar@users.noreply.github.com> Co-authored-by: manoj kumar Co-authored-by: Varadarajan V <109586712+varadarajan-tw@users.noreply.github.com> Co-authored-by: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Co-authored-by: Patryk Smolarz Co-authored-by: Patryk Smolarz <77458595+patricioo1@users.noreply.github.com> Co-authored-by: Sonya Park <68977514+spjtls9@users.noreply.github.com> --- .../vwo/identifyUser/__tests__/index.test.ts | 2 +- .../vwo/identifyUser/generated-types.ts | 2 +- .../destinations/vwo/identifyUser/index.ts | 4 +-- .../vwo/trackEvent/__tests__/index.test.ts | 4 +-- .../vwo/trackEvent/generated-types.ts | 4 +-- .../src/destinations/vwo/trackEvent/index.ts | 6 ++-- .../src/destinations/vwo/utility.ts | 4 +-- .../identifyUser/__tests__/index.test.ts | 33 ++++++++++++++++++- .../mixpanel/identifyUser/index.ts | 6 ++-- .../trackPurchase/__tests__/index.test.ts | 15 ++++----- 10 files changed, 55 insertions(+), 25 deletions(-) diff --git a/packages/browser-destinations/src/destinations/vwo/identifyUser/__tests__/index.test.ts b/packages/browser-destinations/src/destinations/vwo/identifyUser/__tests__/index.test.ts index 5871a2a95b..3f1cb5a34c 100644 --- a/packages/browser-destinations/src/destinations/vwo/identifyUser/__tests__/index.test.ts +++ b/packages/browser-destinations/src/destinations/vwo/identifyUser/__tests__/index.test.ts @@ -54,7 +54,7 @@ describe('VWO.identifyUser', () => { expect(mockVWO.visitor).toHaveBeenCalledWith( { - segment_textAttribute: 'Hello' + 'segment.textAttribute': 'Hello' }, { source: 'segment.web' diff --git a/packages/browser-destinations/src/destinations/vwo/identifyUser/generated-types.ts b/packages/browser-destinations/src/destinations/vwo/identifyUser/generated-types.ts index e20b58e518..cf4b08fc18 100644 --- a/packages/browser-destinations/src/destinations/vwo/identifyUser/generated-types.ts +++ b/packages/browser-destinations/src/destinations/vwo/identifyUser/generated-types.ts @@ -2,7 +2,7 @@ export interface Payload { /** - * A JSON object containing additional attributes that will be associated with the user. + * JSON object containing additional attributes that will be associated with the user. */ attributes: { [k: string]: unknown diff --git a/packages/browser-destinations/src/destinations/vwo/identifyUser/index.ts b/packages/browser-destinations/src/destinations/vwo/identifyUser/index.ts index dd0eb24742..af9b98552a 100644 --- a/packages/browser-destinations/src/destinations/vwo/identifyUser/index.ts +++ b/packages/browser-destinations/src/destinations/vwo/identifyUser/index.ts @@ -7,12 +7,12 @@ import { formatAttributes } from '../utility' // Change from unknown to the partner SDK types const action: BrowserActionDefinition = { title: 'Identify User', - description: 'Forwards user traits to VWO Data360 attributes', + description: `Sends Segment's page event to VWO`, defaultSubscription: 'type = "identify"', platform: 'web', fields: { attributes: { - description: 'A JSON object containing additional attributes that will be associated with the user.', + description: 'JSON object containing additional attributes that will be associated with the user.', label: 'Attributes', required: true, type: 'object', diff --git a/packages/browser-destinations/src/destinations/vwo/trackEvent/__tests__/index.test.ts b/packages/browser-destinations/src/destinations/vwo/trackEvent/__tests__/index.test.ts index d142770094..9501a276e9 100644 --- a/packages/browser-destinations/src/destinations/vwo/trackEvent/__tests__/index.test.ts +++ b/packages/browser-destinations/src/destinations/vwo/trackEvent/__tests__/index.test.ts @@ -54,7 +54,7 @@ describe('VWO.trackEvent', () => { await trackEvent.track?.(context) expect(mockVWO.event).toHaveBeenCalledWith( - 'segment_ctaClick', + 'segment.ctaClick', {}, { source: 'segment.web', @@ -74,7 +74,7 @@ describe('VWO.trackEvent', () => { await trackEvent.track?.(context) expect(mockVWO.event).toHaveBeenCalledWith( - 'segment_buyButtonClick', + 'segment.buyButtonClick', { amount: 1000 }, diff --git a/packages/browser-destinations/src/destinations/vwo/trackEvent/generated-types.ts b/packages/browser-destinations/src/destinations/vwo/trackEvent/generated-types.ts index 26263ab44c..dee238aebe 100644 --- a/packages/browser-destinations/src/destinations/vwo/trackEvent/generated-types.ts +++ b/packages/browser-destinations/src/destinations/vwo/trackEvent/generated-types.ts @@ -2,11 +2,11 @@ export interface Payload { /** - * The name of the event. + * Name of the event. */ eventName: string /** - * A JSON object containing additional properties that will be associated with the event. + * JSON object containing additional properties that will be associated with the event. */ properties?: { [k: string]: unknown diff --git a/packages/browser-destinations/src/destinations/vwo/trackEvent/index.ts b/packages/browser-destinations/src/destinations/vwo/trackEvent/index.ts index 1dd726e768..6517a3d49a 100644 --- a/packages/browser-destinations/src/destinations/vwo/trackEvent/index.ts +++ b/packages/browser-destinations/src/destinations/vwo/trackEvent/index.ts @@ -7,12 +7,12 @@ import { sanitiseEventName } from '../utility' // Change from unknown to the partner SDK types const action: BrowserActionDefinition = { title: 'Track Event', - description: 'Forwards track events to VWO Data360', + description: `Sends Segment's track event to VWO`, platform: 'web', defaultSubscription: 'type = "track"', fields: { eventName: { - description: 'The name of the event.', + description: 'Name of the event.', label: 'Name', required: true, type: 'string', @@ -21,7 +21,7 @@ const action: BrowserActionDefinition = { } }, properties: { - description: 'A JSON object containing additional properties that will be associated with the event.', + description: 'JSON object containing additional properties that will be associated with the event.', label: 'Properties', required: false, type: 'object', diff --git a/packages/browser-destinations/src/destinations/vwo/utility.ts b/packages/browser-destinations/src/destinations/vwo/utility.ts index f555094020..543c49792a 100644 --- a/packages/browser-destinations/src/destinations/vwo/utility.ts +++ b/packages/browser-destinations/src/destinations/vwo/utility.ts @@ -1,11 +1,11 @@ export function formatAttributes(attributes: { [k: string]: unknown }) { const formattedAttributes: { [k: string]: unknown } = {} for (const key in attributes) { - formattedAttributes[`segment_${key}`] = attributes[key] + formattedAttributes[`segment.${key}`] = attributes[key] } return formattedAttributes } export function sanitiseEventName(name: string) { - return 'segment_' + name + return 'segment.' + name } diff --git a/packages/destination-actions/src/destinations/mixpanel/identifyUser/__tests__/index.test.ts b/packages/destination-actions/src/destinations/mixpanel/identifyUser/__tests__/index.test.ts index 3f178f808d..47c0fddcd3 100644 --- a/packages/destination-actions/src/destinations/mixpanel/identifyUser/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/mixpanel/identifyUser/__tests__/index.test.ts @@ -165,7 +165,6 @@ describe('Mixpanel.identifyUser', () => { }) }) ) - }) it('should use EU server URL', async () => { @@ -305,4 +304,36 @@ describe('Mixpanel.identifyUser', () => { }) ) }) + + it('should use anonymous_id as distinct_id if user_id is missing', async () => { + const event = createTestEvent({ userId: null, traits: { abc: '123' } }) + + nock('https://api.mixpanel.com').post('/track').reply(200, {}) + nock('https://api.mixpanel.com').post('/engage').reply(200, {}) + + const responses = await testDestination.testAction('identifyUser', { + event, + useDefaultMappings: true, + settings: { + projectToken: MIXPANEL_PROJECT_TOKEN, + apiSecret: MIXPANEL_API_SECRET, + sourceName: 'example segment source name' + } + }) + + expect(responses[1].status).toBe(200) + expect(responses[1].data).toMatchObject({}) + expect(responses[1].options.body).toMatchObject( + new URLSearchParams({ + data: JSON.stringify({ + $token: MIXPANEL_PROJECT_TOKEN, + $distinct_id: event.anonymousId, + $ip: '8.8.8.8', + $set: { + abc: '123' + } + }) + }) + ) + }) }) diff --git a/packages/destination-actions/src/destinations/mixpanel/identifyUser/index.ts b/packages/destination-actions/src/destinations/mixpanel/identifyUser/index.ts index b198f3410a..35ca321e35 100644 --- a/packages/destination-actions/src/destinations/mixpanel/identifyUser/index.ts +++ b/packages/destination-actions/src/destinations/mixpanel/identifyUser/index.ts @@ -65,7 +65,7 @@ const action: ActionDefinition = { } } - const identifyResponse = await request(`${ apiServerUrl }/track`, { + const identifyResponse = await request(`${apiServerUrl}/track`, { method: 'post', body: new URLSearchParams({ data: JSON.stringify(identifyEvent) }) }) @@ -91,12 +91,12 @@ const action: ActionDefinition = { } const data = { $token: settings.projectToken, - $distinct_id: payload.user_id, + $distinct_id: payload.user_id ?? payload.anonymous_id, $ip: payload.ip, $set: traits } - const engageResponse = request(`${ apiServerUrl }/engage`, { + const engageResponse = request(`${apiServerUrl}/engage`, { method: 'post', body: new URLSearchParams({ data: JSON.stringify(data) }) }) diff --git a/packages/destination-actions/src/destinations/mixpanel/trackPurchase/__tests__/index.test.ts b/packages/destination-actions/src/destinations/mixpanel/trackPurchase/__tests__/index.test.ts index 2b35a694ee..4694a9e5c1 100644 --- a/packages/destination-actions/src/destinations/mixpanel/trackPurchase/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/mixpanel/trackPurchase/__tests__/index.test.ts @@ -55,7 +55,7 @@ const orderCompletedEvent: Partial = { } const expectedOrderCompleted = { - ...omit(orderCompletedEvent, ['anonymousId', 'messageId', 'timestamp', 'type', 'userId']), + ...omit(orderCompletedEvent, ['anonymousId', 'messageId', 'timestamp', 'type', 'userId']) } const mapping = { @@ -76,7 +76,6 @@ describe('Mixpanel.trackPurchase', () => { nock('https://api.mixpanel.com').post('/import?strict=1').reply(200, {}) - const responses = await testDestination.testAction('trackPurchase', { event, mapping, @@ -183,7 +182,7 @@ describe('Mixpanel.trackPurchase', () => { settings: { projectToken: MIXPANEL_PROJECT_TOKEN, apiSecret: MIXPANEL_API_SECRET, - apiRegion: ApiRegions.US, + apiRegion: ApiRegions.US } }) expect(responses.length).toBe(1) @@ -285,7 +284,7 @@ describe('Mixpanel.trackPurchase', () => { ip: '8.8.8.8', id: 'abc123', distinct_id: 'abc123', - $browser: 'Mozilla', + $browser: 'Safari', $current_url: 'https://segment.com/academy/', $insert_id: '112c2a3c-7242-4327-9090-48a89de6a4110', $lib_version: '2.11.1', @@ -336,7 +335,7 @@ describe('Mixpanel.trackPurchase', () => { ip: '8.8.8.8', id: 'abc123', distinct_id: 'abc123', - $browser: 'Mozilla', + $browser: 'Safari', $current_url: 'https://segment.com/academy/', $insert_id: '0112c2a3c-7242-4327-9090-48a89de6a4110', $lib_version: '2.11.1', @@ -367,7 +366,7 @@ describe('Mixpanel.trackPurchase', () => { ip: '8.8.8.8', id: 'abc123', distinct_id: 'abc123', - $browser: 'Mozilla', + $browser: 'Safari', $current_url: 'https://segment.com/academy/', $insert_id: '1112c2a3c-7242-4327-9090-48a89de6a4110', $lib_version: '2.11.1', @@ -405,7 +404,7 @@ describe('Mixpanel.trackPurchase', () => { expect(responses.length).toBe(1) expect(responses[0].status).toBe(200) expect(responses[0].options.body).toMatchInlineSnapshot( - `"[{\\"event\\":\\"Order Completed\\",\\"properties\\":{\\"time\\":1629213675449,\\"ip\\":\\"8.8.8.8\\",\\"id\\":\\"abc123\\",\\"$anon_id\\":\\"anon-2134\\",\\"distinct_id\\":\\"abc123\\",\\"$browser\\":\\"Mozilla\\",\\"$current_url\\":\\"https://segment.com/academy/\\",\\"$identified_id\\":\\"abc123\\",\\"$insert_id\\":\\"112c2a3c-7242-4327-9090-48a89de6a4110\\",\\"$lib_version\\":\\"2.11.1\\",\\"$locale\\":\\"en-US\\",\\"$source\\":\\"segment\\",\\"$user_id\\":\\"abc123\\",\\"mp_country_code\\":\\"United States\\",\\"mp_lib\\":\\"Segment Actions: analytics.js\\",\\"affiliation\\":\\"Super Online Store\\",\\"order_id\\":\\"order-id-123\\",\\"checkout_id\\":\\"checkout-id-123\\",\\"coupon\\":\\"Mixpanel Day\\",\\"currency\\":\\"USD\\",\\"products\\":[{\\"product_id\\":\\"507f1f77bcf86cd799439011\\",\\"sku\\":\\"45790-32\\",\\"name\\":\\"Monopoly: 3rd Edition\\",\\"price\\":19,\\"position\\":1,\\"quantity\\":2,\\"coupon\\":\\"MOUNTAIN\\",\\"brand\\":\\"Unknown\\",\\"category\\":\\"Games\\",\\"variant\\":\\"Black\\",\\"url\\":\\"https://www.example.com/product/path\\",\\"image_url\\":\\"https://www.example.com/product/path.jpg\\"},{\\"product_id\\":\\"505bd76785ebb509fc183733\\",\\"sku\\":\\"46493-32\\",\\"name\\":\\"Uno Card Game\\",\\"price\\":3,\\"position\\":2,\\"category\\":\\"Games\\",\\"custom\\":\\"xyz\\"}],\\"revenue\\":5.99,\\"shipping\\":1.5,\\"tax\\":3,\\"total\\":24.48}}]"` + `"[{\\"event\\":\\"Order Completed\\",\\"properties\\":{\\"time\\":1629213675449,\\"ip\\":\\"8.8.8.8\\",\\"id\\":\\"abc123\\",\\"$anon_id\\":\\"anon-2134\\",\\"distinct_id\\":\\"abc123\\",\\"$browser\\":\\"Safari\\",\\"$browser_version\\":\\"9.0\\",\\"$current_url\\":\\"https://segment.com/academy/\\",\\"$identified_id\\":\\"abc123\\",\\"$insert_id\\":\\"112c2a3c-7242-4327-9090-48a89de6a4110\\",\\"$lib_version\\":\\"2.11.1\\",\\"$locale\\":\\"en-US\\",\\"$source\\":\\"segment\\",\\"$user_id\\":\\"abc123\\",\\"mp_country_code\\":\\"United States\\",\\"mp_lib\\":\\"Segment Actions: analytics.js\\",\\"affiliation\\":\\"Super Online Store\\",\\"order_id\\":\\"order-id-123\\",\\"checkout_id\\":\\"checkout-id-123\\",\\"coupon\\":\\"Mixpanel Day\\",\\"currency\\":\\"USD\\",\\"products\\":[{\\"product_id\\":\\"507f1f77bcf86cd799439011\\",\\"sku\\":\\"45790-32\\",\\"name\\":\\"Monopoly: 3rd Edition\\",\\"price\\":19,\\"position\\":1,\\"quantity\\":2,\\"coupon\\":\\"MOUNTAIN\\",\\"brand\\":\\"Unknown\\",\\"category\\":\\"Games\\",\\"variant\\":\\"Black\\",\\"url\\":\\"https://www.example.com/product/path\\",\\"image_url\\":\\"https://www.example.com/product/path.jpg\\"},{\\"product_id\\":\\"505bd76785ebb509fc183733\\",\\"sku\\":\\"46493-32\\",\\"name\\":\\"Uno Card Game\\",\\"price\\":3,\\"position\\":2,\\"category\\":\\"Games\\",\\"custom\\":\\"xyz\\"}],\\"revenue\\":5.99,\\"shipping\\":1.5,\\"tax\\":3,\\"total\\":24.48}}]"` ) }) @@ -422,7 +421,7 @@ describe('Mixpanel.trackPurchase', () => { expect(responses.length).toBe(1) expect(responses[0].status).toBe(200) expect(responses[0].options.body).toMatchInlineSnapshot( - `"[{\\"event\\":\\"Order Completed\\",\\"properties\\":{\\"time\\":1629213675449,\\"ip\\":\\"8.8.8.8\\",\\"id\\":\\"abc123\\",\\"$anon_id\\":\\"anon-2134\\",\\"distinct_id\\":\\"abc123\\",\\"$browser\\":\\"Mozilla\\",\\"$current_url\\":\\"https://segment.com/academy/\\",\\"$identified_id\\":\\"abc123\\",\\"$insert_id\\":\\"112c2a3c-7242-4327-9090-48a89de6a4110\\",\\"$lib_version\\":\\"2.11.1\\",\\"$locale\\":\\"en-US\\",\\"$source\\":\\"segment\\",\\"$user_id\\":\\"abc123\\",\\"mp_country_code\\":\\"United States\\",\\"mp_lib\\":\\"Segment Actions: analytics.js\\",\\"affiliation\\":\\"Super Online Store\\",\\"order_id\\":\\"order-id-123\\",\\"checkout_id\\":\\"checkout-id-123\\",\\"coupon\\":\\"Mixpanel Day\\",\\"currency\\":\\"USD\\",\\"products\\":[{\\"product_id\\":\\"507f1f77bcf86cd799439011\\",\\"sku\\":\\"45790-32\\",\\"name\\":\\"Monopoly: 3rd Edition\\",\\"price\\":19,\\"position\\":1,\\"quantity\\":2,\\"coupon\\":\\"MOUNTAIN\\",\\"brand\\":\\"Unknown\\",\\"category\\":\\"Games\\",\\"variant\\":\\"Black\\",\\"url\\":\\"https://www.example.com/product/path\\",\\"image_url\\":\\"https://www.example.com/product/path.jpg\\"},{\\"product_id\\":\\"505bd76785ebb509fc183733\\",\\"sku\\":\\"46493-32\\",\\"name\\":\\"Uno Card Game\\",\\"price\\":3,\\"position\\":2,\\"category\\":\\"Games\\",\\"custom\\":\\"xyz\\"}],\\"revenue\\":5.99,\\"shipping\\":1.5,\\"tax\\":3,\\"total\\":24.48}},{\\"event\\":\\"Product Purchased\\",\\"properties\\":{\\"time\\":1629213675448,\\"ip\\":\\"8.8.8.8\\",\\"id\\":\\"abc123\\",\\"$anon_id\\":\\"anon-2134\\",\\"distinct_id\\":\\"abc123\\",\\"$browser\\":\\"Mozilla\\",\\"$current_url\\":\\"https://segment.com/academy/\\",\\"$identified_id\\":\\"abc123\\",\\"$insert_id\\":\\"0112c2a3c-7242-4327-9090-48a89de6a4110\\",\\"$lib_version\\":\\"2.11.1\\",\\"$locale\\":\\"en-US\\",\\"$source\\":\\"segment\\",\\"$user_id\\":\\"abc123\\",\\"mp_country_code\\":\\"United States\\",\\"mp_lib\\":\\"Segment Actions: analytics.js\\",\\"order_id\\":\\"order-id-123\\",\\"checkout_id\\":\\"checkout-id-123\\",\\"product_id\\":\\"507f1f77bcf86cd799439011\\",\\"sku\\":\\"45790-32\\",\\"category\\":\\"Games\\",\\"name\\":\\"Monopoly: 3rd Edition\\",\\"brand\\":\\"Unknown\\",\\"variant\\":\\"Black\\",\\"price\\":19,\\"quantity\\":2,\\"coupon\\":\\"MOUNTAIN\\",\\"position\\":1,\\"url\\":\\"https://www.example.com/product/path\\",\\"image_url\\":\\"https://www.example.com/product/path.jpg\\"}},{\\"event\\":\\"Product Purchased\\",\\"properties\\":{\\"time\\":1629213675447,\\"ip\\":\\"8.8.8.8\\",\\"id\\":\\"abc123\\",\\"$anon_id\\":\\"anon-2134\\",\\"distinct_id\\":\\"abc123\\",\\"$browser\\":\\"Mozilla\\",\\"$current_url\\":\\"https://segment.com/academy/\\",\\"$identified_id\\":\\"abc123\\",\\"$insert_id\\":\\"1112c2a3c-7242-4327-9090-48a89de6a4110\\",\\"$lib_version\\":\\"2.11.1\\",\\"$locale\\":\\"en-US\\",\\"$source\\":\\"segment\\",\\"$user_id\\":\\"abc123\\",\\"mp_country_code\\":\\"United States\\",\\"mp_lib\\":\\"Segment Actions: analytics.js\\",\\"order_id\\":\\"order-id-123\\",\\"checkout_id\\":\\"checkout-id-123\\",\\"product_id\\":\\"505bd76785ebb509fc183733\\",\\"sku\\":\\"46493-32\\",\\"category\\":\\"Games\\",\\"name\\":\\"Uno Card Game\\",\\"price\\":3,\\"position\\":2}}]"` + `"[{\\"event\\":\\"Order Completed\\",\\"properties\\":{\\"time\\":1629213675449,\\"ip\\":\\"8.8.8.8\\",\\"id\\":\\"abc123\\",\\"$anon_id\\":\\"anon-2134\\",\\"distinct_id\\":\\"abc123\\",\\"$browser\\":\\"Safari\\",\\"$browser_version\\":\\"9.0\\",\\"$current_url\\":\\"https://segment.com/academy/\\",\\"$identified_id\\":\\"abc123\\",\\"$insert_id\\":\\"112c2a3c-7242-4327-9090-48a89de6a4110\\",\\"$lib_version\\":\\"2.11.1\\",\\"$locale\\":\\"en-US\\",\\"$source\\":\\"segment\\",\\"$user_id\\":\\"abc123\\",\\"mp_country_code\\":\\"United States\\",\\"mp_lib\\":\\"Segment Actions: analytics.js\\",\\"affiliation\\":\\"Super Online Store\\",\\"order_id\\":\\"order-id-123\\",\\"checkout_id\\":\\"checkout-id-123\\",\\"coupon\\":\\"Mixpanel Day\\",\\"currency\\":\\"USD\\",\\"products\\":[{\\"product_id\\":\\"507f1f77bcf86cd799439011\\",\\"sku\\":\\"45790-32\\",\\"name\\":\\"Monopoly: 3rd Edition\\",\\"price\\":19,\\"position\\":1,\\"quantity\\":2,\\"coupon\\":\\"MOUNTAIN\\",\\"brand\\":\\"Unknown\\",\\"category\\":\\"Games\\",\\"variant\\":\\"Black\\",\\"url\\":\\"https://www.example.com/product/path\\",\\"image_url\\":\\"https://www.example.com/product/path.jpg\\"},{\\"product_id\\":\\"505bd76785ebb509fc183733\\",\\"sku\\":\\"46493-32\\",\\"name\\":\\"Uno Card Game\\",\\"price\\":3,\\"position\\":2,\\"category\\":\\"Games\\",\\"custom\\":\\"xyz\\"}],\\"revenue\\":5.99,\\"shipping\\":1.5,\\"tax\\":3,\\"total\\":24.48}},{\\"event\\":\\"Product Purchased\\",\\"properties\\":{\\"time\\":1629213675448,\\"ip\\":\\"8.8.8.8\\",\\"id\\":\\"abc123\\",\\"$anon_id\\":\\"anon-2134\\",\\"distinct_id\\":\\"abc123\\",\\"$browser\\":\\"Safari\\",\\"$browser_version\\":\\"9.0\\",\\"$current_url\\":\\"https://segment.com/academy/\\",\\"$identified_id\\":\\"abc123\\",\\"$insert_id\\":\\"0112c2a3c-7242-4327-9090-48a89de6a4110\\",\\"$lib_version\\":\\"2.11.1\\",\\"$locale\\":\\"en-US\\",\\"$source\\":\\"segment\\",\\"$user_id\\":\\"abc123\\",\\"mp_country_code\\":\\"United States\\",\\"mp_lib\\":\\"Segment Actions: analytics.js\\",\\"order_id\\":\\"order-id-123\\",\\"checkout_id\\":\\"checkout-id-123\\",\\"product_id\\":\\"507f1f77bcf86cd799439011\\",\\"sku\\":\\"45790-32\\",\\"category\\":\\"Games\\",\\"name\\":\\"Monopoly: 3rd Edition\\",\\"brand\\":\\"Unknown\\",\\"variant\\":\\"Black\\",\\"price\\":19,\\"quantity\\":2,\\"coupon\\":\\"MOUNTAIN\\",\\"position\\":1,\\"url\\":\\"https://www.example.com/product/path\\",\\"image_url\\":\\"https://www.example.com/product/path.jpg\\"}},{\\"event\\":\\"Product Purchased\\",\\"properties\\":{\\"time\\":1629213675447,\\"ip\\":\\"8.8.8.8\\",\\"id\\":\\"abc123\\",\\"$anon_id\\":\\"anon-2134\\",\\"distinct_id\\":\\"abc123\\",\\"$browser\\":\\"Safari\\",\\"$browser_version\\":\\"9.0\\",\\"$current_url\\":\\"https://segment.com/academy/\\",\\"$identified_id\\":\\"abc123\\",\\"$insert_id\\":\\"1112c2a3c-7242-4327-9090-48a89de6a4110\\",\\"$lib_version\\":\\"2.11.1\\",\\"$locale\\":\\"en-US\\",\\"$source\\":\\"segment\\",\\"$user_id\\":\\"abc123\\",\\"mp_country_code\\":\\"United States\\",\\"mp_lib\\":\\"Segment Actions: analytics.js\\",\\"order_id\\":\\"order-id-123\\",\\"checkout_id\\":\\"checkout-id-123\\",\\"product_id\\":\\"505bd76785ebb509fc183733\\",\\"sku\\":\\"46493-32\\",\\"category\\":\\"Games\\",\\"name\\":\\"Uno Card Game\\",\\"price\\":3,\\"position\\":2}}]"` ) }) }) From c8c10ad5e6e49755add693e9c6a31faad583baa6 Mon Sep 17 00:00:00 2001 From: Noah Cooper Date: Tue, 21 Feb 2023 22:59:04 -0500 Subject: [PATCH 04/10] Merge branch 'segmentio-main' --- package.json | 2 +- .../src/destinations/heap/trackEvent/index.ts | 29 +----------------- .../src/destinations/saleswings/converter.ts | 30 ------------------- 3 files changed, 2 insertions(+), 59 deletions(-) delete mode 100644 packages/destination-actions/src/destinations/saleswings/converter.ts diff --git a/package.json b/package.json index 7ac0838a75..b2a2191712 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "types": "./bin/run generate:types", "validate": "./bin/run validate", "lint": "eslint '**/*.ts' --cache", - "subscriptions": "NODE_OPTIONS=--openssl-legacy-provider yarn workspace @segment/destination-subscriptions", + "subscriptions": "yarn workspace @segment/destination-subscriptions", "test": "lerna run test --stream", "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", diff --git a/packages/destination-actions/src/destinations/heap/trackEvent/index.ts b/packages/destination-actions/src/destinations/heap/trackEvent/index.ts index 3acca0ffa2..812440340b 100644 --- a/packages/destination-actions/src/destinations/heap/trackEvent/index.ts +++ b/packages/destination-actions/src/destinations/heap/trackEvent/index.ts @@ -122,11 +122,7 @@ const action: ActionDefinition = { event.user_identifier = getUserIdentifier({ identity: payload.identity, anonymous_id: payload.anonymous_id }) if (payload.timestamp && dayjs.utc(payload.timestamp).isValid()) { - heapPayload.timestamp = dayjs.utc(payload.timestamp).toISOString() - } - - if (payload.session_id) { - heapPayload.session_id = payload.session_id + event.timestamp = dayjs.utc(payload.timestamp).toISOString() } const payLoad: IntegrationsTrackPayload = { @@ -142,27 +138,4 @@ const action: ActionDefinition = { } } -const getEventName = (payload: Payload) => { - let eventName: string | undefined - switch (payload.type) { - case 'track': - eventName = payload.event - break - case 'page': - eventName = payload.name ? payload.name : 'Page Viewed' - break - case 'screen': - eventName = payload.name ? payload.name : 'Screen Viewed' - break - default: - eventName = 'track' - break - } - - if (!eventName) { - return 'track' - } - return eventName -} - export default action diff --git a/packages/destination-actions/src/destinations/saleswings/converter.ts b/packages/destination-actions/src/destinations/saleswings/converter.ts deleted file mode 100644 index 9c9c95e930..0000000000 --- a/packages/destination-actions/src/destinations/saleswings/converter.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { LeadRef, ValueMap } from './api' - -type EventPayload = { - userId?: string - anonymousId?: string - email?: string -} - -export const convertLeadRefs = (payload: EventPayload): LeadRef[] => { - const refs: LeadRef[] = [] - if (payload.userId) refs.push({ type: 'client-id', value: payload.userId }) - if (payload.anonymousId) refs.push({ type: 'client-id', value: payload.anonymousId }) - if (payload.email) refs.push({ type: 'email', value: payload.email }) - return refs -} - -export const convertValues = (source: { [k: string]: unknown } | undefined): ValueMap => { - const values: ValueMap = {} - if (!source) return values - Object.entries(source).forEach(([key, prop]) => { - if (typeof prop === 'number' || typeof prop === 'boolean' || typeof prop === 'string') values[key] = prop - }) - return values -} - -export const convertTimestamp = (timestamp: any): number => { - if (!timestamp) return Date.now() - if (typeof timestamp === 'number') return timestamp - return Date.parse(timestamp) -} From e50e1f9f3f6b7f154e6d9bf66189a819e9b86bd8 Mon Sep 17 00:00:00 2001 From: Noah Cooper Date: Tue, 7 Mar 2023 03:13:24 -0500 Subject: [PATCH 05/10] Merge remote-tracking branch 'upstream/main' into blackbaud-raisers-edge-nxt --- docs/testing.md | 167 ++- package.json | 2 +- packages/browser-destinations/package.json | 2 +- .../src/destinations/commandbar/index.ts | 5 + .../setConfigurationFields/index.ts | 3 +- .../src/destinations/index.ts | 3 +- .../playerzero-web/__tests__/index.test.ts | 2 +- .../identifyUser/__tests__/index.test.ts | 2 +- .../ripe/group/__tests__/index.test.ts | 22 +- .../ripe/group/generated-types.ts | 8 +- .../src/destinations/ripe/group/index.ts | 18 +- .../ripe/identify/__tests__/index.test.ts | 21 +- .../ripe/identify/generated-types.ts | 8 +- .../src/destinations/ripe/identify/index.ts | 19 +- .../src/destinations/ripe/init-script.ts | 2 +- .../ripe/page/__tests__/index.test.ts | 24 +- .../destinations/ripe/page/generated-types.ts | 8 +- .../src/destinations/ripe/page/index.ts | 20 +- .../ripe/track/__tests__/index.test.ts | 23 +- .../ripe/track/generated-types.ts | 8 +- .../src/destinations/ripe/track/index.ts | 19 +- .../src/destinations/ripe/types.ts | 63 +- .../upollo/__tests__/index.test.ts | 24 + .../destinations/upollo/generated-types.ts | 8 + .../identifyUser/__tests__/index.test.ts | 44 + .../upollo/identifyUser/generated-types.ts | 30 + .../destinations/upollo/identifyUser/index.ts | 91 ++ .../src/destinations/upollo/index.ts | 61 ++ .../src/destinations/upollo/types.ts | 17 + .../wisepops/__tests__/index.test.ts | 12 +- .../src/destinations/wisepops/index.ts | 6 +- packages/cli-internal/package.json | 6 +- packages/cli/package.json | 8 +- packages/destination-actions/package.json | 2 +- .../__snapshots__/snapshot.test.ts.snap | 72 +- .../__tests__/snapshot.test.ts | 4 +- .../blackbaud-raisers-edge-nxt/api/index.ts | 492 ++++++++- .../constants/index.ts | 4 +- .../__snapshots__/snapshot.test.ts.snap | 22 + .../__tests__/index.test.ts | 84 ++ .../__tests__/snapshot.test.ts | 77 ++ .../createConstituentAction/fixtures.ts | 31 + .../generated-types.ts | 139 +++ .../createConstituentAction/index.ts | 164 +++ .../__snapshots__/snapshot.test.ts.snap | 22 + .../createGift/__tests__/index.test.ts | 84 ++ .../createGift/__tests__/snapshot.test.ts | 79 ++ .../createGift/fixtures.ts | 65 ++ .../createGift/generated-types.ts | 170 +++ .../createGift/index.ts | 249 +++++ .../__snapshots__/snapshot.test.ts.snap | 34 +- .../__tests__/index.test.ts | 82 +- .../fixtures.ts | 2 +- .../generated-types.ts | 4 + .../index.ts | 980 ++++++------------ .../blackbaud-raisers-edge-nxt/index.ts | 4 + .../blackbaud-raisers-edge-nxt/types/index.ts | 100 +- .../blackbaud-raisers-edge-nxt/utils/index.ts | 250 ++++- .../addContactToList/__tests__/index.test.ts | 6 +- .../__tests__/snapshot.test.ts | 8 +- .../cordial/addContactToList/index.ts | 2 +- .../addProductToCart/__tests__/index.test.ts | 39 +- .../cordial/addProductToCart/index.ts | 6 +- .../destinations/cordial/cordial-client.ts | 8 +- .../__tests__/index.test.ts | 8 +- .../cordial/createContactactivity/index.ts | 2 +- .../destinations/cordial/identities-fields.ts | 8 +- .../src/destinations/cordial/index.ts | 2 +- .../__snapshots__/snapshot.test.ts.snap | 17 + .../mergeContacts/__tests__/index.test.ts | 19 +- .../cordial/mergeContacts/index.ts | 2 +- .../__tests__/index.test.ts | 6 +- .../__tests__/snapshot.test.ts | 8 +- .../cordial/removeContactFromList/index.ts | 2 +- .../__tests__/index.test.ts | 13 +- .../cordial/removeProductFromCart/index.ts | 4 +- .../upsertContact/__tests__/index.test.ts | 6 +- .../upsertOrder/__tests__/index.test.ts | 38 +- .../destinations/cordial/upsertOrder/index.ts | 23 +- .../__snapshots__/snapshot.test.ts.snap | 82 ++ .../emarsys/__tests__/index.test.ts | 36 + .../emarsys/__tests__/snapshot.test.ts | 77 ++ .../__snapshots__/snapshot.test.ts.snap | 19 + .../addToContactList/__tests__/index.test.ts | 98 ++ .../__tests__/snapshot.test.ts | 75 ++ .../addToContactList/generated-types.ts | 16 + .../emarsys/addToContactList/index.ts | 124 +++ .../destinations/emarsys/emarsys-helper.ts | 162 +++ .../destinations/emarsys/generated-types.ts | 12 + .../src/destinations/emarsys/index.ts | 61 ++ .../__snapshots__/snapshot.test.ts.snap | 21 + .../__tests__/index.test.ts | 97 ++ .../__tests__/snapshot.test.ts | 75 ++ .../removeFromContactList/generated-types.ts | 16 + .../emarsys/removeFromContactList/index.ts | 124 +++ .../__snapshots__/snapshot.test.ts.snap | 19 + .../triggerEvent/__tests__/index.test.ts | 97 ++ .../triggerEvent/__tests__/snapshot.test.ts | 75 ++ .../emarsys/triggerEvent/generated-types.ts | 22 + .../emarsys/triggerEvent/index.ts | 140 +++ .../__snapshots__/snapshot.test.ts.snap | 26 + .../upsertContact/__tests__/index.test.ts | 99 ++ .../upsertContact/__tests__/snapshot.test.ts | 75 ++ .../emarsys/upsertContact/generated-types.ts | 21 + .../emarsys/upsertContact/index.ts | 139 +++ .../__tests__/send-email.test.ts | 20 + .../sendEmail/generated-types.ts | 2 +- .../sendEmail/index.ts | 11 +- .../__tests__/send-sms.test.ts | 17 + .../__tests__/send-whatsapp.test.ts | 76 +- .../sendSms/generated-types.ts | 2 +- .../engage-messaging-twilio/sendSms/index.ts | 1 - .../sendSms/sms-sender.ts | 11 +- .../sendWhatsApp/generated-types.ts | 4 + .../sendWhatsApp/index.ts | 8 + .../sendWhatsApp/whatsapp-sender.ts | 10 +- .../utils/message-sender.ts | 4 +- .../__tests__/addToCart.test.ts | 12 +- .../__tests__/custom.test.ts | 16 +- .../__tests__/initiateCheckout.test.ts | 18 +- .../__tests__/pageView.test.ts | 22 +- .../__tests__/purchase.test.ts | 12 +- .../__tests__/search.test.ts | 12 +- .../__tests__/user-data.test.ts | 168 +-- .../__tests__/viewContent.test.ts | 12 +- .../addToCart/generated-types.ts | 8 + .../custom/generated-types.ts | 8 + .../fb-capi-user-data.ts | 19 +- .../initiateCheckout/generated-types.ts | 8 + .../initiateCheckout/index.ts | 1 + .../pageView/generated-types.ts | 8 + .../purchase/generated-types.ts | 8 + .../search/generated-types.ts | 8 + .../viewContent/generated-types.ts | 8 + .../__tests__/addPaymentInfo.test.ts | 188 +++- .../__tests__/addToCart.test.ts | 136 ++- .../__tests__/addToWishlist.test.ts | 139 ++- .../__tests__/beginCheckout.test.ts | 149 ++- .../__tests__/customEvent.test.ts | 111 +- .../__tests__/generateLead.test.ts | 122 ++- .../__tests__/index.test.ts | 24 + .../__tests__/login.test.ts | 110 +- .../__tests__/pageView.test.ts | 129 ++- .../__tests__/purchase.test.ts | 184 +++- .../__tests__/refund.test.ts | 138 ++- .../__tests__/removeFromCart.test.ts | 135 ++- .../__tests__/search.test.ts | 121 ++- .../__tests__/selectItem.test.ts | 128 ++- .../__tests__/selectPromotion.test.ts | 144 ++- .../__tests__/signUp.test.ts | 106 +- .../__tests__/viewCart.test.ts | 158 ++- .../__tests__/viewItem.test.ts | 125 ++- .../__tests__/viewItemList.test.ts | 209 +++- .../__tests__/viewPromotion.test.ts | 166 ++- .../addPaymentInfo/generated-types.ts | 12 +- .../addPaymentInfo/index.ts | 35 +- .../addToCart/generated-types.ts | 12 +- .../google-analytics-4/addToCart/index.ts | 35 +- .../addToWishlist/generated-types.ts | 12 +- .../google-analytics-4/addToWishlist/index.ts | 35 +- .../beginCheckout/generated-types.ts | 12 +- .../google-analytics-4/beginCheckout/index.ts | 35 +- .../customEvent/generated-types.ts | 12 +- .../google-analytics-4/customEvent/index.ts | 33 +- .../google-analytics-4/ga4-functions.ts | 44 + .../google-analytics-4/ga4-properties.ts | 22 +- .../google-analytics-4/ga4-types.ts | 11 + .../generateLead/generated-types.ts | 12 +- .../google-analytics-4/generateLead/index.ts | 34 +- .../google-analytics-4/generated-types.ts | 8 +- .../destinations/google-analytics-4/index.ts | 32 +- .../login/generated-types.ts | 12 +- .../google-analytics-4/login/index.ts | 33 +- .../pageView/generated-types.ts | 12 +- .../google-analytics-4/pageView/index.ts | 33 +- .../purchase/generated-types.ts | 12 +- .../google-analytics-4/purchase/index.ts | 35 +- .../refund/generated-types.ts | 12 +- .../google-analytics-4/refund/index.ts | 35 +- .../removeFromCart/generated-types.ts | 12 +- .../removeFromCart/index.ts | 35 +- .../search/generated-types.ts | 12 +- .../google-analytics-4/search/index.ts | 33 +- .../selectItem/generated-types.ts | 12 +- .../google-analytics-4/selectItem/index.ts | 35 +- .../selectPromotion/generated-types.ts | 12 +- .../selectPromotion/index.ts | 35 +- .../signUp/generated-types.ts | 12 +- .../google-analytics-4/signUp/index.ts | 33 +- .../viewCart/generated-types.ts | 12 +- .../google-analytics-4/viewCart/index.ts | 35 +- .../viewItem/generated-types.ts | 12 +- .../google-analytics-4/viewItem/index.ts | 35 +- .../viewItemList/generated-types.ts | 12 +- .../google-analytics-4/viewItemList/index.ts | 36 +- .../viewPromotion/generated-types.ts | 12 +- .../google-analytics-4/viewPromotion/index.ts | 35 +- .../src/destinations/index.ts | 3 + .../ironclad/recordAction.types.ts | 32 + .../mixpanel/mixpanel-properties.ts | 47 +- .../trackEvent/__tests__/index.test.ts | 1 + .../mixpanel/trackEvent/functions.ts | 114 +- .../mixpanel/trackEvent/generated-types.ts | 6 - .../trackPurchase/__tests__/index.test.ts | 7 +- .../mixpanel/trackPurchase/generated-types.ts | 6 - .../__snapshots__/snapshot.test.ts.snap | 84 ++ .../outfunnel/__tests__/index.test.ts | 24 + .../outfunnel/__tests__/snapshot.test.ts | 77 ++ .../forwardGroupEvent/__tests__/index.test.ts | 32 + .../forwardGroupEvent/generated-types.ts | 40 + .../outfunnel/forwardGroupEvent/index.ts | 87 ++ .../__tests__/index.test.ts | 32 + .../forwardIdentifyEvent/generated-types.ts | 36 + .../outfunnel/forwardIdentifyEvent/index.ts | 80 ++ .../forwardTrackEvent/__tests__/index.test.ts | 32 + .../forwardTrackEvent/generated-types.ts | 44 + .../outfunnel/forwardTrackEvent/index.ts | 100 ++ .../destinations/outfunnel/generated-types.ts | 12 + .../src/destinations/outfunnel/index.ts | 56 + .../src/destinations/outfunnel/presets.ts | 27 + .../src/destinations/outfunnel/utils.ts | 3 + .../qualtrics/qualtricsApiClient.ts | 4 +- .../src/destinations/ripe/group/index.ts | 1 - .../src/destinations/ripe/identify/index.ts | 1 - .../src/destinations/ripe/page/index.ts | 1 - .../src/destinations/ripe/track/index.ts | 1 - .../segment-profiles/segment-properties.ts | 2 +- .../sendGroup/generated-types.ts | 2 +- .../sendIdentify/generated-types.ts | 2 +- .../src/destinations/sendgrid/index.ts | 2 +- .../src/destinations/twilio/sendSMS.types.ts | 4 + .../src/destinations/voucherify/index.ts | 2 +- 232 files changed, 9634 insertions(+), 1613 deletions(-) create mode 100644 packages/browser-destinations/src/destinations/upollo/__tests__/index.test.ts create mode 100644 packages/browser-destinations/src/destinations/upollo/generated-types.ts create mode 100644 packages/browser-destinations/src/destinations/upollo/identifyUser/__tests__/index.test.ts create mode 100644 packages/browser-destinations/src/destinations/upollo/identifyUser/generated-types.ts create mode 100644 packages/browser-destinations/src/destinations/upollo/identifyUser/index.ts create mode 100644 packages/browser-destinations/src/destinations/upollo/index.ts create mode 100644 packages/browser-destinations/src/destinations/upollo/types.ts create mode 100644 packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createConstituentAction/__tests__/__snapshots__/snapshot.test.ts.snap create mode 100644 packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createConstituentAction/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createConstituentAction/__tests__/snapshot.test.ts create mode 100644 packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createConstituentAction/fixtures.ts create mode 100644 packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createConstituentAction/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createConstituentAction/index.ts create mode 100644 packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createGift/__tests__/__snapshots__/snapshot.test.ts.snap create mode 100644 packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createGift/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createGift/__tests__/snapshot.test.ts create mode 100644 packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createGift/fixtures.ts create mode 100644 packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createGift/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/blackbaud-raisers-edge-nxt/createGift/index.ts create mode 100644 packages/destination-actions/src/destinations/cordial/mergeContacts/__tests__/__snapshots__/snapshot.test.ts.snap create mode 100644 packages/destination-actions/src/destinations/emarsys/__tests__/__snapshots__/snapshot.test.ts.snap create mode 100644 packages/destination-actions/src/destinations/emarsys/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/emarsys/__tests__/snapshot.test.ts create mode 100644 packages/destination-actions/src/destinations/emarsys/addToContactList/__tests__/__snapshots__/snapshot.test.ts.snap create mode 100644 packages/destination-actions/src/destinations/emarsys/addToContactList/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/emarsys/addToContactList/__tests__/snapshot.test.ts create mode 100644 packages/destination-actions/src/destinations/emarsys/addToContactList/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/emarsys/addToContactList/index.ts create mode 100644 packages/destination-actions/src/destinations/emarsys/emarsys-helper.ts create mode 100644 packages/destination-actions/src/destinations/emarsys/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/emarsys/index.ts create mode 100644 packages/destination-actions/src/destinations/emarsys/removeFromContactList/__tests__/__snapshots__/snapshot.test.ts.snap create mode 100644 packages/destination-actions/src/destinations/emarsys/removeFromContactList/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/emarsys/removeFromContactList/__tests__/snapshot.test.ts create mode 100644 packages/destination-actions/src/destinations/emarsys/removeFromContactList/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/emarsys/removeFromContactList/index.ts create mode 100644 packages/destination-actions/src/destinations/emarsys/triggerEvent/__tests__/__snapshots__/snapshot.test.ts.snap create mode 100644 packages/destination-actions/src/destinations/emarsys/triggerEvent/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/emarsys/triggerEvent/__tests__/snapshot.test.ts create mode 100644 packages/destination-actions/src/destinations/emarsys/triggerEvent/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/emarsys/triggerEvent/index.ts create mode 100644 packages/destination-actions/src/destinations/emarsys/upsertContact/__tests__/__snapshots__/snapshot.test.ts.snap create mode 100644 packages/destination-actions/src/destinations/emarsys/upsertContact/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/emarsys/upsertContact/__tests__/snapshot.test.ts create mode 100644 packages/destination-actions/src/destinations/emarsys/upsertContact/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/emarsys/upsertContact/index.ts create mode 100644 packages/destination-actions/src/destinations/google-analytics-4/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/ironclad/recordAction.types.ts create mode 100644 packages/destination-actions/src/destinations/outfunnel/__tests__/__snapshots__/snapshot.test.ts.snap create mode 100644 packages/destination-actions/src/destinations/outfunnel/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/outfunnel/__tests__/snapshot.test.ts create mode 100644 packages/destination-actions/src/destinations/outfunnel/forwardGroupEvent/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/outfunnel/forwardGroupEvent/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/outfunnel/forwardGroupEvent/index.ts create mode 100644 packages/destination-actions/src/destinations/outfunnel/forwardIdentifyEvent/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/outfunnel/forwardIdentifyEvent/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/outfunnel/forwardIdentifyEvent/index.ts create mode 100644 packages/destination-actions/src/destinations/outfunnel/forwardTrackEvent/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/outfunnel/forwardTrackEvent/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/outfunnel/forwardTrackEvent/index.ts create mode 100644 packages/destination-actions/src/destinations/outfunnel/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/outfunnel/index.ts create mode 100644 packages/destination-actions/src/destinations/outfunnel/presets.ts create mode 100644 packages/destination-actions/src/destinations/outfunnel/utils.ts diff --git a/docs/testing.md b/docs/testing.md index b4709ab647..6fa63d0ea9 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -1,10 +1,13 @@ # Build & Test Cloud Destinations - [Build & Test Cloud Destinations](#build--test-cloud-destinations) - - [Actions Tester](#actions-tester) - - [Local End-to-end Testing](#local-end-to-end-testing) - - [Example](#example) - - [Testing Batches](#testing-batches) + - [Approaches to local testing](#approaches-to-local-testing) + - [Local testing with the Actions Tester](#local-testing-with-the-actions-tester) + - [Local testing with cURL or Postman](#local-testing-with-curl-or-postman) + - [Testing an Action's perform() or performBatch() function](#testing-an-actions-perform-or-performbatch-function) + - [Example request to invoke perform()](#example-request-to-invoke-perform) + - [Example request to invoke performBatch()](#example-request-to-invoke-performbatch) + - [Example request to invoke testAuthentication()](#example-request-to-invoke-testauthentication) - [Unit Testing](#unit-testing) - [Mocking HTTP Requests](#mocking-http-requests) - [Examples](#examples) @@ -12,69 +15,153 @@ - [Code Coverage](#code-coverage) - [Post Deployment Change Testing](#post-deployment-change-testing) -## Actions Tester +## Approaches to local testing + +There are 2 ways in which you can do local testing. Both require running a local server - which is explained below. + +Approach 1: Use the [Actions Tester](#local-testing-with-the-actions-tester): The Actions Tester provides a UI which allows you to specify an inbound Segment API call payload - such as a track() or identify() payload - and route that payload to an Action's perform() function in your Destination. The Destination's Settings Fields and Action Fields are rendered in the UI and provide an approximation of what the customer will see when configuring the Destination. The perform() function will then send data to your Platform. + +Approach 2: [Local testing with cURL or Postman](#local-testing-with-curl-or-postman): You can use an API testing tool such as cURL or Postman to test not only an Action's perform() function, but also the performBatch(), delete() and testAuthentication() functions. With this approach there is no UI so you'll need to provide not only the payload from an inbound Segment API call - such as a track() or identify() payload, but potentially other objects such as settings or mapping objects in your HTTP request. + +Note: there is no 'Staging' platform available, so the testing you'll be doing will all be local until your Integration has been deployed to Segment's Production environment. When your Integration has been deployed to Production it will initially be set to 'Private Beta' mode and will not be findable by customers in the Catalog. However you will be able to set up and configure your Integration via a URL so that you can do further testing. + +## Local testing with the Actions Tester In order to see a visual representation of the settings/mappings fields we provide a tool to preview and execute simulated actions mappings against your in development destination. For more information on how to use actions tester [click here](./actions_tester.md). -## Local End-to-end Testing +## Local testing with cURL or Postman -To test a destination action locally, you can spin up a local HTTP server through the Actions CLI. +To test a destination action locally you can spin up a local HTTP server through the Actions CLI. ```sh # For more information, add the --help flag ./bin/run serve ``` -The default port is set to `3000`. To use a different port, you can specify the `PORT` environment variable (e.g. `PORT=3001 ./bin/run serve`). +The default port is set to `3000`. To use a different port, you can specify the `PORT` environment variable (e.g. `PORT=3001 ./bin/run serve`). The examples in this documenation will assume `PORT` is set to `3000`. -After running the `serve` command, select the destination you want to test locally. Once a destination is selected, the server should start up. +After running the `serve` command, select the destination you want to test locally. Once a destination is selected the server should start up. -To test a specific destination action, 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. +### Testing an Action's perform() or performBatch() function -### Example +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. -The following is an example of a cURL command for `google-analytics-4`'s `search` action. Note that `payload`, `settings`, `auth`, and `features` values are all optional in the request body. However, you must still pass in all required fields for the specific destination action under `payload`. `features` is for internal Twilio/Segment use only. +For example if you wanted to test the the Emarsys Destination's upsertContact Action the URL you would POST to would be: `http://localhost:3000/upsertContact`. + +### Example request to invoke perform() + +The following is an example of a cURL command which will invoke the Emarsys Destination's `upsertContact` Action (Emarsys is an email tool popular with some Segment customers). Data for 3 objects is included in the data/body: `payload`, `mapping` and `settings`. + +#### Example oauth object + +Emarsys doesn't use OAuth so you can leave out the `auth` object; however you if your Integration is using OAuth then you'll need to include an `oauth` object in the HTTP request. ```sh -curl --location --request POST 'http://localhost:3000/search' \ + "oauth": { + "access_token": "" + } +``` + +`payload` - this should contain the payload coming in to your Integration. It could be a track() or identify() or other payload. In the example below only the fields needed by the Emarsys upsertContact Action are included. +`mapping` - this should include mappings to be applied to the payload in order to extract out the field data you want to pass to the perform() function. +`settings` - this should include the Settings fields data to be passed to the perform() function. In the case of Emarsys there are only 2 Settings fields, the api_user and api_password fields. + +```sh +curl --location --request POST 'http://localhost:3000/upsertContact' \ --header 'Content-Type: application/json' \ --data '{ - "payload": { - "client_id": "", - "search_term": "" - }, - "settings": { - "measurementId": "", - "apiSecret": "" - }, - "auth": { - "accessToken": "", - "refreshToken": "" - } - "features": { - "test_feature": true, + "mapping": { + "key_field": { "@path": "$.properties.key_field" }, + "key_value": { "@path": "$.properties.key_value" }, + "write_field": { "@path": "$.traits" } + }, + "settings": { + "api_user": "", + "api_password": "" + }, + "payload": + { + "properties": { + "key_field": "3", + "key_value": "tester@emarsys.com" + }, + "traits": { + "1": "Hans", + "2": "Müller" + } } }' ``` -### Testing Batches +### Example request to invoke performBatch() + +Invoking an Action's performBatch() is nearly identical to [invoking the perform()](#example-request-to-invoke-perform) function except that the`payload` object will be an array of events rather than a single event object. In the example below a batch of 2 events is sent to the Emarsys performBatch() function. + +```sh +curl --location --request POST 'http://localhost:3000/upsertContact' \ +--header 'Content-Type: application/json' \ +--data '{ + "mapping": { + "key_field": { "@path": "$.properties.key_field" }, + "key_value": { "@path": "$.properties.key_value" }, + "write_field": { "@path": "$.traits" } + }, + "settings": { + "api_user": "", + "api_password": "" + }, + "payload":[ + { + "properties": { + "key_field": "3", + "key_value": "tester@emarsys.com" + }, + "traits": { + "1": "Hans", + "2": "Müller" + } + }, + { + "properties": { + "key_field": "3", + "key_value": "another_tester@emarsys.com" + }, + "traits": { + "1": "James", + "2": "Mills" + } + }] +}' +``` + +### Example request to invoke testAuthentication() + +The testAuthentication() function is a function which is called in the Segment UI after a user provides Settings information in a Destination's Settings tab. The purpose of the testAuthentication() function is to verify that the authentication credentials provided by the customer are valid. + +If the credentials are invalid the testAuthentication() function should throw an Error which will be displayed to the customer in the Segment UI. + +The example below shows the HTTP POST request you would use to invoke the testAuthentication() function for the Emarsys Destination. This particular Destination doesn't use OAuth and instead has authentication fields in the `settings` object. The POST request should be sent to `http://localhost:3000/authentication`. + +```sh +curl --location --request POST 'http://localhost:3000/authentication' \ +--header 'Content-Type: application/json' \ +--data '{ + "settings": { + "api_user": "", + "api_password": "" + } +}' +``` -Actions destinations that support batching, i.e. that have a `performBatch` handler implemented, can also be tested locally. Test events should be formatted similarly to the example above, with the exception that `payload` will be an array. Here is an example of `webhook`'s `send` action, with a batch `payload`. +If your Integration instead uses OAuth you will need to pass in an `oauth` object in the HTTP request: ```sh -curl --location --request POST 'http://localhost:3000/send' \ +curl --location --request POST 'http://localhost:3000/authentication' \ --header 'Content-Type: application/json' \ --data '{ - "payload": [{ - "url": "https://www.example.com", - "method": "PUT", - "data": { - "cool": true - } - }], - "settings": {}, - "auth": {}, - "features": {} + "oauth": { + "access_token": "" + } }' ``` diff --git a/package.json b/package.json index b2a2191712..32c65c9e3a 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "test-partners": "lerna run test --stream --ignore @segment/actions-core --ignore @segment/actions-cli --ignore @segment/ajv-human-errors", "test-browser": "bash scripts/test-browser.sh", "typecheck": "lerna run typecheck --stream", - "alpha": "lerna publish --canary --preid $(git branch --show-current) --include-merged-tags", + "alpha": "lerna version prerelease --allow-branch $(git branch --show-current) --preid $(git branch --show-current) --no-push --no-git-tag-version", "prepare": "husky install", "clean": "sh scripts/clean.sh" }, diff --git a/packages/browser-destinations/package.json b/packages/browser-destinations/package.json index c0838c6bac..500ae1cd73 100644 --- a/packages/browser-destinations/package.json +++ b/packages/browser-destinations/package.json @@ -1,6 +1,6 @@ { "name": "@segment/browser-destinations", - "version": "3.75.0", + "version": "3.77.0", "description": "Action based browser destinations", "author": "Netto Farah", "license": "MIT", diff --git a/packages/browser-destinations/src/destinations/commandbar/index.ts b/packages/browser-destinations/src/destinations/commandbar/index.ts index a17e6814e9..43da498cec 100644 --- a/packages/browser-destinations/src/destinations/commandbar/index.ts +++ b/packages/browser-destinations/src/destinations/commandbar/index.ts @@ -58,6 +58,11 @@ export const destination: BrowserDestinationDefinition Object.prototype.hasOwnProperty.call(window, 'CommandBar'), 100) + // Older CommandBar snippets initialize a proxy that traps all field access including `then` + // `then` is called implicitly on the return value of an async function on await + // So, we need to remove that behavior for the promise to resolve + Object.assign(window.CommandBar, { then: undefined }) + return window.CommandBar }, diff --git a/packages/browser-destinations/src/destinations/google-analytics-4-web/setConfigurationFields/index.ts b/packages/browser-destinations/src/destinations/google-analytics-4-web/setConfigurationFields/index.ts index b00845ea08..c87d3e8ef5 100644 --- a/packages/browser-destinations/src/destinations/google-analytics-4-web/setConfigurationFields/index.ts +++ b/packages/browser-destinations/src/destinations/google-analytics-4-web/setConfigurationFields/index.ts @@ -93,6 +93,7 @@ const action: BrowserActionDefinition = { } }, perform: (gtag, { payload, settings }) => { + updateUser(payload.user_id, payload.user_properties, gtag) if (settings.enableConsentMode) { window.gtag('consent', 'update', { ad_storage: payload.ads_storage_consent_state, @@ -135,7 +136,7 @@ const action: BrowserActionDefinition = { if (payload.campaign_content) { gtag('set', { campaign_content: payload.campaign_content }) } - updateUser(payload.user_id, payload.user_properties, gtag) + gtag('event', 'page_view') } } diff --git a/packages/browser-destinations/src/destinations/index.ts b/packages/browser-destinations/src/destinations/index.ts index f0fc861ecc..efa6336d03 100644 --- a/packages/browser-destinations/src/destinations/index.ts +++ b/packages/browser-destinations/src/destinations/index.ts @@ -47,4 +47,5 @@ register('6372e1e36d9c2181f3900834', './wisepops') register('637c192eba61b944e08ee158', './vwo') register('638f843c4520d424f63c9e51', './commandbar') register('63913b2bf906ea939f153851', './ripe') -register('63b557a652cb6b4f28a4d118', './google-analytics-4-web') +register('63ed446fe60a1b56c5e6f130', './google-analytics-4-web') +register('640267d74c13708d74062dcd', './upollo') diff --git a/packages/browser-destinations/src/destinations/playerzero-web/__tests__/index.test.ts b/packages/browser-destinations/src/destinations/playerzero-web/__tests__/index.test.ts index cfd37c9575..bdd9b6cdb9 100644 --- a/packages/browser-destinations/src/destinations/playerzero-web/__tests__/index.test.ts +++ b/packages/browser-destinations/src/destinations/playerzero-web/__tests__/index.test.ts @@ -2,7 +2,7 @@ import { Analytics, Context } from '@segment/analytics-next' import playerzero, { destination } from '../index' import { subscriptions, TEST_PROJECT_ID } from '../test-utils' -test('load PlayerZero', async () => { +test.skip('load PlayerZero', async () => { const [event] = await playerzero({ projectId: TEST_PROJECT_ID, subscriptions diff --git a/packages/browser-destinations/src/destinations/playerzero-web/identifyUser/__tests__/index.test.ts b/packages/browser-destinations/src/destinations/playerzero-web/identifyUser/__tests__/index.test.ts index 8f2ba1ee4d..45529a46a3 100644 --- a/packages/browser-destinations/src/destinations/playerzero-web/identifyUser/__tests__/index.test.ts +++ b/packages/browser-destinations/src/destinations/playerzero-web/identifyUser/__tests__/index.test.ts @@ -2,7 +2,7 @@ import { Analytics, Context } from '@segment/analytics-next' import playerzero from '../../index' import { subscriptions, TEST_PROJECT_ID } from '../../test-utils' -describe('PlayerzeroWeb.identifyUser', () => { +describe.skip('PlayerzeroWeb.identifyUser', () => { it('should keep anonymous users', async () => { const [_, identifyUser] = await playerzero({ projectId: TEST_PROJECT_ID, diff --git a/packages/browser-destinations/src/destinations/ripe/group/__tests__/index.test.ts b/packages/browser-destinations/src/destinations/ripe/group/__tests__/index.test.ts index 830b7b4856..0abf4f2960 100644 --- a/packages/browser-destinations/src/destinations/ripe/group/__tests__/index.test.ts +++ b/packages/browser-destinations/src/destinations/ripe/group/__tests__/index.test.ts @@ -17,6 +17,15 @@ const subscriptions: Subscription[] = [ enabled: true, subscribe: 'type = "group"', mapping: { + messageId: { + '@path': '$.messageId' + }, + anonymousId: { + '@path': '$.anonymousId' + }, + userId: { + '@path': '$.userId' + }, groupId: { '@path': '$.groupId' }, @@ -46,6 +55,8 @@ describe('Ripe.group', () => { await event.group?.( new Context({ + messageId: 'ajs-71f386523ee5dfa90c7d0fda28b6b5c6', + anonymousId: 'anonId1', type: 'group', groupId: 'groupId1', traits: { @@ -58,6 +69,9 @@ describe('Ripe.group', () => { expect.anything(), expect.objectContaining({ payload: { + messageId: 'ajs-71f386523ee5dfa90c7d0fda28b6b5c6', + anonymousId: 'anonId1', + userId: undefined, groupId: 'groupId1', traits: { is_new_group: true @@ -66,6 +80,12 @@ describe('Ripe.group', () => { }) ) - expect(window.Ripe.group).toHaveBeenCalledWith('groupId1', expect.objectContaining({ is_new_group: true })) + expect(window.Ripe.group).toHaveBeenCalledWith({ + messageId: 'ajs-71f386523ee5dfa90c7d0fda28b6b5c6', + anonymousId: 'anonId1', + userId: undefined, + groupId: 'groupId1', + traits: expect.objectContaining({ is_new_group: true }) + }) }) }) diff --git a/packages/browser-destinations/src/destinations/ripe/group/generated-types.ts b/packages/browser-destinations/src/destinations/ripe/group/generated-types.ts index 2d62b12b15..fddde614a6 100644 --- a/packages/browser-destinations/src/destinations/ripe/group/generated-types.ts +++ b/packages/browser-destinations/src/destinations/ripe/group/generated-types.ts @@ -8,15 +8,19 @@ export interface Payload { /** * The ID associated with the user */ - userId?: string + userId?: string | null /** * The ID associated groupId */ - groupId: string + groupId: string | null /** * Traits to associate with the group */ traits?: { [k: string]: unknown } + /** + * The Segment messageId + */ + messageId?: string } diff --git a/packages/browser-destinations/src/destinations/ripe/group/index.ts b/packages/browser-destinations/src/destinations/ripe/group/index.ts index 3f814039e9..818babe45f 100644 --- a/packages/browser-destinations/src/destinations/ripe/group/index.ts +++ b/packages/browser-destinations/src/destinations/ripe/group/index.ts @@ -19,6 +19,7 @@ const action: BrowserActionDefinition = { userId: { type: 'string', required: false, + allowNull: true, description: 'The ID associated with the user', label: 'User ID', default: { '@path': '$.userId' } @@ -26,6 +27,7 @@ const action: BrowserActionDefinition = { groupId: { type: 'string', required: true, + allowNull: true, description: 'The ID associated groupId', label: 'Group ID', default: { '@path': '$.groupId' } @@ -36,11 +38,23 @@ const action: BrowserActionDefinition = { description: 'Traits to associate with the group', required: false, default: { '@path': '$.traits' } + }, + messageId: { + type: 'string', + required: false, + description: 'The Segment messageId', + label: 'MessageId', + default: { '@path': '$.messageId' } } }, perform: async (ripe, { payload }) => { - await ripe.setIds(payload.anonymousId, payload.userId, payload.groupId) - return ripe.group(payload.groupId, payload.traits) + return ripe.group({ + messageId: payload.messageId, + anonymousId: payload.anonymousId, + userId: payload.userId, + groupId: payload.groupId, + traits: payload.traits + }) } } diff --git a/packages/browser-destinations/src/destinations/ripe/identify/__tests__/index.test.ts b/packages/browser-destinations/src/destinations/ripe/identify/__tests__/index.test.ts index ba7225f353..d9410ec42e 100644 --- a/packages/browser-destinations/src/destinations/ripe/identify/__tests__/index.test.ts +++ b/packages/browser-destinations/src/destinations/ripe/identify/__tests__/index.test.ts @@ -17,12 +17,18 @@ const subscriptions: Subscription[] = [ enabled: true, subscribe: 'type = "identify"', mapping: { + messageId: { + '@path': '$.messageId' + }, anonymousId: { '@path': '$.anonymousId' }, userId: { '@path': '$.userId' }, + groupId: { + '@path': '$.groupId' + }, traits: { '@path': '$.traits' } @@ -50,6 +56,7 @@ describe('Ripe.identify', () => { await event.identify?.( new Context({ type: 'identify', + messageId: 'ajs-71f386523ee5dfa90c7d0fda28b6b5c6', anonymousId: 'anonymousId', userId: 'userId', traits: { @@ -62,8 +69,10 @@ describe('Ripe.identify', () => { expect.anything(), expect.objectContaining({ payload: { + messageId: 'ajs-71f386523ee5dfa90c7d0fda28b6b5c6', anonymousId: 'anonymousId', userId: 'userId', + groupId: undefined, traits: { name: 'Simon' } @@ -71,10 +80,12 @@ describe('Ripe.identify', () => { }) ) - expect(window.Ripe.identify).toHaveBeenCalledWith( - expect.stringMatching('anonymousId'), - expect.stringMatching('userId'), - expect.objectContaining({ name: 'Simon' }) - ) + expect(window.Ripe.identify).toHaveBeenCalledWith({ + messageId: 'ajs-71f386523ee5dfa90c7d0fda28b6b5c6', + userId: expect.stringMatching('userId'), + anonymousId: 'anonymousId', + groupId: undefined, + traits: expect.objectContaining({ name: 'Simon' }) + }) }) }) diff --git a/packages/browser-destinations/src/destinations/ripe/identify/generated-types.ts b/packages/browser-destinations/src/destinations/ripe/identify/generated-types.ts index b9ef09efdd..aafc8a5b93 100644 --- a/packages/browser-destinations/src/destinations/ripe/identify/generated-types.ts +++ b/packages/browser-destinations/src/destinations/ripe/identify/generated-types.ts @@ -8,15 +8,19 @@ export interface Payload { /** * The ID associated with the user */ - userId?: string + userId?: string | null /** * The ID associated groupId */ - groupId?: string + groupId?: string | null /** * Traits to associate with the user */ traits?: { [k: string]: unknown } + /** + * The Segment messageId + */ + messageId?: string } diff --git a/packages/browser-destinations/src/destinations/ripe/identify/index.ts b/packages/browser-destinations/src/destinations/ripe/identify/index.ts index 408f023566..c5efb0b2a2 100644 --- a/packages/browser-destinations/src/destinations/ripe/identify/index.ts +++ b/packages/browser-destinations/src/destinations/ripe/identify/index.ts @@ -19,6 +19,7 @@ const action: BrowserActionDefinition = { userId: { type: 'string', required: false, + allowNull: true, description: 'The ID associated with the user', label: 'User ID', default: { '@path': '$.userId' } @@ -26,6 +27,7 @@ const action: BrowserActionDefinition = { groupId: { type: 'string', required: false, + allowNull: true, description: 'The ID associated groupId', label: 'Group ID', default: { '@path': '$.context.groupId' } @@ -36,12 +38,23 @@ const action: BrowserActionDefinition = { description: 'Traits to associate with the user', required: false, default: { '@path': '$.traits' } + }, + messageId: { + type: 'string', + required: false, + description: 'The Segment messageId', + label: 'MessageId', + default: { '@path': '$.messageId' } } }, perform: async (ripe, { payload }) => { - console.log(JSON.stringify(payload, null, 2)) - await ripe.setIds(payload.anonymousId, payload.userId, payload.groupId) - return ripe.identify(payload.anonymousId, payload.userId, payload.traits) + return ripe.identify({ + messageId: payload.messageId, + anonymousId: payload.anonymousId, + userId: payload.userId, + groupId: payload.groupId, + traits: payload.traits + }) } } diff --git a/packages/browser-destinations/src/destinations/ripe/init-script.ts b/packages/browser-destinations/src/destinations/ripe/init-script.ts index 9d075d896a..bce5329816 100644 --- a/packages/browser-destinations/src/destinations/ripe/init-script.ts +++ b/packages/browser-destinations/src/destinations/ripe/init-script.ts @@ -6,7 +6,7 @@ export function initScript() { } window.Ripe = [] - ;['group', 'identify', 'init', 'page', 'setIds', 'track'].forEach(function (method) { + ;['group', 'identify', 'init', 'page', 'track'].forEach(function (method) { window.Ripe[method] = function () { let args = Array.from(arguments) args.unshift(method) diff --git a/packages/browser-destinations/src/destinations/ripe/page/__tests__/index.test.ts b/packages/browser-destinations/src/destinations/ripe/page/__tests__/index.test.ts index ad3bdbd756..370a66bbd8 100644 --- a/packages/browser-destinations/src/destinations/ripe/page/__tests__/index.test.ts +++ b/packages/browser-destinations/src/destinations/ripe/page/__tests__/index.test.ts @@ -17,9 +17,18 @@ const subscriptions: Subscription[] = [ enabled: true, subscribe: 'type = "page"', mapping: { + messageId: { + '@path': '$.messageId' + }, anonymousId: { '@path': '$.anonymousId' }, + userId: { + '@path': '$.userId' + }, + groupId: { + '@path': '$.groupId' + }, category: { '@path': '$.category' }, @@ -52,6 +61,7 @@ describe('Ripe.page', () => { await event.page?.( new Context({ + messageId: 'ajs-71f386523ee5dfa90c7d0fda28b6b5c6', anonymousId: 'anonymousId', type: 'page', category: 'main', @@ -66,7 +76,10 @@ describe('Ripe.page', () => { expect.anything(), expect.objectContaining({ payload: { + messageId: 'ajs-71f386523ee5dfa90c7d0fda28b6b5c6', anonymousId: 'anonymousId', + userId: undefined, + groupId: undefined, category: 'main', name: 'page2', properties: { @@ -76,7 +89,14 @@ describe('Ripe.page', () => { }) ) - expect(window.Ripe.page).toHaveBeenCalledWith('main', 'page2', expect.objectContaining({ previous: 'page1' })) - expect(window.Ripe.setIds).toHaveBeenCalledWith('anonymousId', undefined, undefined) + expect(window.Ripe.page).toHaveBeenCalledWith({ + messageId: 'ajs-71f386523ee5dfa90c7d0fda28b6b5c6', + userId: undefined, + groupId: undefined, + anonymousId: 'anonymousId', + category: 'main', + name: 'page2', + properties: expect.objectContaining({ previous: 'page1' }) + }) }) }) diff --git a/packages/browser-destinations/src/destinations/ripe/page/generated-types.ts b/packages/browser-destinations/src/destinations/ripe/page/generated-types.ts index 5fbda49ba9..88306f8cb7 100644 --- a/packages/browser-destinations/src/destinations/ripe/page/generated-types.ts +++ b/packages/browser-destinations/src/destinations/ripe/page/generated-types.ts @@ -8,11 +8,11 @@ export interface Payload { /** * The ID associated with the user */ - userId?: string + userId?: string | null /** * The ID associated groupId */ - groupId?: string + groupId?: string | null /** * The category of the page */ @@ -27,4 +27,8 @@ export interface Payload { properties?: { [k: string]: unknown } + /** + * The Segment messageId + */ + messageId?: string } diff --git a/packages/browser-destinations/src/destinations/ripe/page/index.ts b/packages/browser-destinations/src/destinations/ripe/page/index.ts index a13cb38359..d1e7bffcc1 100644 --- a/packages/browser-destinations/src/destinations/ripe/page/index.ts +++ b/packages/browser-destinations/src/destinations/ripe/page/index.ts @@ -19,6 +19,7 @@ const action: BrowserActionDefinition = { userId: { type: 'string', required: false, + allowNull: true, description: 'The ID associated with the user', label: 'User ID', default: { '@path': '$.userId' } @@ -26,6 +27,7 @@ const action: BrowserActionDefinition = { groupId: { type: 'string', required: false, + allowNull: true, description: 'The ID associated groupId', label: 'Group ID', default: { '@path': '$.context.groupId' } @@ -62,11 +64,25 @@ const action: BrowserActionDefinition = { description: 'Page properties', label: 'Properties', default: { '@path': '$.properties' } + }, + messageId: { + type: 'string', + required: false, + description: 'The Segment messageId', + label: 'MessageId', + default: { '@path': '$.messageId' } } }, perform: async (ripe, { payload }) => { - await ripe.setIds(payload.anonymousId, payload.userId, payload.groupId) - return ripe.page(payload.category, payload.name, payload.properties) + return ripe.page({ + messageId: payload.messageId, + anonymousId: payload.anonymousId, + userId: payload.userId, + groupId: payload.groupId, + category: payload.category, + name: payload.name, + properties: payload.properties + }) } } diff --git a/packages/browser-destinations/src/destinations/ripe/track/__tests__/index.test.ts b/packages/browser-destinations/src/destinations/ripe/track/__tests__/index.test.ts index 7452f1ebae..abf38f8405 100644 --- a/packages/browser-destinations/src/destinations/ripe/track/__tests__/index.test.ts +++ b/packages/browser-destinations/src/destinations/ripe/track/__tests__/index.test.ts @@ -17,9 +17,18 @@ const subscriptions: Subscription[] = [ enabled: true, subscribe: 'type = "track"', mapping: { + messageId: { + '@path': '$.messageId' + }, anonymousId: { '@path': '$.anonymousId' }, + userId: { + '@path': '$.userId' + }, + groupId: { + '@path': '$.groupId' + }, event: { '@path': '$.event' }, @@ -49,6 +58,7 @@ describe('Ripe.track', () => { await event.track?.( new Context({ + messageId: 'ajs-71f386523ee5dfa90c7d0fda28b6b5c6', type: 'track', anonymousId: 'anonymousId', event: 'Form Submitted', @@ -62,7 +72,10 @@ describe('Ripe.track', () => { expect.anything(), expect.objectContaining({ payload: { + messageId: 'ajs-71f386523ee5dfa90c7d0fda28b6b5c6', anonymousId: 'anonymousId', + userId: undefined, + groupId: undefined, event: 'Form Submitted', properties: { is_new_lead: true @@ -71,7 +84,13 @@ describe('Ripe.track', () => { }) ) - expect(window.Ripe.track).toHaveBeenCalledWith('Form Submitted', expect.objectContaining({ is_new_lead: true })) - expect(window.Ripe.setIds).toHaveBeenCalledWith('anonymousId', undefined, undefined) + expect(window.Ripe.track).toHaveBeenCalledWith({ + messageId: 'ajs-71f386523ee5dfa90c7d0fda28b6b5c6', + anonymousId: 'anonymousId', + userId: undefined, + groupId: undefined, + event: 'Form Submitted', + properties: expect.objectContaining({ is_new_lead: true }) + }) }) }) diff --git a/packages/browser-destinations/src/destinations/ripe/track/generated-types.ts b/packages/browser-destinations/src/destinations/ripe/track/generated-types.ts index 9f035e260f..8e79d8bb6f 100644 --- a/packages/browser-destinations/src/destinations/ripe/track/generated-types.ts +++ b/packages/browser-destinations/src/destinations/ripe/track/generated-types.ts @@ -8,11 +8,11 @@ export interface Payload { /** * The ID associated with the user */ - userId?: string + userId?: string | null /** * The ID associated groupId */ - groupId?: string + groupId?: string | null /** * The event name */ @@ -23,4 +23,8 @@ export interface Payload { properties?: { [k: string]: unknown } + /** + * The Segment messageId + */ + messageId?: string } diff --git a/packages/browser-destinations/src/destinations/ripe/track/index.ts b/packages/browser-destinations/src/destinations/ripe/track/index.ts index 5543963c24..5054822439 100644 --- a/packages/browser-destinations/src/destinations/ripe/track/index.ts +++ b/packages/browser-destinations/src/destinations/ripe/track/index.ts @@ -19,6 +19,7 @@ const action: BrowserActionDefinition = { userId: { type: 'string', required: false, + allowNull: true, description: 'The ID associated with the user', label: 'User ID', default: { '@path': '$.userId' } @@ -26,6 +27,7 @@ const action: BrowserActionDefinition = { groupId: { type: 'string', required: false, + allowNull: true, description: 'The ID associated groupId', label: 'Group ID', default: { '@path': '$.context.groupId' } @@ -43,12 +45,25 @@ const action: BrowserActionDefinition = { description: 'Properties to send with the event', label: 'Event properties', default: { '@path': '$.properties' } + }, + messageId: { + type: 'string', + required: false, + description: 'The Segment messageId', + label: 'MessageId', + default: { '@path': '$.messageId' } } }, perform: async (ripe, { payload }) => { - await ripe.setIds(payload.anonymousId, payload.userId, payload.groupId) if (payload?.event) { - return ripe.track(payload.event, payload.properties) + return ripe.track({ + messageId: payload.messageId, + anonymousId: payload.anonymousId, + userId: payload.userId, + groupId: payload.groupId, + event: payload.event, + properties: payload.properties + }) } } } diff --git a/packages/browser-destinations/src/destinations/ripe/types.ts b/packages/browser-destinations/src/destinations/ripe/types.ts index 9ba0298646..5638beecb5 100644 --- a/packages/browser-destinations/src/destinations/ripe/types.ts +++ b/packages/browser-destinations/src/destinations/ripe/types.ts @@ -1,8 +1,61 @@ export interface RipeSDK { - group: (groupId: string, traits?: Record) => Promise - identify: (anonymousId: string, userId?: string | undefined | null, traits?: Record) => Promise + group: ({ + anonymousId, + userId, + messageId, + groupId, + traits + }: { + messageId?: string + anonymousId: string + userId?: string | null + groupId: string | null + traits?: Record + }) => Promise + identify: ({ + messageId, + anonymousId, + userId, + groupId, + traits + }: { + messageId?: string + anonymousId: string + userId?: string | null + groupId?: string | null + traits?: Record + }) => Promise init: (apiKey: string) => Promise - page: (category?: string, name?: string, properties?: Record) => Promise - setIds: (anonymousId: string, userId?: string, groupId?: string) => Promise - track: (event: string, properties?: Record) => Promise + page: ({ + messageId, + anonymousId, + userId, + groupId, + category, + name, + properties + }: { + messageId?: string + anonymousId: string + userId?: string | null + groupId?: string | null + category?: string + name?: string + properties?: Record + }) => Promise + track: ({ + messageId, + anonymousId, + userId, + groupId, + event, + properties + }: { + messageId?: string + anonymousId: string + userId?: string | null + groupId?: string | null + event: string + properties?: Record + }) => Promise } diff --git a/packages/browser-destinations/src/destinations/upollo/__tests__/index.test.ts b/packages/browser-destinations/src/destinations/upollo/__tests__/index.test.ts new file mode 100644 index 0000000000..14324c57e0 --- /dev/null +++ b/packages/browser-destinations/src/destinations/upollo/__tests__/index.test.ts @@ -0,0 +1,24 @@ +import { Analytics, Context } from '@segment/analytics-next' +import plugin, { destination } from '..' + +it('should init', async () => { + const [event] = await plugin({ + apiKey: '123', + + subscriptions: [ + { + enabled: true, + name: 'Identify', + subscribe: 'type = "identify"', + partnerAction: 'identifyUser', + mapping: {} + } + ] + }) + + jest.spyOn(destination, 'initialize') + + await event.load(Context.system(), {} as Analytics) + expect(destination.initialize).toHaveBeenCalled() + expect(window).toHaveProperty('upollo') +}) diff --git a/packages/browser-destinations/src/destinations/upollo/generated-types.ts b/packages/browser-destinations/src/destinations/upollo/generated-types.ts new file mode 100644 index 0000000000..c1ea7060d0 --- /dev/null +++ b/packages/browser-destinations/src/destinations/upollo/generated-types.ts @@ -0,0 +1,8 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Settings { + /** + * The api key of your Upollo project. Get it from the Upollo [dashboard](https://upollo.ai/dashboard) + */ + apiKey: string +} diff --git a/packages/browser-destinations/src/destinations/upollo/identifyUser/__tests__/index.test.ts b/packages/browser-destinations/src/destinations/upollo/identifyUser/__tests__/index.test.ts new file mode 100644 index 0000000000..18a37e02ae --- /dev/null +++ b/packages/browser-destinations/src/destinations/upollo/identifyUser/__tests__/index.test.ts @@ -0,0 +1,44 @@ +import { Analytics, Context } from '@segment/analytics-next' + +import identify from '..' +import { UpolloClient } from '../../types' +import { Payload } from '../generated-types' + +it('should identify', async () => { + const client = { + track: jest.fn() + } as any as UpolloClient + + await identify.perform(client as any as UpolloClient, { + settings: { apiKey: '123' }, + analytics: jest.fn() as any as Analytics, + context: new Context({ + type: 'identify', + event: 'Signed Up' + }), + payload: { + user_id: 'u1', + email: 'foo@bar.com', + phone: '+611231234', + name: 'Mr Foo', + avatar_image_url: 'http://smile', + custom_traits: { + DOB: '1990-01-01', + Plan: 'Bronze', + session: { + // session is excluded because its not a string + count: 1 + } + } + } as Payload + }) + + expect(client.track).toHaveBeenCalledWith({ + userId: 'u1', + userEmail: 'foo@bar.com', + userPhone: '+611231234', + userName: 'Mr Foo', + userImage: 'http://smile', + customerSuppliedValues: { DOB: '1990-01-01', Plan: 'Bronze' } + }) +}) diff --git a/packages/browser-destinations/src/destinations/upollo/identifyUser/generated-types.ts b/packages/browser-destinations/src/destinations/upollo/identifyUser/generated-types.ts new file mode 100644 index 0000000000..dcc62f19e1 --- /dev/null +++ b/packages/browser-destinations/src/destinations/upollo/identifyUser/generated-types.ts @@ -0,0 +1,30 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * The ID of the user + */ + user_id?: string + /** + * The user's name. + */ + name?: string + /** + * The user's email address. + */ + email?: string + /** + * The user's phone number. + */ + phone?: string + /** + * The URL for the user's avatar/profile image. + */ + avatar_image_url?: string + /** + * The user's custom attributes. + */ + custom_traits?: { + [k: string]: unknown + } +} diff --git a/packages/browser-destinations/src/destinations/upollo/identifyUser/index.ts b/packages/browser-destinations/src/destinations/upollo/identifyUser/index.ts new file mode 100644 index 0000000000..914c80aa9c --- /dev/null +++ b/packages/browser-destinations/src/destinations/upollo/identifyUser/index.ts @@ -0,0 +1,91 @@ +import { BrowserActionDefinition } from 'src/lib/browser-destinations' +import { UpolloClient } from '../types' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' + +const identifyUser: BrowserActionDefinition = { + title: 'Identify user', + description: 'Identify the user', + defaultSubscription: 'type = "identify"', + platform: 'web', + fields: { + user_id: { + type: 'string', + required: false, + label: 'User ID', + description: 'The ID of the user ', + default: { + '@path': '$.userId' + } + }, + name: { + description: "The user's name.", + label: 'Name', + type: 'string', + required: false, + default: { + '@path': '$.traits.name' + } + }, + email: { + description: "The user's email address.", + label: 'Email Address', + type: 'string', + required: false, + default: { + '@path': '$.traits.email' + } + }, + phone: { + description: "The user's phone number.", + label: 'Phone Number', + type: 'string', + required: false, + default: { + '@path': '$.traits.phone' + } + }, + avatar_image_url: { + description: "The URL for the user's avatar/profile image.", + label: 'Avatar', + type: 'string', + required: false, + default: { '@path': '$.traits.avatar' } + }, + custom_traits: { + description: "The user's custom attributes.", + label: 'Custom Attributes', + type: 'object', + required: false, + defaultObjectUI: 'keyvalue' + } + }, + perform: (UpClient, { payload }) => { + const userInfo = { + userId: payload.user_id, + userEmail: payload.email, + userPhone: payload.phone, + userName: payload.name, + userImage: payload.avatar_image_url, + customerSuppliedValues: payload.custom_traits ? toCustomValues(payload.custom_traits) : undefined + } + + void UpClient.track(userInfo) + } +} + +export default identifyUser + +function toCustomValues(values: Record): Record { + const xs = Object.entries(values) + .map(([k, v]) => { + if (typeof v === 'string') { + return [k, v] + } else { + return [] + } + }) + .filter((xs) => xs.length === 2) + + return Object.fromEntries(xs) +} diff --git a/packages/browser-destinations/src/destinations/upollo/index.ts b/packages/browser-destinations/src/destinations/upollo/index.ts new file mode 100644 index 0000000000..73e00d6567 --- /dev/null +++ b/packages/browser-destinations/src/destinations/upollo/index.ts @@ -0,0 +1,61 @@ +import type { Settings } from './generated-types' +import type { BrowserDestinationDefinition } from '../../lib/browser-destinations' +import { browserDestination } from '../../runtime/shim' +import { UpolloClient } from './types' +import { defaultValues } from '@segment/actions-core' +import identifyUser from './identifyUser' + +declare global { + interface Window { + upollo: { + UpolloClient: typeof UpolloClient + } + } +} + +export const destination: BrowserDestinationDefinition = { + name: 'Upollo Web (Actions)', + slug: 'actions-upollo', + mode: 'device', + + presets: [ + { + name: 'Identify', + subscribe: 'type = "identify"', + partnerAction: 'identifyUser', + mapping: defaultValues(identifyUser.fields) + } + ], + + settings: { + // Add any Segment destination settings required here + apiKey: { + description: + 'The api key of your Upollo project. Get it from the Upollo [dashboard](https://upollo.ai/dashboard)', + label: 'API Key', + type: 'string', + required: true + } + }, + + initialize: async ({ settings }, deps) => { + try { + await deps.loadScript('https://cdn.upollo.ai/web/0.2/bundle.min.js') + + await deps.resolveWhen( + () => Object.prototype.hasOwnProperty.call(window, 'upollo'), + 500 // wait up to 500ms for the script to run. + ) + + return new window.upollo.UpolloClient(settings.apiKey) + } catch (err) { + throw new Error('Could not load the Upollo client ' + err) + } + }, + + actions: { + identifyUser + } +} + +export default browserDestination(destination) diff --git a/packages/browser-destinations/src/destinations/upollo/types.ts b/packages/browser-destinations/src/destinations/upollo/types.ts new file mode 100644 index 0000000000..b9b4ed5d0a --- /dev/null +++ b/packages/browser-destinations/src/destinations/upollo/types.ts @@ -0,0 +1,17 @@ +export declare class UpolloClient { + private readonly upollo + constructor(projectApiKey: string, options?: unknown) + track( + userinfo?: UpUser, + eventtype?: number // eventtype is a proto enum, but we cant use those types here. + ): Promise +} + +export interface UpUser { + userId?: string + userEmail?: string + userPhone?: string + userName?: string + userImage?: string + customerSuppliedValues?: Record +} diff --git a/packages/browser-destinations/src/destinations/wisepops/__tests__/index.test.ts b/packages/browser-destinations/src/destinations/wisepops/__tests__/index.test.ts index 4847ace930..a8c0757e27 100644 --- a/packages/browser-destinations/src/destinations/wisepops/__tests__/index.test.ts +++ b/packages/browser-destinations/src/destinations/wisepops/__tests__/index.test.ts @@ -15,9 +15,9 @@ const subscriptions: Subscription[] = [ describe('Wisepops', () => { test('initialize Wisepops with a website hash', async () => { - const startTime = Date.now(); + const startTime = Date.now() jest.spyOn(destination, 'initialize') - nock('https://loader.wisepops.com').get('/get-loader.js?v=1&site=1234567890').reply(200, {}) + nock('https://loader.wisepops.com').get('/get-loader.js?plugin=segment&v=1&site=1234567890').reply(200, {}) const [event] = await wisepopsDestination({ websiteId: '1234567890', @@ -27,15 +27,15 @@ describe('Wisepops', () => { await event.load(Context.system(), {} as Analytics) expect(destination.initialize).toHaveBeenCalled() - expect(window.wisepops.q).toEqual([['options', {autoPageview: false}]]); - expect(window.wisepops.l).toBeGreaterThanOrEqual(startTime); - expect(window.wisepops.l).toBeLessThanOrEqual(Date.now()); + expect(window.wisepops.q).toEqual([['options', { autoPageview: false }]]) + expect(window.wisepops.l).toBeGreaterThanOrEqual(startTime) + expect(window.wisepops.l).toBeLessThanOrEqual(Date.now()) const scripts = window.document.querySelectorAll('script') expect(scripts).toMatchInlineSnapshot(` NodeList [