From 65c237158cef2947dbfeec3a38a286cce8bdfb8a Mon Sep 17 00:00:00 2001 From: Oscar Bazaldua <511911+oscb@users.noreply.github.com> Date: Wed, 6 Apr 2022 17:39:58 -0700 Subject: [PATCH] feat: plugins can now disable integrations by marking them as false in the event.integrations (#496) --- example/e2e/mockServer.js | 13 +++- example/package.json | 2 +- example/yarn.lock | 59 ++++++++++--------- .../__tests__/__helpers__/mockSegmentStore.ts | 5 +- .../__tests__/internal/fetchSettings.test.ts | 6 +- packages/core/src/analytics.ts | 3 +- packages/core/src/constants.e2e.mock.ts | 9 ++- packages/core/src/constants.ts | 2 + packages/core/src/plugin.ts | 17 ++++++ .../__tests__/SegmentDestination.test.ts | 31 ++++++++++ 10 files changed, 111 insertions(+), 36 deletions(-) diff --git a/example/e2e/mockServer.js b/example/e2e/mockServer.js index 9aac1e50d..f61f971d4 100644 --- a/example/e2e/mockServer.js +++ b/example/e2e/mockServer.js @@ -15,12 +15,23 @@ export const startServer = async (mockServerListener) => { app.use(bodyParser.json()); - app.post('/', (req, res) => { + // Handles batch events + app.post('/events', (req, res) => { console.log(`➡️ Received request`); mockServerListener(req.body); res.status(200).send({ mockSuccess: true }); }); + // Handles settings calls + app.get('/settings/:writeKey/*', (req, res) => { + console.log(`➡️ Replying with Settings`); + res.status(200).send({ + integrations: { + 'Segment.io': {}, + }, + }); + }); + server = app.listen(port, () => { console.log(`🚀 Started mock server on port ${port}`); resolve(); diff --git a/example/package.json b/example/package.json index df089c277..a6762ff51 100644 --- a/example/package.json +++ b/example/package.json @@ -42,7 +42,7 @@ "babel-jest": "^26.6.3", "babel-plugin-module-resolver": "^4.1.0", "body-parser": "^1.19.0", - "detox": "^18.23.1", + "detox": "^19.6.0", "eslint": "8.2.0", "express": "^4.17.1", "jest": "^27.3.1", diff --git a/example/yarn.lock b/example/yarn.lock index 1837ca8f2..09fb1b6dd 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -1902,6 +1902,16 @@ ajv@^6.10.0, ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^8.6.3: + version "8.11.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f" + integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + anser@^1.4.9: version "1.4.10" resolved "https://registry.yarnpkg.com/anser/-/anser-1.4.10.tgz#befa3eddf282684bd03b63dcda3927aef8c2e35b" @@ -2436,10 +2446,10 @@ buffer@^5.2.0: base64-js "^1.3.1" ieee754 "^1.1.13" -bunyan-debug-stream@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/bunyan-debug-stream/-/bunyan-debug-stream-1.1.2.tgz#3d09a788a8ddf37a23b6840e7e19cf46239bc7b4" - integrity sha512-mNU4QelBu9tUyE6VA0+AQdyillEMefx/2h7xkNL1Uvhw5w9JWtwGWAb7Rdnmj9opmwEaPrRvnJSy2+c1q47+sA== +bunyan-debug-stream@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/bunyan-debug-stream/-/bunyan-debug-stream-2.0.1.tgz#9bd7c7e30c7b2cf711317e9d37529b0464c3b164" + integrity sha512-MCEoqggU7NMt7f2O+PU8VkqfSkoQoa4lmN/OWhaRfqFRBF1Se2TOXQyLF6NxC+EtfrdthnquQe8jOe83fpEoGA== dependencies: colors "1.4.0" exception-formatter "^1.0.4" @@ -3004,19 +3014,19 @@ detect-newline@^3.0.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== -detox@^18.23.1: - version "18.23.1" - resolved "https://registry.yarnpkg.com/detox/-/detox-18.23.1.tgz#f09f5e50291cdab3d62dc40ff2e8bb5cfb3cb776" - integrity sha512-MnOXfTcBBcXTrlLk3EeHq1nEfob79nChZbfOtlEummyec/X+PQzEvmKk2cvsUzu1f7GiNbCiBKN66w47Z7b/CQ== +detox@^19.6.0: + version "19.6.0" + resolved "https://registry.yarnpkg.com/detox/-/detox-19.6.0.tgz#eaef86524dc6184b5bf2e841a5f00968f5fe4813" + integrity sha512-TEoi19rJQIValWrvHf6ensOxw1smykj3qemvqOGF+KJT5pf5WcPgEpNI/Z6/9AipGqEhgbTDt7GpOnA7WS+VNQ== dependencies: + ajv "^8.6.3" bunyan "^1.8.12" - bunyan-debug-stream "^1.1.0" + bunyan-debug-stream "^2.0.1" chalk "^2.4.2" child-process-promise "^2.2.0" find-up "^4.1.0" fs-extra "^4.0.2" funpermaproxy "^1.0.1" - get-port "^2.1.0" ini "^1.3.4" lodash "^4.17.5" minimist "^1.2.0" @@ -3813,13 +3823,6 @@ get-package-type@^0.1.0: resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== -get-port@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/get-port/-/get-port-2.1.0.tgz#8783f9dcebd1eea495a334e1a6a251e78887ab1a" - integrity sha1-h4P53OvR7qSVozThpqJR54iHqxo= - dependencies: - pinkie-promise "^2.0.0" - get-stdin@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b" @@ -5084,6 +5087,11 @@ json-schema-traverse@^0.4.1: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" @@ -6210,18 +6218,6 @@ pify@^4.0.1: resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== -pinkie-promise@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" - integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= - dependencies: - pinkie "^2.0.0" - -pinkie@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" - integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= - pirates@^4.0.1, pirates@^4.0.4, pirates@^4.0.5: version "4.0.5" resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" @@ -6678,6 +6674,11 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + require-main-filename@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" diff --git a/packages/core/src/__tests__/__helpers__/mockSegmentStore.ts b/packages/core/src/__tests__/__helpers__/mockSegmentStore.ts index a3ec28ec4..3aa2870d5 100644 --- a/packages/core/src/__tests__/__helpers__/mockSegmentStore.ts +++ b/packages/core/src/__tests__/__helpers__/mockSegmentStore.ts @@ -1,3 +1,4 @@ +import { SEGMENT_DESTINATION_KEY } from '../../plugins/SegmentDestination'; import type { DeepLinkData, Storage } from '../../storage'; import type { Context, @@ -21,7 +22,9 @@ const INITIAL_VALUES: Data = { isReady: true, events: [], context: undefined, - settings: {}, + settings: { + [SEGMENT_DESTINATION_KEY]: {}, + }, userInfo: { anonymousId: 'anonymousId', userId: undefined, diff --git a/packages/core/src/__tests__/internal/fetchSettings.test.ts b/packages/core/src/__tests__/internal/fetchSettings.test.ts index 141bdec82..8369934c9 100644 --- a/packages/core/src/__tests__/internal/fetchSettings.test.ts +++ b/packages/core/src/__tests__/internal/fetchSettings.test.ts @@ -1,10 +1,14 @@ import { getMockLogger } from '../__helpers__/mockLogger'; import { SegmentClient } from '../../analytics'; import { MockSegmentStore } from '../__helpers__/mockSegmentStore'; +import { SEGMENT_DESTINATION_KEY } from '../../plugins/SegmentDestination'; describe('internal #getSettings', () => { const defaultIntegrationSettings = { - integrations: {}, + integrations: { + // This one is injected by the mock + [SEGMENT_DESTINATION_KEY]: {}, + }, }; const store = new MockSegmentStore(); diff --git a/packages/core/src/analytics.ts b/packages/core/src/analytics.ts index 549e88fb0..5a565ef42 100644 --- a/packages/core/src/analytics.ts +++ b/packages/core/src/analytics.ts @@ -1,6 +1,7 @@ import type { Unsubscribe } from '@segment/sovran-react-native'; import deepmerge from 'deepmerge'; import { AppState, AppStateStatus } from 'react-native'; +import { settingsCDN } from './constants'; import { getContext } from './context'; import { applyRawEventData, @@ -237,7 +238,7 @@ export class SegmentClient { } async fetchSettings() { - const settingsEndpoint = `https://cdn-settings.segment.com/v1/projects/${this.config.writeKey}/settings`; + const settingsEndpoint = `${settingsCDN}/${this.config.writeKey}/settings`; try { const res = await fetch(settingsEndpoint); diff --git a/packages/core/src/constants.e2e.mock.ts b/packages/core/src/constants.e2e.mock.ts index 4cc3b8867..09436949e 100644 --- a/packages/core/src/constants.e2e.mock.ts +++ b/packages/core/src/constants.e2e.mock.ts @@ -2,8 +2,13 @@ import { Platform } from 'react-native'; import type { Config } from '.'; export const batchApi = Platform.select({ - ios: 'http://localhost:9091', - android: 'http://10.0.2.2:9091', + ios: 'http://localhost:9091/events', + android: 'http://10.0.2.2:9091/events', +}); + +export const settingsCDN = Platform.select({ + ios: 'http://localhost:9091/settings', + android: 'http://10.0.2.2:9091/settings', }); export const defaultApiHost = 'api.segment.io/v1'; diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts index 440190dcb..eb030f8bd 100644 --- a/packages/core/src/constants.ts +++ b/packages/core/src/constants.ts @@ -3,6 +3,8 @@ import type { Config } from './types'; export const batchApi = 'https://api.segment.io/v1/batch'; export const defaultApiHost = 'api.segment.io/v1'; +export const settingsCDN = 'https://cdn-settings.segment.com/v1/projects'; + export const defaultConfig: Config = { writeKey: '', flushAt: 20, diff --git a/packages/core/src/plugin.ts b/packages/core/src/plugin.ts index 65dd49779..bde86f7ca 100644 --- a/packages/core/src/plugin.ts +++ b/packages/core/src/plugin.ts @@ -100,6 +100,19 @@ export class DestinationPlugin extends EventPlugin { timeline = new Timeline(); + private hasSettings() { + return this.analytics?.settings.get()?.[this.key] !== undefined; + } + + private isEnabled(event: SegmentEvent): boolean { + let customerDisabled = false; + if (event.integrations?.[this.key] === false) { + customerDisabled = true; + } + + return this.hasSettings() && !customerDisabled; + } + /** Adds a new plugin to the currently loaded set. @@ -141,6 +154,10 @@ export class DestinationPlugin extends EventPlugin { } execute(event: SegmentEvent): SegmentEvent | undefined { + if (!this.isEnabled(event)) { + return undefined; + } + // Apply before and enrichment plugins const beforeResult = this.timeline.applyPlugins({ type: PluginType.before, diff --git a/packages/core/src/plugins/__tests__/SegmentDestination.test.ts b/packages/core/src/plugins/__tests__/SegmentDestination.test.ts index 680240512..a3319988f 100644 --- a/packages/core/src/plugins/__tests__/SegmentDestination.test.ts +++ b/packages/core/src/plugins/__tests__/SegmentDestination.test.ts @@ -53,6 +53,7 @@ describe('SegmentDestination', () => { firebase: { someConfig: 'someValue', }, + [SEGMENT_DESTINATION_KEY]: {}, }, }), }); @@ -140,6 +141,7 @@ describe('SegmentDestination', () => { firebase: { someConfig: 'someValue', }, + [SEGMENT_DESTINATION_KEY]: {}, }, }), }); @@ -209,4 +211,33 @@ describe('SegmentDestination', () => { })), }); }); + + it('lets plugins/events disable destinations individually', () => { + const plugin = new SegmentDestination(); + // @ts-ignore + plugin.analytics = new SegmentClient({ + ...clientArgs, + store: new MockSegmentStore({ + settings: { + [SEGMENT_DESTINATION_KEY]: {}, + }, + }), + }); + + const event: TrackEventType = { + anonymousId: '3534a492-e975-4efa-a18b-3c70c562fec2', + event: 'Awesome event', + type: EventType.TrackEvent, + properties: {}, + timestamp: '2000-01-01T00:00:00.000Z', + messageId: '1d1744bf-5beb-41ac-ad7a-943eac33babc', + context: { app: { name: 'TestApp' } }, + integrations: { + [SEGMENT_DESTINATION_KEY]: false, + }, + }; + + const result = plugin.execute(event); + expect(result).toEqual(undefined); + }); });