diff --git a/examples/AnalyticsReactNativeExample/App.tsx b/examples/AnalyticsReactNativeExample/App.tsx index 96c9a39e..35b353ce 100644 --- a/examples/AnalyticsReactNativeExample/App.tsx +++ b/examples/AnalyticsReactNativeExample/App.tsx @@ -32,7 +32,7 @@ import {useState} from 'react'; // import { BrazePlugin } from '@segment/analytics-react-native-plugin-braze'; const segmentClient = createClient({ - writeKey: '', + writeKey: '', trackAppLifecycleEvents: true, collectDeviceId: true, debug: true, diff --git a/examples/AnalyticsReactNativeExample/ios/AnalyticsReactNativeExample/PrivacyInfo.xcprivacy b/examples/AnalyticsReactNativeExample/ios/AnalyticsReactNativeExample/PrivacyInfo.xcprivacy index 41b8317f..bad32761 100644 --- a/examples/AnalyticsReactNativeExample/ios/AnalyticsReactNativeExample/PrivacyInfo.xcprivacy +++ b/examples/AnalyticsReactNativeExample/ios/AnalyticsReactNativeExample/PrivacyInfo.xcprivacy @@ -6,18 +6,18 @@ NSPrivacyAccessedAPIType - NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPICategoryUserDefaults NSPrivacyAccessedAPITypeReasons - C617.1 + CA92.1 NSPrivacyAccessedAPIType - NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPICategoryFileTimestamp NSPrivacyAccessedAPITypeReasons - CA92.1 + C617.1 diff --git a/examples/AnalyticsReactNativeExample/ios/Podfile.lock b/examples/AnalyticsReactNativeExample/ios/Podfile.lock index 57c49cbc..efdb1540 100644 --- a/examples/AnalyticsReactNativeExample/ios/Podfile.lock +++ b/examples/AnalyticsReactNativeExample/ios/Podfile.lock @@ -1,5 +1,34 @@ PODS: - boost (1.84.0) + - braze-react-native-sdk (13.2.0): + - BrazeKit (~> 11.2.0) + - BrazeLocation (~> 11.2.0) + - BrazeUI (~> 11.2.0) + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - BrazeKit (11.2.0) + - BrazeLocation (11.2.0): + - BrazeKit (= 11.2.0) + - BrazeUI (11.2.0): + - BrazeKit (= 11.2.0) - DoubleConversion (1.1.6) - FBLazyVector (0.76.1) - fmt (9.1.0) @@ -1577,7 +1606,7 @@ PODS: - React-logger (= 0.76.1) - React-perflogger (= 0.76.1) - React-utils (= 0.76.1) - - RNCAsyncStorage (2.0.0): + - RNCAsyncStorage (2.1.0): - DoubleConversion - glog - hermes-engine @@ -1619,16 +1648,17 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - segment-analytics-react-native (2.20.2): + - segment-analytics-react-native (2.20.3): - React-Core - sovran-react-native - SocketRocket (0.7.1) - - sovran-react-native (1.1.2): + - sovran-react-native (1.1.3): - React-Core - Yoga (0.0.0) DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) + - "braze-react-native-sdk (from `../node_modules/@braze/react-native-sdk`)" - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) - fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`) @@ -1696,17 +1726,22 @@ DEPENDENCIES: - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - - "segment-analytics-react-native (from `../node_modules/@segment/analytics-react-native`)" - - "sovran-react-native (from `../node_modules/@segment/sovran-react-native`)" + - segment-analytics-react-native (from `../../../packages/core`) + - sovran-react-native (from `../../../packages/sovran`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) SPEC REPOS: trunk: + - BrazeKit + - BrazeLocation + - BrazeUI - SocketRocket EXTERNAL SOURCES: boost: :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" + braze-react-native-sdk: + :path: "../node_modules/@braze/react-native-sdk" DoubleConversion: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" FBLazyVector: @@ -1839,84 +1874,88 @@ EXTERNAL SOURCES: RNGestureHandler: :path: "../node_modules/react-native-gesture-handler" segment-analytics-react-native: - :path: "../node_modules/@segment/analytics-react-native" + :path: "../../../packages/core" sovran-react-native: - :path: "../node_modules/@segment/sovran-react-native" + :path: "../../../packages/sovran" Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 + braze-react-native-sdk: d3ba64fe18e95f16c1ba8c105d68d3f19d4d75f1 + BrazeKit: d430e506a325f0f5a1b60cea61ef95035c1d396b + BrazeLocation: f574503d26e00658c7a6ddfa1efec68ddaa12e4d + BrazeUI: 1500cfce86252db6ce280815c94e0eed03e42691 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 FBLazyVector: 7075bb12898bc3998fd60f4b7ca422496cc2cdf7 fmt: 10c6e61f4be25dc963c36bd73fc7b1705fe975be glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a hermes-engine: 46f1ffbf0297f4298862068dd4c274d4ac17a1fd - RCT-Folly: bf5c0376ffe4dd2cf438dcf86db385df9fdce648 + RCT-Folly: 84578c8756030547307e4572ab1947de1685c599 RCTDeprecation: fde92935b3caa6cb65cbff9fbb7d3a9867ffb259 RCTRequired: 75c6cee42d21c1530a6f204ba32ff57335d19007 RCTTypeSafety: 7e6fe47bfb693c50d4669db1a480ca5331795f5b React: 8e73704cdd5c7f801936776d2fc434c605a7827b React-callinvoker: fa27d1e091e683de88f576e6a5d4efc171929a4c - React-Core: 8dd14bffcc9b877091b698e45701160669a31f91 - React-CoreModules: b4437acf2ef25ce3689c84df661dc5d806559b35 - React-cxxreact: 6125cd820da7e18f9ca8343b3c42ee61634a4e0d + React-Core: 948deed7fa720eeb0d901ff9e45c3719767dab5f + React-CoreModules: a11ba75f64245d12a0869203664a802c11594c43 + React-cxxreact: a5ce05f8a0a1398958523f948fce00d4c8ce38ff React-debug: f474f5c202a277f76c81bf7cf26284f2c09880d7 - React-defaultsnativemodule: 7141fa704531cbf7a7e7af3bc02adfa367e831a7 - React-domnativemodule: c1806b8584a53ed912012a4d8b2c6f96a84c77a3 - React-Fabric: ba9636cfc7f9b77df6cb7edb2c70d0237026404b - React-FabricComponents: c408da05a4ea5ba071732245b4a7f48f904e610a - React-FabricImage: c409858f319f11709b49ffa6c5bca4faf794cb44 + React-defaultsnativemodule: 41cc9a60277f1bec4b258df324e28705ac00b91a + React-domnativemodule: 4fe895d9e4aa99590700c5a5f9ff5706e9481ed7 + React-Fabric: bbdcc01a98528846efacf0767567a8e76df794bb + React-FabricComponents: ab8967c5898d88f37486df0eb0111384c498d821 + React-FabricImage: 7a06db59488b37f509dee73fa0b2811608a67058 React-featureflags: 929732439d139ac0662e08f009f1a51ed2b91ed3 - React-featureflagsnativemodule: 02dd903d4cbe4fae0e6cd02bc32a09d30543282f - React-graphics: a5cad35307286e9f83e212834e95fef4010d03d0 - React-hermes: 14aafa9630579b84c2167b563bdb8c811970a03e - React-idlecallbacksnativemodule: 69581ac44bd355acce3739c3fe380c0f6d7a6d09 - React-ImageManager: 41945afb3ace0c52255057ec4ae6af6f5a23539f - React-jserrorhandler: ecbc4622df7ab3d0066a4313cde4172d45745508 - React-jsi: ff383df87c7047e976a66be45df59e4e0db5346e - React-jsiexecutor: 2bb8b172f226f2f502521d33dd7666e701d45f45 - React-jsinspector: 4d51b903543f21076b658ef8412f3102778dbc92 - React-jsitracing: 654f4d9cb9fd99b3d96f239ceb215ae49ce28ac0 - React-logger: 97c9dafae1f1a638001a9d1d0e93d431f2f9cb7b - React-Mapbuffer: 3146a13424f9fec2ea1f1462d49d566e4d69b732 - React-microtasksnativemodule: 02d218c79c72d373a92a8552183f4ead0d1c6e05 - react-native-get-random-values: 21325b2244dfa6b58878f51f9aa42821e7ba3d06 - react-native-safe-area-context: 2500e4fe998caad50ad3bc51ec23ef951308569e + React-featureflagsnativemodule: b88d53b6d63ee037c5cdefb9861edfd16b4afce1 + React-graphics: 6367275cc82d631c588a7146fd8dc69ec2f447e8 + React-hermes: b9bbe9c808d7ab1750ce089b243b03e4a099af63 + React-idlecallbacksnativemodule: 6fff2280f860f29a3c049695d3ef04c8f70212aa + React-ImageManager: 5b001b9e974f5ba81f0645d3d799e2a20c61d91e + React-jserrorhandler: 35e5e5a5a99b7b36c3802a2d12ca86889ed5982a + React-jsi: d0d8c4019fd91d0cb4b432f2518e08dc37433a13 + React-jsiexecutor: 1cdaf24e36919d899250938f0f6c79ec1a256923 + React-jsinspector: 2fabeadbd0eb1cbd83a6fc2026fb38c75b200947 + React-jsitracing: 7c7c89c963893efd25e0d04c23e854b9a93e0b7e + React-logger: 7b5b458327a1ff0d7e5a349430d1ed133dcebaa3 + React-Mapbuffer: 0d88ad9afa9e195dd7634424bde1d38e4129e646 + React-microtasksnativemodule: 17234f35d37e6ed388e18a6314210b3b9e051219 + react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba + react-native-safe-area-context: 819935871d06a80e963546208027f839aa972a85 React-nativeconfig: 93fe8c85a8c40820c57814e30f3e44b94c995a7b - React-NativeModulesApple: b3e076fd0d7b73417fe1e8c8b26e3c57ae9b74aa - React-perflogger: 1c55bcd3c392137cbaf0d21d8bb87ce9a0cebb15 - React-performancetimeline: e89249db10b8f7bf8f72c2e9bd471ac37d48b753 + React-NativeModulesApple: a4457b73e63e983db66d66612160006bccb00ad5 + React-perflogger: 3140b7778984a486db80d4d2aeaa266cae4eb8c7 + React-performancetimeline: 41c100bc1299d7b150821b99cf26661c51ed9ab0 React-RCTActionSheet: 9407c795fbeee35da2dae3cd6b5c4e5da6ff8bd3 - React-RCTAnimation: 7ee1c2a77aab7e5c568611d8092a994cfcbe8410 - React-RCTAppDelegate: 10c2b0c434baf5a71b53d5c86c4d8d0dbd6bb380 - React-RCTBlob: 761072706300d22624ec2d6bf860b77d95ebd3da - React-RCTFabric: 871d38933a94554d9e27963aa4bb67184dc7529e - React-RCTImage: b6614fde902ec9647f15236da94df2d24c40523f - React-RCTLinking: 25950eda5d5f786bfb3daf513ea7d848555a2a93 - React-RCTNetwork: b69407c4119fd7a1cc07db4a94563f2546f8770d - React-RCTSettings: b310a4923446c3a8950fa866c8cf83323a9e1b87 - React-RCTText: 77c6eda5be1dee657f5183f75fe0fdcdb7b2b35d - React-RCTVibration: b4889c7702aea1b07316be1ec0de2e36e9a4d077 + React-RCTAnimation: 48e5c6b541fd4c7a96c333e61974c3de34bbe849 + React-RCTAppDelegate: 602daadf2452a56ca54a6257052ddba89e680486 + React-RCTBlob: f67be4e0fbe51db1574aec402754054ab9c39668 + React-RCTFabric: ee6706069cbc4e1ffd5f23553e999a42b08414f7 + React-RCTImage: 57894a0e42502461d87449bec6cb0f124a49a93b + React-RCTLinking: abd71677bc3353327bec26b0ccd0a0c3960efa1c + React-RCTNetwork: 2e91efa49b63e54a9782922e5ca1d09ff2789341 + React-RCTSettings: fd13eebaa3f9af0b56a0ecb053b108e160fbfe07 + React-RCTText: 4cd7c87db1e1da51a96b86ce39c5468c1dbaae60 + React-RCTVibration: 579f64ceb06701eca3004a500169e1152c1ef7d2 React-rendererconsistency: 5ef1c4642fd6365bf6d5d4e29a3ae02c3a1b8980 - React-rendererdebug: 7f6a24cbb5008a22ccb34a0d031a259b006facf6 + React-rendererdebug: 8952e1ad914c680d4978916a9eed7c6dc85301d7 React-rncore: 0e5394ce20a9d2bf12409d14395588c7b9e6e9ce - React-RuntimeApple: bbe293f233d17304c9597309acde7505080fd53d - React-RuntimeCore: 5a1cbfc3e7af4fbdea2b9b1efd39cd51a4d4006f + React-RuntimeApple: f5ed38fba1230713313e88e750dcad06948ba625 + React-RuntimeCore: 0fc488daf136f05d96349772828ccf64f66d6d2a React-runtimeexecutor: ffac5f09795a5e881477e0d72a0fa6385456bed3 - React-RuntimeHermes: 0a1fd1c150faed8341887dd89895eeb8d4d2d3c5 - React-runtimescheduler: e7df538274de0c65736068e40efc0d2228f42d0d + React-RuntimeHermes: b8f395d41116c3bdf3373e87c39a856f69c3fff8 + React-runtimescheduler: 933c72afd4f285b2bb473c0de2482ee250f3e735 React-timing: b3b233fe819d9e5b6ca32b605aa732621bdfa5aa - React-utils: 5362bd16a9563f9916e7a56c011ddc533507650f - ReactCodegen: 865bafc5c17ec2181620ced1a32c39c38ab2951d - ReactCommon: 422e364463f33e336fc4db196aeb50fd801d90d6 - RNCAsyncStorage: 597673c6086d359029afefef8fd5859f1f35ab87 - RNGestureHandler: fc5ce5bf284640d3af6431c3a5c3bc121e98d045 - segment-analytics-react-native: 53785e35d44a0643beffa40eada68a4cbdf7292e + React-utils: 0c825829a8e2ca39bb049d95f270a2dbf39ecb05 + ReactCodegen: 3b0ff1c9015e3ebcf2bd2f8559995c74bfacf8a1 + ReactCommon: c21a3d6a8d3e98b6e99730139a52f59f0beea89d + RNCAsyncStorage: 2edc69cf6db9299363a11477668b7f452f2cb4a6 + RNGestureHandler: 16ef3dc2d7ecb09f240f25df5255953c4098819b + segment-analytics-react-native: 6f98edf18246782ee7428c5380c6519a3d2acf5e SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - sovran-react-native: 5f02bd2d111ffe226d00c7b0435290eae6f10934 + sovran-react-native: a3ad3f8ff90c2002b2aa9790001a78b0b0a38594 Yoga: db69236006b8b1c6d55ab453390c882306cbf219 PODFILE CHECKSUM: 8834295e47cf03bbd18d22b7b8db5ca14f4085ae -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/examples/AnalyticsReactNativeExample/package.json b/examples/AnalyticsReactNativeExample/package.json index a6cb0bf8..0ff4b223 100644 --- a/examples/AnalyticsReactNativeExample/package.json +++ b/examples/AnalyticsReactNativeExample/package.json @@ -15,10 +15,12 @@ "clean:ios": "rimraf ios/build ios/Pods" }, "dependencies": { + "@braze/react-native-sdk": "^13.2.0", "@react-native-async-storage/async-storage": "^2.1.0", "@react-navigation/native": "^6.1.18", "@react-navigation/stack": "^6.4.1", "@segment/analytics-react-native": "^2.20.3", + "@segment/analytics-react-native-plugin-braze": "^0.7.0", "@segment/sovran-react-native": "^1.1.3", "react": "18.3.1", "react-native": "0.76.1", diff --git a/examples/AnalyticsReactNativeExample/yarn.lock b/examples/AnalyticsReactNativeExample/yarn.lock index ad9881d6..ae8fa797 100644 --- a/examples/AnalyticsReactNativeExample/yarn.lock +++ b/examples/AnalyticsReactNativeExample/yarn.lock @@ -1534,6 +1534,13 @@ __metadata: languageName: node linkType: hard +"@braze/react-native-sdk@npm:^13.2.0": + version: 13.2.0 + resolution: "@braze/react-native-sdk@npm:13.2.0" + checksum: 10c0/7626d50a620fc4a7efbe8c8a36171a822b43af8e5672f8caa719e2e5d904878515bf9ec28c1d692da3558902243052ccef092f2d8ac61d8ce5dddcacdf3c138b + languageName: node + linkType: hard + "@egjs/hammerjs@npm:^2.0.17": version: 2.0.17 resolution: "@egjs/hammerjs@npm:2.0.17" @@ -2524,6 +2531,16 @@ __metadata: languageName: node linkType: hard +"@segment/analytics-react-native-plugin-braze@npm:^0.7.0": + version: 0.7.0 + resolution: "@segment/analytics-react-native-plugin-braze@npm:0.7.0" + peerDependencies: + "@braze/react-native-sdk": ^10.x + "@segment/analytics-react-native": ^2.18.0 + checksum: 10c0/dcff89d861655c65c548fcd53812c423d43bf730d8a6c3d5c21af7e1a165b95ef216465990c71be9bf24115afa83f49301ba8c5e0b13cbac26117fb4d02e7d51 + languageName: node + linkType: hard + "@segment/analytics-react-native@npm:^2.20.3": version: 2.20.3 resolution: "@segment/analytics-react-native@npm:2.20.3" @@ -4195,6 +4212,7 @@ __metadata: "@babel/core": "npm:^7.25.2" "@babel/preset-env": "npm:^7.25.3" "@babel/runtime": "npm:^7.25.0" + "@braze/react-native-sdk": "npm:^13.2.0" "@react-native-async-storage/async-storage": "npm:^2.1.0" "@react-native-community/cli": "npm:15.0.0" "@react-native-community/cli-platform-android": "npm:15.0.0" @@ -4206,6 +4224,7 @@ __metadata: "@react-navigation/native": "npm:^6.1.18" "@react-navigation/stack": "npm:^6.4.1" "@segment/analytics-react-native": "npm:^2.20.3" + "@segment/analytics-react-native-plugin-braze": "npm:^0.7.0" "@segment/sovran-react-native": "npm:^1.1.3" "@types/react": "npm:^18.2.6" "@types/react-test-renderer": "npm:^18.0.0" diff --git a/examples/E2E/App.tsx b/examples/E2E/App.tsx index a8488968..9db2bcf3 100644 --- a/examples/E2E/App.tsx +++ b/examples/E2E/App.tsx @@ -40,6 +40,7 @@ const segmentClient = createClient({ autoAddSegmentDestination: true, collectDeviceId: true, debug: true, + useSegmentEndpoints: true, //if you pass only domain/v1 as proxy setup, use this flag to append segment endpoints. Otherwise you can remove it and customise proxy completely flushPolicies: [ new CountFlushPolicy(5), // These are disabled for E2E tests @@ -47,12 +48,12 @@ const segmentClient = createClient({ // new StartupFlushPolicy(), ], proxy: Platform.select({ - ios: 'http://localhost:9091/events', - android: 'http://10.0.2.2:9091/events', + ios: 'http://localhost:9091/v1', + android: 'http://10.0.2.2:9091/v1', }), cdnProxy: Platform.select({ - ios: 'http://localhost:9091/settings', - android: 'http://10.0.2.2:9091/settings', + ios: 'http://localhost:9091/v1', + android: 'http://10.0.2.2:9091/v1', }), }); diff --git a/examples/E2E/e2e/mockServer.js b/examples/E2E/e2e/mockServer.js index d5976832..644a6d5c 100644 --- a/examples/E2E/e2e/mockServer.js +++ b/examples/E2E/e2e/mockServer.js @@ -14,9 +14,9 @@ export const startServer = async (mockServerListener) => { const app = express(); app.use(bodyParser.json()); - + // Handles batch events - app.post('/events', (req, res) => { + app.post('/v1/b', (req, res) => { console.log(`➡️ Received request`); const body = req.body; mockServerListener(body); @@ -25,7 +25,7 @@ export const startServer = async (mockServerListener) => { }); // Handles settings calls - app.get('/settings/*', (req, res) => { + app.get('/v1/projects/yup/settings', (req, res) => { console.log(`➡️ Replying with Settings`); res.status(200).send({ integrations: { diff --git a/packages/core/src/__tests__/internal/fetchSettings.test.ts b/packages/core/src/__tests__/internal/fetchSettings.test.ts index c36d8e84..f0f0206d 100644 --- a/packages/core/src/__tests__/internal/fetchSettings.test.ts +++ b/packages/core/src/__tests__/internal/fetchSettings.test.ts @@ -2,6 +2,7 @@ import { SegmentClient } from '../../analytics'; import { settingsCDN } from '../../constants'; import { SEGMENT_DESTINATION_KEY } from '../../plugins/SegmentDestination'; import { getMockLogger, MockSegmentStore } from '../../test-helpers'; +import { getURL } from '../../util'; describe('internal #getSettings', () => { const defaultIntegrationSettings = { @@ -25,7 +26,7 @@ describe('internal #getSettings', () => { store: store, }; - const client = new SegmentClient(clientArgs); + //const client = new SegmentClient(clientArgs); const setSettingsSpy = jest.spyOn(store.settings, 'set'); @@ -37,104 +38,223 @@ describe('internal #getSettings', () => { jest.clearAllMocks(); }); - it('fetches the settings succesfully ', async () => { - const mockJSONResponse = { integrations: { foo: 'bar' } }; - const mockResponse = Promise.resolve({ - ok: true, - json: () => mockJSONResponse, - }); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - global.fetch = jest.fn(() => Promise.resolve(mockResponse)); - - await client.fetchSettings(); - - expect(fetch).toHaveBeenCalledWith( - `${settingsCDN}/${clientArgs.config.writeKey}/settings`, - { - headers: { - 'Cache-Control': 'no-cache', - }, - } - ); - - expect(setSettingsSpy).toHaveBeenCalledWith(mockJSONResponse.integrations); - expect(store.settings.get()).toEqual(mockJSONResponse.integrations); - expect(client.settings.get()).toEqual(mockJSONResponse.integrations); - }); + it.each([ + [false, false], // No proxy, No segment endpoint + [false, true], // No proxy, Yes segment endpoint + [true, false], // Yes proxy, No segment endpoint + [true, true], // Yes proxy, Yes segment endpoint + ])( + 'fetches the settings successfully when hasProxy is %s and useSegmentEndpoints is %s', + async (hasProxy, useSegmentEndpoints) => { + const mockJSONResponse = { integrations: { foo: 'bar' } }; + const mockResponse = Promise.resolve({ + ok: true, + json: () => mockJSONResponse, + }); - it('fails to the settings succesfully and uses the default if specified', async () => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - global.fetch = jest.fn(() => Promise.reject()); - - await client.fetchSettings(); - - expect(fetch).toHaveBeenCalledWith( - `${settingsCDN}/${clientArgs.config.writeKey}/settings`, - { - headers: { - 'Cache-Control': 'no-cache', - }, - } - ); - - expect(setSettingsSpy).toHaveBeenCalledWith( - defaultIntegrationSettings.integrations - ); - expect(store.settings.get()).toEqual( - defaultIntegrationSettings.integrations - ); - expect(client.settings.get()).toEqual( - defaultIntegrationSettings.integrations - ); - }); + // Mock global fetch function + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + global.fetch = jest.fn(() => Promise.resolve(mockResponse)); - it('fails to the settings succesfully and has no default settings', async () => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - global.fetch = jest.fn(() => Promise.reject()); - const anotherClient = new SegmentClient({ - ...clientArgs, - config: { ...clientArgs.config, defaultSettings: undefined }, - }); - - await anotherClient.fetchSettings(); - - expect(fetch).toHaveBeenCalledWith( - `${settingsCDN}/${clientArgs.config.writeKey}/settings`, - { - headers: { - 'Cache-Control': 'no-cache', - }, - } - ); - expect(setSettingsSpy).not.toHaveBeenCalled(); - }); + // Set up config based on test parameters + const config = { + ...clientArgs.config, + useSegmentEndpoints, + cdnProxy: hasProxy ? 'https://custom-proxy.com' : undefined, // Set proxy only when true + }; - it('fails to the settings succesfully and has no default settings for soft API errors', async () => { - const mockResponse = Promise.resolve({ - ok: false, - status: 500, - }); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - global.fetch = jest.fn(() => Promise.resolve(mockResponse)); - const anotherClient = new SegmentClient({ - ...clientArgs, - config: { ...clientArgs.config, defaultSettings: undefined }, - }); - - await anotherClient.fetchSettings(); - - expect(fetch).toHaveBeenCalledWith( - `${settingsCDN}/${clientArgs.config.writeKey}/settings`, - { - headers: { - 'Cache-Control': 'no-cache', - }, - } - ); - expect(setSettingsSpy).not.toHaveBeenCalled(); - }); + // Create client with the dynamic config + const client = new SegmentClient({ + ...clientArgs, + config, + }); + + await client.fetchSettings(); + + // Determine expected settings URL based on the logic + const settingsPrefix = config.cdnProxy ?? settingsCDN; + const expectedSettingsPath = + hasProxy && useSegmentEndpoints + ? `/projects/${config.writeKey}/settings` + : `/${config.writeKey}/settings`; + + expect(fetch).toHaveBeenCalledWith( + getURL(settingsPrefix, expectedSettingsPath), + { + headers: { + 'Cache-Control': 'no-cache', + }, + } + ); + + expect(setSettingsSpy).toHaveBeenCalledWith( + mockJSONResponse.integrations + ); + expect(store.settings.get()).toEqual(mockJSONResponse.integrations); + expect(client.settings.get()).toEqual(mockJSONResponse.integrations); + } + ); + + it.each([ + [false, false], // No proxy, No segment endpoint + [false, true], // No proxy, Yes segment endpoint + [true, false], // Yes proxy, No segment endpoint + [true, true], // Yes proxy, Yes segment endpoint + ])( + 'fails to fetch settings and falls back to defaults when hasProxy is %s and useSegmentEndpoints is %s', + async (hasProxy, useSegmentEndpoints) => { + // Mock fetch to reject (simulate failure) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + global.fetch = jest.fn(() => Promise.reject()); + + // Set up config dynamically + const config = { + ...clientArgs.config, + useSegmentEndpoints, + cdnProxy: hasProxy ? 'https://custom-proxy.com' : undefined, // Set proxy only when true + }; + + // Create client with the dynamic config + const client = new SegmentClient({ + ...clientArgs, + config, + }); + + await client.fetchSettings(); + + // Determine expected settings URL + const settingsPrefix = config.cdnProxy ?? settingsCDN; + const expectedSettingsPath = + hasProxy && useSegmentEndpoints + ? `/projects/${config.writeKey}/settings` + : `/${config.writeKey}/settings`; + + expect(fetch).toHaveBeenCalledWith( + getURL(settingsPrefix, expectedSettingsPath), + { + headers: { + 'Cache-Control': 'no-cache', + }, + } + ); + + // Ensure default settings are used after failure + expect(setSettingsSpy).toHaveBeenCalledWith( + defaultIntegrationSettings.integrations + ); + expect(store.settings.get()).toEqual( + defaultIntegrationSettings.integrations + ); + expect(client.settings.get()).toEqual( + defaultIntegrationSettings.integrations + ); + } + ); + + it.each([ + [false, false], // No proxy, No segment endpoint + [false, true], // No proxy, Yes segment endpoint + [true, false], // Yes proxy, No segment endpoint + [true, true], // Yes proxy, Yes segment endpoint + ])( + 'fails to fetch settings and has no default settings when hasProxy is %s and useSegmentEndpoints is %s', + async (hasProxy, useSegmentEndpoints) => { + // Mock fetch to reject (simulate failure) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + global.fetch = jest.fn(() => Promise.reject()); + + // Set up config dynamically + const config = { + ...clientArgs.config, + useSegmentEndpoints, + cdnProxy: hasProxy ? 'https://custom-proxy.com' : undefined, // Set proxy only when true + defaultSettings: undefined, // Ensure no default settings + }; + + // Create client with the dynamic config + const anotherClient = new SegmentClient({ + ...clientArgs, + config, + }); + + await anotherClient.fetchSettings(); + + // Determine expected settings URL + const settingsPrefix = config.cdnProxy ?? settingsCDN; + const expectedSettingsPath = + hasProxy && useSegmentEndpoints + ? `/projects/${config.writeKey}/settings` + : `/${config.writeKey}/settings`; + + expect(fetch).toHaveBeenCalledWith( + getURL(settingsPrefix, expectedSettingsPath), + { + headers: { + 'Cache-Control': 'no-cache', + }, + } + ); + + // Ensure no default settings are applied + expect(setSettingsSpy).not.toHaveBeenCalled(); + } + ); + + it.each([ + [false, false], // No proxy, No segment endpoint + [false, true], // No proxy, Yes segment endpoint + [true, false], // Yes proxy, No segment endpoint + [true, true], // Yes proxy, Yes segment endpoint + ])( + 'fails to fetch settings due to soft API errors and has no default settings when hasProxy is %s and useSegmentEndpoints is %s', + async (hasProxy, useSegmentEndpoints) => { + const mockResponse = Promise.resolve({ + ok: false, + status: 500, // Simulate a soft API error (server error) + }); + + // Mock fetch to return the error response + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + global.fetch = jest.fn(() => Promise.resolve(mockResponse)); + + // Set up config dynamically + const config = { + ...clientArgs.config, + useSegmentEndpoints, + cdnProxy: hasProxy ? 'https://custom-proxy.com' : undefined, // Set proxy only when true + defaultSettings: undefined, // Ensure no default settings + }; + + // Create client with the dynamic config + const anotherClient = new SegmentClient({ + ...clientArgs, + config, + }); + + await anotherClient.fetchSettings(); + + // Determine expected settings URL + const settingsPrefix = config.cdnProxy ?? settingsCDN; + const expectedSettingsPath = + hasProxy && useSegmentEndpoints + ? `/projects/${config.writeKey}/settings` + : `/${config.writeKey}/settings`; + + expect(fetch).toHaveBeenCalledWith( + getURL(settingsPrefix, expectedSettingsPath), + { + headers: { + 'Cache-Control': 'no-cache', + }, + } + ); + + // Ensure no default settings are applied when API fails + expect(setSettingsSpy).not.toHaveBeenCalled(); + } + ); }); diff --git a/packages/core/src/__tests__/util.test.ts b/packages/core/src/__tests__/util.test.ts index 5b34d391..3d2a9a5e 100644 --- a/packages/core/src/__tests__/util.test.ts +++ b/packages/core/src/__tests__/util.test.ts @@ -1,5 +1,5 @@ import { UserTraits } from '../types'; -import { chunk, allSettled, deepCompare } from '../util'; +import { chunk, allSettled, deepCompare, getURL } from '../util'; describe('#chunk', () => { it('handles empty array', () => { @@ -159,3 +159,42 @@ describe('deepCompare', () => { expect(deepCompare(a, b)).toBe(false); }); }); + +describe('getURL function', () => { + // Positive Test Cases + it('should return correct URL for valid host and path', () => { + expect(getURL('www.example.com', '/home')).toBe( + 'https://www.example.com/home' + ); + expect(getURL('blog.example.com', '/posts')).toBe( + 'https://blog.example.com/posts' + ); + }); + + it('should return the root URL when the path is empty', () => { + expect(getURL('www.example.com', '')).toBe('https://www.example.com/'); + }); + + it('should handle query parameters correctly in the URL path', () => { + expect(getURL('www.example.com', '/search?q=test')).toBe( + 'https://www.example.com/search?q=test' + ); + }); + + it('should handle special characters in the URL path', () => { + expect(getURL('www.example.com', '/about#section1')).toBe( + 'https://www.example.com/about#section1' + ); + }); + + // Negative Test Cases + it('should handle empty host gracefully', () => { + expect(getURL('', '/home')).toBe('https:///home'); + }); + + it('should handle invalid characters in the host', () => { + expect(getURL('invalid host.com', '/path')).toBe( + 'https://invalid host.com/path' + ); + }); +}); diff --git a/packages/core/src/analytics.ts b/packages/core/src/analytics.ts index 374b4b9e..8ca66863 100644 --- a/packages/core/src/analytics.ts +++ b/packages/core/src/analytics.ts @@ -57,7 +57,12 @@ import { UserInfoState, UserTraits, } from './types'; -import { allSettled, getPluginsWithFlush, getPluginsWithReset } from './util'; +import { + allSettled, + getPluginsWithFlush, + getPluginsWithReset, + getURL, +} from './util'; import { getUUID } from './uuid'; import type { FlushPolicy } from './flushPolicies'; import { @@ -160,15 +165,13 @@ export class SegmentClient { if (ofType !== undefined) { return [...(plugins[ofType] ?? [])]; } - return ( - [ - ...this.getPlugins(PluginType.before), - ...this.getPlugins(PluginType.enrichment), - ...this.getPlugins(PluginType.utility), - ...this.getPlugins(PluginType.destination), - ...this.getPlugins(PluginType.after), - ] - ); + return [ + ...this.getPlugins(PluginType.before), + ...this.getPlugins(PluginType.enrichment), + ...this.getPlugins(PluginType.utility), + ...this.getPlugins(PluginType.destination), + ...this.getPlugins(PluginType.after), + ]; } /** @@ -320,10 +323,17 @@ export class SegmentClient { async fetchSettings() { const settingsPrefix: string = this.config.cdnProxy ?? settingsCDN; - const settingsEndpoint = `${settingsPrefix}/${this.config.writeKey}/settings`; - + const hasProxy = !!(this.config?.cdnProxy ?? ''); + const useSegmentEndpoints = Boolean(this.config?.useSegmentEndpoints); + let settingsEndpoint = ''; + if (hasProxy && useSegmentEndpoints) { + settingsEndpoint = `/projects/${this.config.writeKey}/settings`; + } else { + settingsEndpoint = `/${this.config.writeKey}/settings`; + } + const settingsURL = getURL(settingsPrefix, settingsEndpoint); try { - const res = await fetch(settingsEndpoint, { + const res = await fetch(settingsURL, { headers: { 'Cache-Control': 'no-cache', }, diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts index a9dd5b87..7ed99478 100644 --- a/packages/core/src/constants.ts +++ b/packages/core/src/constants.ts @@ -1,7 +1,6 @@ import type { Config } from './types'; export const defaultApiHost = 'https://api.segment.io/v1/b'; - export const settingsCDN = 'https://cdn-settings.segment.com/v1/projects'; export const defaultConfig: Config = { @@ -10,6 +9,7 @@ export const defaultConfig: Config = { trackDeepLinks: false, trackAppLifecycleEvents: false, autoAddSegmentDestination: true, + useSegmentEndpoints: false, }; export const workspaceDestinationFilterKey = ''; diff --git a/packages/core/src/plugins/SegmentDestination.ts b/packages/core/src/plugins/SegmentDestination.ts index b3717b8e..ba2353cc 100644 --- a/packages/core/src/plugins/SegmentDestination.ts +++ b/packages/core/src/plugins/SegmentDestination.ts @@ -6,7 +6,7 @@ import { SegmentEvent, UpdateType, } from '../types'; -import { chunk, createPromise } from '../util'; +import { chunk, createPromise, getURL } from '../util'; import { uploadEvents } from '../api'; import type { SegmentClient } from '../analytics'; import { DestinationMetadataEnrichment } from './DestinationMetadataEnrichment'; @@ -90,9 +90,20 @@ export class SegmentDestination extends DestinationPlugin { private getEndpoint(): string { const config = this.analytics?.getConfig(); - return config?.proxy ?? this.apiHost ?? defaultApiHost; - } + const hasProxy = !!(config?.proxy ?? ''); + const useSegmentEndpoints = Boolean(config?.useSegmentEndpoints); + + let endpoint = ''; + if (hasProxy) { + endpoint = useSegmentEndpoints ? '/b' : ''; + } else { + endpoint = '/b'; // If no proxy, always append '/b' + } + + const baseURL = config?.proxy ?? this.apiHost ?? defaultApiHost; + return getURL(baseURL, endpoint); + } configure(analytics: SegmentClient): void { super.configure(analytics); @@ -116,7 +127,8 @@ export class SegmentDestination extends DestinationPlugin { segmentSettings?.apiHost !== undefined && segmentSettings?.apiHost !== null ) { - this.apiHost = `https://${segmentSettings.apiHost}/b`; + //assign the api host from segment settings (domain/v1) + this.apiHost = segmentSettings.apiHost; } this.settingsResolve(); } diff --git a/packages/core/src/plugins/__tests__/SegmentDestination.test.ts b/packages/core/src/plugins/__tests__/SegmentDestination.test.ts index 11f0a49f..0116c9da 100644 --- a/packages/core/src/plugins/__tests__/SegmentDestination.test.ts +++ b/packages/core/src/plugins/__tests__/SegmentDestination.test.ts @@ -18,6 +18,7 @@ import { SEGMENT_DESTINATION_KEY, SegmentDestination, } from '../SegmentDestination'; +import { getURL } from '../../util'; jest.mock('uuid'); @@ -319,14 +320,14 @@ describe('SegmentDestination', () => { expect(sendEventsSpy).toHaveBeenCalledTimes(2); expect(sendEventsSpy).toHaveBeenCalledWith({ - url: defaultApiHost, + url: getURL(defaultApiHost, '/b'), writeKey: '123-456', events: events.slice(0, 2).map((e) => ({ ...e, })), }); expect(sendEventsSpy).toHaveBeenCalledWith({ - url: defaultApiHost, + url: getURL(defaultApiHost, '/b'), writeKey: '123-456', events: events.slice(2, 4).map((e) => ({ ...e, @@ -353,7 +354,7 @@ describe('SegmentDestination', () => { expect(sendEventsSpy).toHaveBeenCalledTimes(1); expect(sendEventsSpy).toHaveBeenCalledWith({ - url: `https://${customEndpoint}/b`, + url: getURL(customEndpoint, '/b'), writeKey: '123-456', events: events.slice(0, 2).map((e) => ({ ...e, @@ -361,35 +362,61 @@ describe('SegmentDestination', () => { }); }); - it('lets user override apiHost with proxy', async () => { - const customEndpoint = 'https://customproxy.com/batchEvents'; - const events = [ - { messageId: 'message-1' }, - { messageId: 'message-2' }, - ] as SegmentEvent[]; - - const { plugin, sendEventsSpy } = createTestWith({ - events: events, - settings: { - apiKey: '', - apiHost: 'events.eu1.segmentapis.com', - }, - config: { - ...clientArgs.config, - proxy: customEndpoint, - }, - }); - - await plugin.flush(); - - expect(sendEventsSpy).toHaveBeenCalledTimes(1); - expect(sendEventsSpy).toHaveBeenCalledWith({ - url: customEndpoint, - writeKey: '123-456', - events: events.slice(0, 2).map((e) => ({ - ...e, - })), - }); - }); + it.each([ + [false, false], // No proxy, No segment endpoint + [false, true], // No proxy, Yes segment endpoint + [true, false], // Yes proxy, No segment endpoint + [true, true], // Yes proxy, Yes segment endpoint + ])( + 'lets user override apiHost with proxy when hasProxy is %s and useSegmentEndpoints is %s', + async (hasProxy, useSegmentEndpoints) => { + const customEndpoint = 'https://customproxy.com/batchEvents'; + const events = [ + { messageId: 'message-1' }, + { messageId: 'message-2' }, + ] as SegmentEvent[]; + + const { plugin, sendEventsSpy } = createTestWith({ + events, + settings: { + apiKey: '', + apiHost: 'events.eu1.segmentapis.com', + }, + config: { + ...clientArgs.config, + proxy: hasProxy ? customEndpoint : undefined, // Only set proxy when true + useSegmentEndpoints, // Pass the flag dynamically + }, + }); + + // Determine expected URL logic + let expectedUrl: string; + if (hasProxy) { + if (useSegmentEndpoints) { + expectedUrl = getURL(customEndpoint, '/b'); + } else { + expectedUrl = getURL(customEndpoint, ''); + console.log('expected URL---->', expectedUrl); + } + } else { + expectedUrl = getURL('events.eu1.segmentapis.com', '/b'); + } + + // let expectedUrl = hasProxy + // ? getURL(customEndpoint, useSegmentEndpoints ? '/b' : '') + // : getURL('events.eu1.segmentapis.com', '/b'); + + await plugin.flush(); + + expect(sendEventsSpy).toHaveBeenCalledTimes(1); + expect(sendEventsSpy).toHaveBeenCalledWith({ + url: expectedUrl, + writeKey: '123-456', + events: events.map((e) => ({ + ...e, + })), + }); + } + ); }); }); diff --git a/packages/core/src/timeline.ts b/packages/core/src/timeline.ts index a0c94aa4..172557ba 100644 --- a/packages/core/src/timeline.ts +++ b/packages/core/src/timeline.ts @@ -75,7 +75,11 @@ export class Timeline { if (key !== PluginType.destination) { if (result === undefined) { return; - } else if (key === PluginType.enrichment && pluginResult?.enrichment && typeof pluginResult.enrichment === 'function') { + } else if ( + key === PluginType.enrichment && + pluginResult?.enrichment && + typeof pluginResult.enrichment === 'function' + ) { result = pluginResult.enrichment(pluginResult); } else { result = pluginResult; diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index e58c64ef..595ffa35 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -149,15 +149,32 @@ export type Config = { storePersistorSaveDelay?: number; proxy?: string; cdnProxy?: string; + useSegmentEndpoints?: boolean; // Use if you want to use Segment endpoints errorHandler?: (error: SegmentError) => void; }; export type ClientMethods = { - screen: (name: string, properties?: JsonMap, enrichment?: EnrichmentClosure) => Promise; - track: (event: string, properties?: JsonMap, enrichment?: EnrichmentClosure) => Promise; - identify: (userId?: string, userTraits?: UserTraits, enrichment?: EnrichmentClosure) => Promise; + screen: ( + name: string, + properties?: JsonMap, + enrichment?: EnrichmentClosure + ) => Promise; + track: ( + event: string, + properties?: JsonMap, + enrichment?: EnrichmentClosure + ) => Promise; + identify: ( + userId?: string, + userTraits?: UserTraits, + enrichment?: EnrichmentClosure + ) => Promise; flush: () => Promise; - group: (groupId: string, groupTraits?: GroupTraits, enrichment?: EnrichmentClosure) => Promise; + group: ( + groupId: string, + groupTraits?: GroupTraits, + enrichment?: EnrichmentClosure + ) => Promise; alias: (newUserId: string, enrichment?: EnrichmentClosure) => Promise; reset: (resetAnonymousId?: boolean) => Promise; }; diff --git a/packages/core/src/util.ts b/packages/core/src/util.ts index c2e2ccca..47360005 100644 --- a/packages/core/src/util.ts +++ b/packages/core/src/util.ts @@ -257,3 +257,14 @@ export const createPromise = ( resolve: resolver!, }; }; + +export function getURL(host: string, path: string) { + if (path === '') { + path = '/'; // Ensure a trailing slash if path is empty + } + if (!host.startsWith('https://') && !host.startsWith('http://')) { + host = 'https://' + host; + } + const s = `${host}${path}`; + return s; +} diff --git a/packages/plugins/plugin-appsflyer/src/AppsflyerPlugin.tsx b/packages/plugins/plugin-appsflyer/src/AppsflyerPlugin.tsx index 49430e3e..4fa306b9 100644 --- a/packages/plugins/plugin-appsflyer/src/AppsflyerPlugin.tsx +++ b/packages/plugins/plugin-appsflyer/src/AppsflyerPlugin.tsx @@ -134,11 +134,23 @@ export class AppsflyerPlugin extends DestinationPlugin { } if (Boolean(is_first_launch) && JSON.parse(is_first_launch) === true) { if (af_status === 'Non-organic') { - this.analytics?.track('Install Attributed', properties).then(() => this.analytics?.logger.info("Sent Install Attributed event to Segment")); + this.analytics + ?.track('Install Attributed', properties) + .then(() => + this.analytics?.logger.info( + 'Sent Install Attributed event to Segment' + ) + ); } else { - this.analytics?.track('Organic Install', { - provider: 'AppsFlyer', - }).then(() => this.analytics?.logger.info("Sent Organic Install event to Segment")); + this.analytics + ?.track('Organic Install', { + provider: 'AppsFlyer', + }) + .then(() => + this.analytics?.logger.info( + 'Sent Organic Install event to Segment' + ) + ); } } });