diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2857c4870..a83508c95 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,8 @@ jobs: build-and-test: needs: cancel_previous runs-on: 'ubuntu-latest' + env: + YARN_ENABLE_HARDENED_MODE: 0 steps: - uses: actions/checkout@v4 # Workaround for corepack enable in node @@ -33,7 +35,6 @@ jobs: run: yarn install --immutable - name: Build run: yarn build - # Linter has to run after the build because it relies on TS types - name: Lint run: yarn lint - name: Test @@ -41,6 +42,8 @@ jobs: run-e2e-ios: runs-on: 'macos-13' + env: + YARN_ENABLE_HARDENED_MODE: 0 steps: - uses: maxim-lobanov/setup-xcode@v1 with: @@ -76,17 +79,19 @@ jobs: run: yarn e2e test:ios run-e2e-android: - runs-on: 'macos-latest' # This is important, linux cannot run the emulator graphically for e2e tests + runs-on: 'macos-13' # This is important, linux cannot run the emulator graphically for e2e tests strategy: matrix: api-level: [21] profile: ['pixel_xl'] + env: + YARN_ENABLE_HARDENED_MODE: 0 steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: - distribution: 'adopt' - java-version: '11' + distribution: 'temurin' + java-version: '17' cache: 'gradle' - name: Gradle cache @@ -112,7 +117,7 @@ jobs: with: api-level: ${{ matrix.api-level }} profile: ${{matrix.profile}} - name: Pixel_API_21_AOSP + avd-name: Pixel_API_21_AOSP target: default force-avd-creation: false emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none diff --git a/.github/workflows/create_jira.yml b/.github/workflows/create_jira.yml index 03f8e658b..16e5a4596 100644 --- a/.github/workflows/create_jira.yml +++ b/.github/workflows/create_jira.yml @@ -24,7 +24,7 @@ jobs: id: create uses: atlassian/gajira-create@master with: - project: LIBWEB + project: LIBRARIES issuetype: Bug summary: | [${{ github.event.repository.name }}] (${{ github.event.issue.number }}): ${{ github.event.issue.title }} @@ -33,8 +33,8 @@ jobs: ${{ github.event.issue.body }} # Parent and Epic Link fields (set to same) fields: '{ - "parent": {"key": "LIBWEB-1530"}, - "customfield_10002": "LIBWEB-1530" + "parent": {"key": "LIBRARIES-2048"}, + "customfield_10002": "LIBRARIES-2048" }' - name: Log created issue diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 390a3112e..f19960e67 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -3,7 +3,7 @@ name: Publish on: workflow_dispatch: secrets: - GITHUB_TOKEN: + GH_TOKEN: required: true NPM_TOKEN: required: true @@ -55,13 +55,3 @@ jobs: yarn install --no-immutable yarn e2e install --no-immutable yarn example install --no-immutable - - - name: Commit Updated App Locks - run: | - git config user.email "no-reply@segment.com" - git config user.name "semantic-release-bot" - git add yarn.lock - git add examples/E2E/yarn.lock - git add examples/AnalyticsReactNativeExample/yarn.lock - git commit -m "chore(release): update lockfiles [skip ci]" --no-verify - git push diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 12c6f6ba1..80ea98492 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,17 @@ We want this community to be friendly and respectful to each other. Please follow it in all your interactions with the project. +## Prerequisites + +Follow the official guide for getting your [RN Environment setup](https://reactnative.dev/docs/0.72/environment-setup) + +React Native requires different versions of the tools/languages you might be using already. Even among RN releases there might be different versions required. We recommend using the following tools to manage your toolsets: + +- [Xcodes](https://github.com/XcodesOrg/XcodesApp) + - To manage different releases of Xcode. The latest release of RN is usually supported by the latest Xcode release but previous releases might not. +- [Mise](https://mise.jdx.dev/dev-tools/) or [ASDF](https://asdf-vm.com/guide/getting-started.html) for everything else + - Node, Ruby and Java version support might change amongst RN releases. These version managers let you manage multiple versions of them. + ## Development workflow To get started with the project, run `yarn bootstrap` in the root directory to install the required dependencies for each package: @@ -11,7 +22,7 @@ yarn bootstrap ``` While developing, you can run the [example app](/example/) to test your changes. - +code To start the packager: ```sh @@ -52,16 +63,16 @@ yarn test The are also end-to-end tests. First you will have to build the app and then run the tests: ``` -# Start the server (*note there's a separate e2e command* -yarn example start:e2e +# Start the server (*note there's a separate e2e command*) +yarn e2e start:e2e # iOS -yarn example e2e:build:ios -yarn example e2e:test:ios +yarn e2e e2e:build:ios +yarn e2e e2e:test:ios # Android -yarn example e2e:build:android -yarn example e2e:test:android +yarn e2e e2e:build:android +yarn e2e e2e:test:android ``` To edit the Objective-C / Swift files, open `example/ios/AnalyticsReactNativeExample.xcworkspace` in XCode and find the source files at `Pods > Development Pods > @segment/analytics-react-native`. @@ -100,12 +111,12 @@ The `package.json` file contains various scripts for common tasks: - `yarn example start`: start the Metro server for the example app. - `yarn example android`: run the example app on Android. - `yarn example ios`: run the example app on iOS. -- `yarn example e2e:build:ios`: builds the example app using detox -- `yarn example e2e:test:ios`: runs the e2e on a simulator(headless if not ran manually) -- `yarn example e2e:build:android`: builds the example app using detox -- `yarn example e2e:test:android`: runs the e2e on an emulator -- `yarn example ios:deeplink`: opens the ios app via deep link (example app must already be installed) -- `yarn example android:deeplink`: opens the Android app via deep link (example app must already be installed) +- `yarn e2e e2e:build:ios`: builds the e2e app using detox +- `yarn e2e e2e:test:ios`: runs the e2e on a simulator(headless if not ran manually) +- `yarn e2e e2e:build:android`: builds the e2e app using detox +- `yarn e2e e2e:test:android`: runs the e2e on an emulator +- `yarn e2e ios:deeplink`: opens the ios app via deep link (example app must already be installed) +- `yarn e2e android:deeplink`: opens the Android app via deep link (example app must already be installed) ### Sending a pull request @@ -118,3 +129,13 @@ When you're sending a pull request: - Review the documentation to make sure it looks good. - Follow the pull request template when opening a pull request. - For pull requests that change the API or implementation, discuss with maintainers first by opening an issue. + +## Release + +Release is automated in GHA. By default `yarn release` won't let you trigger a release from your personal computer. + +To trigger a release go to Actions. Select the `Publish` workflow and trigger a new job. + +Automatically the workflow will analyze the commits, bump versions, create changesets, build and release to NPM the packages that need so. + +The CI/CD is automated using [semantic-release](https://github.com/semantic-release/semantic-release). diff --git a/README.md b/README.md index 36bec10bc..f3c9da0ba 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,12 @@ The hassle-free way to add Segment analytics to your React-Native app. - [Automatic screen tracking](#automatic-screen-tracking) - [React Navigation](#react-navigation) - [React Native Navigation](#react-native-navigation) + - [Consent Management](#consent-management) + - [Segment CMP](#segment-managed-cmp) + - [Event Stamping](#event-stamping) + - [Segment Consent Preference Updated Event](#segment-consent-preference-updated-event) + - [Event Flow](#event-flow) + - [Getting Started](#getting-started) - [Plugins + Timeline architecture](#plugins--timeline-architecture) - [Plugin Types](#plugin-types) - [Destination Plugins](#destination-plugins) @@ -152,6 +158,20 @@ To track deep links in iOS you must add the following to your `AppDelegate.m` fi return YES; } ``` + +If you are using Expo, you need to create an [AppDelegateSubscriber](https://docs.expo.dev/modules/appdelegate-subscribers/), make sure to include `segment_analytics_react_native` in your `podspec` file. +The rest of the code looks like this: +```swift + import segment_analytics_react_native + + ... + + open func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + AnalyticsReactNative.trackDeepLink(url: url as NSURL, options: options) + return false + } +``` + ### Native AnonymousId If you need to generate an `anonymousId` either natively or before the Analytics React Native package is initialized, you can send the anonymousId value from native code. The value has to be generated and stored by the caller. For reference, you can find a working example in the app and reference the code below: @@ -379,10 +399,12 @@ The reset method clears the internal state of the library for the current user a Note: Each time you call reset, a new AnonymousId is generated automatically. +And when false is passed as an argument in reset method, it will skip resetting the anonymousId (but reset the rest of the user date). + Method signature: ```js -reset: () => void; +reset: (resetAnonymousId = true) => void; ``` Example usage: @@ -391,6 +413,8 @@ Example usage: const { reset } = useAnalytics(); reset(); + +reset(resetAnonymousId = false); ``` ### Flush @@ -481,6 +505,164 @@ Navigation.events().registerComponentDidAppearListener(({ componentName }) => { }); ``` +## Consent Management + +Consent Management is the management of a user’s consent preferences related to privacy. You might be familiar with the Privacy Pop-ups that have become mandated recently that ask the user if he or she consents to the use of certain category of cookies: + +![Sample CMP UI](imgs/cmp-sample.png?raw=true "Sample CMP UI") + + +The Privacy pop-up asks the user if he or she will consent to the use of cookies and allows the user to customize their consent by turning on/off different categories of cookies. + +After the user selects “Allow All” or “Save Preferences” a callback is fired and the owner of the website is notified as to the consent preferences of a given user. The website owner must then store that consent preference and abide by it. Any rejected cookies must not be set or read to avoid large fines that can be handed down by government authorities. + +Additionally, besides the initial pop-up the website owner must give users a way to later change any preferences they originally selected. This is usually accomplished by providing a link to display the customization screen. + + +### Segment managed CMP + +Segment provides a framework for users to integrate any CMP they choose and use the Segment web app to map consent categories to device mode destinations. This information is sent down the analytics-kotlin SDK and stored for later lookup. + +Every event that flows through the library will be stamped with the current status according to whatever configured CMP is used. Event stamping is handled by the ConsentManagementPlugin. + +Using consent status stamped on the events and the mappings sent down from the Segment web app each event is evaluated and action is taken. Currently the supported actions are: + +- Blocking - This action is implemented by the ConsentBlockingPlugin + +### Event Stamping + +Event stamping is the process of adding the consent status information to an existing event. The information is added to the context object of every event. Below is a before and after example: + +Before + +```json +{ + "anonymousId": "23adfd82-aa0f-45a7-a756-24f2a7a4c895", + "type": "track", + "event": "MyEvent", + "userId": "u123", + "timestamp": "2023-01-01T00:00:00.000Z", + "context": { + "traits": { + "email": "peter@example.com", + "phone": "555-555-5555" + }, + "device": { + "advertisingId": "7A3CBBA0-BDF5-11E4-8DFC-AA02A5B093DB" + } + } +} +``` +After + +```json +{ + "anonymousId": "23adfd82-aa0f-45a7-a756-24f2a7a4c895", + "type": "track", + "event": "MyEvent", + "userId": "u123", + "timestamp": "2023-01-01T00:00:00.000Z", + "context": { + "traits": { + "email": "peter@example.com", + "phone": "555-555-5555" + }, + "device": { + "advertisingId": "7A3CBBA0-BDF5-11E4-8DFC-AA02A5B093DB" + }, + "consent": { + "categoryPreferences": { + "Advertising": true, + "Analytics": false, + "Functional": true, + "DataSharing": false + } + } + } +} +``` + +### Segment Consent Preference Updated Event + +When notified by the CMP SDK that consent has changed, a track event with name “Segment Consent Preference Updated” will be emitted. Below is example of what that event will look like: + +```json +{ + "anonymousId": "23adfd82-aa0f-45a7-a756-24f2a7a4c895", + "type": "track", + "event": "Segment Consent Preference Updated", + "userId": "u123", + "timestamp": "2023-01-01T00:00:00.000Z", + "context": { + "device": { + "advertisingId": "7A3CBEA0-BDF5-11E4-8DFC-AA07A5B093DB" + }, + "consent": { + "categoryPreferences": { + "Advertising": true, + "Analytics": false, + "Functional": true, + "DataSharing": false + } + } + } +} +``` + +### Event Flow + + +![Shows how an event is stamped and later checked for consent](imgs/main-flow-diagram.png?raw=true "Event Flow Diagram") + +1. An event is dropped onto the timeline by some tracking call. +2. The ConsentManagementPlugin consumes the event, stamps it, and returns it. +3. The event is now stamped with consent information from this point forward. +4. The event is copied. The copy is consumed by a Destination Plugin and continues down its internal timeline. The original event is returned and continues down the main timeline. + a. The stamped event is now on the timeline of the destination plugin. + b. The event reaches the ConsentBlockingPlugin which makes a decision as to whether or not to let the event continue down the timeline. + c. If the event has met the consent requirements it continues down the timeline. +5. The event continues down the timeline. + +### Getting Started + +1. Since the Consent Management Plugin is built into the core `Analytics-React-Native` SDK, you can simply import it and begin using it without adding any additional Segment dependencies. + +``` +import { createClient, ConsentPlugin} from '@segment/analytics-react-native'; +``` +2. From here, you will have to build an Consent Provider integration with your CMP. You can reference our example `OneTrust` [integration here](https://github.com/segmentio/analytics-react-native/tree/master/packages/plugins/plugin-onetrust). It is not possible for Segment to support this as an active plugin as OneTrust requires you to use very specific versions of their SDK. However, the functionality is usually unchanged across versions so the example linked above should be almost copy/paste. If you build your own, it needs to imlpement the `CategoryConsentProvider` interface: + +``` +interface CategoryConsentStatusProvider { + setApplicableCategories(categories: string[]): void; + getConsentStatus(): Promise>; + onConsentChange(cb: (updConsent: Record) => void): void; + shutdown?(): void; +} +``` + +3. Add the Consent Provider to the `ConsentPlugin()` and add `ConsentPlugin()` to the `client`. A full example of this setup, including initializing the `OneTrust` SDK can be [found here](https://github.com/segmentio/analytics-react-native/blob/master/packages/plugins/plugin-onetrust/README.md). + + +``` +const segment = createClient({ + writeKey: 'SEGMENT_KEY', + ... +}); + + +const myCustomProvider = new MyCustomProvider(MyCMP) +const consentPlugin = new ConsentPlugin(myCustomProvider); + +segment.add({ plugin: oneTrustPlugin }); +``` + +4. Once the Segment Client and third-party CMP have been initialized, start processing queued events + +``` +consentPlugin.start() +``` + ## Plugins + Timeline architecture You have complete control over how the events are processed before being uploaded to the Segment API. diff --git a/examples/AnalyticsReactNativeExample/ios/AnalyticsReactNativeExample.xcodeproj/project.pbxproj b/examples/AnalyticsReactNativeExample/ios/AnalyticsReactNativeExample.xcodeproj/project.pbxproj index 0b451192f..ddcd99568 100644 --- a/examples/AnalyticsReactNativeExample/ios/AnalyticsReactNativeExample.xcodeproj/project.pbxproj +++ b/examples/AnalyticsReactNativeExample/ios/AnalyticsReactNativeExample.xcodeproj/project.pbxproj @@ -486,6 +486,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; INFOPLIST_FILE = AnalyticsReactNativeExample/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -513,6 +514,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = AnalyticsReactNativeExample/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -600,11 +602,7 @@ "-DFOLLY_MOBILE=1", "-DFOLLY_USE_LIBCPP=1", ); - OTHER_LDFLAGS = ( - "$(inherited)", - "-Wl", - "-ld_classic", - ); + OTHER_LDFLAGS = "$(inherited)"; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; }; @@ -674,11 +672,7 @@ "-DFOLLY_MOBILE=1", "-DFOLLY_USE_LIBCPP=1", ); - OTHER_LDFLAGS = ( - "$(inherited)", - "-Wl", - "-ld_classic", - ); + OTHER_LDFLAGS = "$(inherited)"; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; diff --git a/examples/AnalyticsReactNativeExample/ios/AnalyticsReactNativeExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/examples/AnalyticsReactNativeExample/ios/AnalyticsReactNativeExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/examples/AnalyticsReactNativeExample/ios/AnalyticsReactNativeExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/examples/AnalyticsReactNativeExample/ios/Podfile.lock b/examples/AnalyticsReactNativeExample/ios/Podfile.lock index aaae785d6..50e2099e8 100644 --- a/examples/AnalyticsReactNativeExample/ios/Podfile.lock +++ b/examples/AnalyticsReactNativeExample/ios/Podfile.lock @@ -499,11 +499,11 @@ PODS: - RNScreens (3.27.0): - RCT-Folly (= 2021.07.22.00) - React-Core - - segment-analytics-react-native (2.18.0): + - segment-analytics-react-native (2.19.3): - React-Core - sovran-react-native - SocketRocket (0.6.1) - - sovran-react-native (1.1.0): + - sovran-react-native (1.1.2): - React-Core - Yoga (1.14.0) - YogaKit (1.18.1): @@ -752,12 +752,12 @@ SPEC CHECKSUMS: RNCMaskedView: 0e1bc4bfa8365eba5fbbb71e07fbdc0555249489 RNGestureHandler: 32a01c29ecc9bb0b5bf7bc0a33547f61b4dc2741 RNScreens: 3c2d122f5e08c192e254c510b212306da97d2581 - segment-analytics-react-native: abcd50e51633527c8b116a8b14780e8e4e5ad9d1 + segment-analytics-react-native: a803de6a9406f7b18928d352962c2f49663a0869 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 - sovran-react-native: 26dc5e42e618e311bb93d6e825e80af1934b8887 + sovran-react-native: 5f02bd2d111ffe226d00c7b0435290eae6f10934 Yoga: eddf2bbe4a896454c248a8f23b4355891eb720a6 YogaKit: f782866e155069a2cca2517aafea43200b01fd5a PODFILE CHECKSUM: 329f06ebb76294acf15c298d0af45530e2797740 -COCOAPODS: 1.14.3 +COCOAPODS: 1.11.3 diff --git a/examples/AnalyticsReactNativeExample/ios/PrivacyInfo.xcprivacy b/examples/AnalyticsReactNativeExample/ios/PrivacyInfo.xcprivacy new file mode 100644 index 000000000..c8fd0beb3 --- /dev/null +++ b/examples/AnalyticsReactNativeExample/ios/PrivacyInfo.xcprivacy @@ -0,0 +1,70 @@ + + + + + NSPrivacyTracking + + NSPrivacyCollectedDataTypes + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeDeviceID + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + + + + + NSPrivacyCollectedDataType + App Name + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeDeveloperAdvertising + + + + NSPrivacyCollectedDataType + App Version + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeDeveloperAdvertising + + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeAdvertisingData + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeDeveloperAdvertising + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + 1C8F.1 + + + + + \ No newline at end of file diff --git a/examples/AnalyticsReactNativeExample/yarn.lock b/examples/AnalyticsReactNativeExample/yarn.lock index fc0619b3c..8ca35253e 100644 --- a/examples/AnalyticsReactNativeExample/yarn.lock +++ b/examples/AnalyticsReactNativeExample/yarn.lock @@ -3689,13 +3689,13 @@ __metadata: linkType: hard "fast-xml-parser@npm:^4.0.12": - version: 4.3.2 - resolution: "fast-xml-parser@npm:4.3.2" + version: 4.4.1 + resolution: "fast-xml-parser@npm:4.4.1" dependencies: strnum: "npm:^1.0.5" bin: fxparser: src/cli/cli.js - checksum: 10c0/7c1611349384656ec4faa9802fbc8cf8c01206a1b79193d5cd54586307801562509007f6cf16e5da7d43da4fa4639770f38959a285b9466aa98dab0a9b8ca171 + checksum: 10c0/7f334841fe41bfb0bf5d920904ccad09cefc4b5e61eaf4c225bf1e1bb69ee77ef2147d8942f783ee8249e154d1ca8a858e10bda78a5d78b8bed3f48dcee9bf33 languageName: node linkType: hard @@ -4367,9 +4367,9 @@ __metadata: linkType: hard "ip@npm:^1.1.5": - version: 1.1.8 - resolution: "ip@npm:1.1.8" - checksum: 10c0/ab32a5ecfa678d4c158c1381c4c6744fce89a1d793e1b6635ba79d0753c069030b672d765887b6fff55670c711dfa47475895e5d6013efbbcf04687c51cb8db9 + version: 1.1.9 + resolution: "ip@npm:1.1.9" + checksum: 10c0/5af58bfe2110c9978acfd77a2ffcdf9d33a6ce1c72f49edbaf16958f7a8eb979b5163e43bb18938caf3aaa55cdacde4e470874c58ca3b4b112ea7a30461a0c27 languageName: node linkType: hard @@ -7438,8 +7438,8 @@ __metadata: linkType: hard "tar@npm:^6.1.11, tar@npm:^6.1.2": - version: 6.2.0 - resolution: "tar@npm:6.2.0" + version: 6.2.1 + resolution: "tar@npm:6.2.1" dependencies: chownr: "npm:^2.0.0" fs-minipass: "npm:^2.0.0" @@ -7447,7 +7447,7 @@ __metadata: minizlib: "npm:^2.1.1" mkdirp: "npm:^1.0.3" yallist: "npm:^4.0.0" - checksum: 10c0/02ca064a1a6b4521fef88c07d389ac0936730091f8c02d30ea60d472e0378768e870769ab9e986d87807bfee5654359cf29ff4372746cc65e30cbddc352660d8 + checksum: 10c0/a5eca3eb50bc11552d453488344e6507156b9193efd7635e98e867fab275d527af53d8866e2370cd09dfe74378a18111622ace35af6a608e5223a7d27fe99537 languageName: node linkType: hard diff --git a/examples/E2E-73/.mise.toml b/examples/E2E-73/.mise.toml new file mode 100644 index 000000000..a34d84035 --- /dev/null +++ b/examples/E2E-73/.mise.toml @@ -0,0 +1,5 @@ +[tools] +java = "zulu-17" +node = "18" +ruby = '3.3.0' +cocoapods = "latest" \ No newline at end of file diff --git a/examples/E2E-73/.rtx.toml b/examples/E2E-73/.rtx.toml deleted file mode 100644 index 00f4792ba..000000000 --- a/examples/E2E-73/.rtx.toml +++ /dev/null @@ -1,2 +0,0 @@ -[tools] -java = "zulu-17" diff --git a/examples/E2E-73/package.json b/examples/E2E-73/package.json index 1931d1f17..ad95047c7 100644 --- a/examples/E2E-73/package.json +++ b/examples/E2E-73/package.json @@ -46,7 +46,7 @@ "babel-plugin-module-resolver": "^5.0.0", "detox": "^20.17.0", "eslint": "^8.19.0", - "express": "^4.17.1", + "express": "^4.19.2", "jest": "^29.7.0", "jest-circus": "^29.3.1", "prettier": "2.8.8", diff --git a/examples/E2E-73/yarn.lock b/examples/E2E-73/yarn.lock index 5871211d4..88663644b 100644 --- a/examples/E2E-73/yarn.lock +++ b/examples/E2E-73/yarn.lock @@ -3088,7 +3088,7 @@ __metadata: babel-plugin-module-resolver: "npm:^5.0.0" detox: "npm:^20.17.0" eslint: "npm:^8.19.0" - express: "npm:^4.17.1" + express: "npm:^4.19.2" jest: "npm:^29.7.0" jest-circus: "npm:^29.3.1" prettier: "npm:2.8.8" @@ -3660,12 +3660,12 @@ __metadata: languageName: node linkType: hard -"body-parser@npm:1.20.1": - version: 1.20.1 - resolution: "body-parser@npm:1.20.1" +"body-parser@npm:1.20.2": + version: 1.20.2 + resolution: "body-parser@npm:1.20.2" dependencies: bytes: "npm:3.1.2" - content-type: "npm:~1.0.4" + content-type: "npm:~1.0.5" debug: "npm:2.6.9" depd: "npm:2.0.0" destroy: "npm:1.2.0" @@ -3673,10 +3673,10 @@ __metadata: iconv-lite: "npm:0.4.24" on-finished: "npm:2.4.1" qs: "npm:6.11.0" - raw-body: "npm:2.5.1" + raw-body: "npm:2.5.2" type-is: "npm:~1.6.18" unpipe: "npm:1.0.0" - checksum: 10c0/a202d493e2c10a33fb7413dac7d2f713be579c4b88343cd814b6df7a38e5af1901fc31044e04de176db56b16d9772aa25a7723f64478c20f4d91b1ac223bf3b8 + checksum: 10c0/06f1438fff388a2e2354c96aa3ea8147b79bfcb1262dfcc2aae68ec13723d01d5781680657b74e9f83c808266d5baf52804032fbde2b7382b89bd8cdb273ace9 languageName: node linkType: hard @@ -4258,7 +4258,7 @@ __metadata: languageName: node linkType: hard -"content-type@npm:~1.0.4": +"content-type@npm:~1.0.4, content-type@npm:~1.0.5": version: 1.0.5 resolution: "content-type@npm:1.0.5" checksum: 10c0/b76ebed15c000aee4678c3707e0860cb6abd4e680a598c0a26e17f0bfae723ec9cc2802f0ff1bc6e4d80603719010431d2231018373d4dde10f9ccff9dadf5af @@ -4279,10 +4279,10 @@ __metadata: languageName: node linkType: hard -"cookie@npm:0.5.0": - version: 0.5.0 - resolution: "cookie@npm:0.5.0" - checksum: 10c0/c01ca3ef8d7b8187bae434434582288681273b5a9ed27521d4d7f9f7928fe0c920df0decd9f9d3bbd2d14ac432b8c8cf42b98b3bdd5bfe0e6edddeebebe8b61d +"cookie@npm:0.6.0": + version: 0.6.0 + resolution: "cookie@npm:0.6.0" + checksum: 10c0/f2318b31af7a31b4ddb4a678d024514df5e705f9be5909a192d7f116cfb6d45cbacf96a473fa733faa95050e7cff26e7832bb3ef94751592f1387b71c8956686 languageName: node linkType: hard @@ -5193,16 +5193,16 @@ __metadata: languageName: node linkType: hard -"express@npm:^4.17.1": - version: 4.18.2 - resolution: "express@npm:4.18.2" +"express@npm:^4.19.2": + version: 4.19.2 + resolution: "express@npm:4.19.2" dependencies: accepts: "npm:~1.3.8" array-flatten: "npm:1.1.1" - body-parser: "npm:1.20.1" + body-parser: "npm:1.20.2" content-disposition: "npm:0.5.4" content-type: "npm:~1.0.4" - cookie: "npm:0.5.0" + cookie: "npm:0.6.0" cookie-signature: "npm:1.0.6" debug: "npm:2.6.9" depd: "npm:2.0.0" @@ -5228,7 +5228,7 @@ __metadata: type-is: "npm:~1.6.18" utils-merge: "npm:1.0.1" vary: "npm:~1.1.2" - checksum: 10c0/75af556306b9241bc1d7bdd40c9744b516c38ce50ae3210658efcbf96e3aed4ab83b3432f06215eae5610c123bc4136957dc06e50dfc50b7d4d775af56c4c59c + checksum: 10c0/e82e2662ea9971c1407aea9fc3c16d6b963e55e3830cd0ef5e00b533feda8b770af4e3be630488ef8a752d7c75c4fcefb15892868eeaafe7353cb9e3e269fdcb languageName: node linkType: hard @@ -6079,9 +6079,9 @@ __metadata: linkType: hard "ip@npm:^1.1.5": - version: 1.1.8 - resolution: "ip@npm:1.1.8" - checksum: 10c0/ab32a5ecfa678d4c158c1381c4c6744fce89a1d793e1b6635ba79d0753c069030b672d765887b6fff55670c711dfa47475895e5d6013efbbcf04687c51cb8db9 + version: 1.1.9 + resolution: "ip@npm:1.1.9" + checksum: 10c0/5af58bfe2110c9978acfd77a2ffcdf9d33a6ce1c72f49edbaf16958f7a8eb979b5163e43bb18938caf3aaa55cdacde4e470874c58ca3b4b112ea7a30461a0c27 languageName: node linkType: hard @@ -8772,15 +8772,15 @@ __metadata: languageName: node linkType: hard -"raw-body@npm:2.5.1": - version: 2.5.1 - resolution: "raw-body@npm:2.5.1" +"raw-body@npm:2.5.2": + version: 2.5.2 + resolution: "raw-body@npm:2.5.2" dependencies: bytes: "npm:3.1.2" http-errors: "npm:2.0.0" iconv-lite: "npm:0.4.24" unpipe: "npm:1.0.0" - checksum: 10c0/5dad5a3a64a023b894ad7ab4e5c7c1ce34d3497fc7138d02f8c88a3781e68d8a55aa7d4fd3a458616fa8647cc228be314a1c03fb430a07521de78b32c4dd09d2 + checksum: 10c0/b201c4b66049369a60e766318caff5cb3cc5a900efd89bdac431463822d976ad0670912c931fdbdcf5543207daf6f6833bca57aa116e1661d2ea91e12ca692c4 languageName: node linkType: hard @@ -9923,8 +9923,8 @@ __metadata: linkType: hard "tar@npm:^6.1.11, tar@npm:^6.1.2": - version: 6.2.0 - resolution: "tar@npm:6.2.0" + version: 6.2.1 + resolution: "tar@npm:6.2.1" dependencies: chownr: "npm:^2.0.0" fs-minipass: "npm:^2.0.0" @@ -9932,7 +9932,7 @@ __metadata: minizlib: "npm:^2.1.1" mkdirp: "npm:^1.0.3" yallist: "npm:^4.0.0" - checksum: 10c0/02ca064a1a6b4521fef88c07d389ac0936730091f8c02d30ea60d472e0378768e870769ab9e986d87807bfee5654359cf29ff4372746cc65e30cbddc352660d8 + checksum: 10c0/a5eca3eb50bc11552d453488344e6507156b9193efd7635e98e867fab275d527af53d8866e2370cd09dfe74378a18111622ace35af6a608e5223a7d27fe99537 languageName: node linkType: hard diff --git a/examples/E2E/.mise.toml b/examples/E2E/.mise.toml new file mode 100644 index 000000000..24e432f19 --- /dev/null +++ b/examples/E2E/.mise.toml @@ -0,0 +1,5 @@ +[tools] +java = "zulu-11" +node = "18" +ruby = '3.3.0' +cocoapods = "latest" \ No newline at end of file diff --git a/examples/E2E/ios/Podfile.lock b/examples/E2E/ios/Podfile.lock index 8ddba76fb..36c20f909 100644 --- a/examples/E2E/ios/Podfile.lock +++ b/examples/E2E/ios/Podfile.lock @@ -499,11 +499,11 @@ PODS: - RNScreens (3.27.0): - RCT-Folly (= 2021.07.22.00) - React-Core - - segment-analytics-react-native (2.18.0): + - segment-analytics-react-native (2.19.2): - React-Core - sovran-react-native - SocketRocket (0.6.1) - - sovran-react-native (1.1.0): + - sovran-react-native (1.1.1): - React-Core - Yoga (1.14.0) - YogaKit (1.18.1): @@ -752,12 +752,12 @@ SPEC CHECKSUMS: RNCMaskedView: 0e1bc4bfa8365eba5fbbb71e07fbdc0555249489 RNGestureHandler: 32a01c29ecc9bb0b5bf7bc0a33547f61b4dc2741 RNScreens: 3c2d122f5e08c192e254c510b212306da97d2581 - segment-analytics-react-native: abcd50e51633527c8b116a8b14780e8e4e5ad9d1 + segment-analytics-react-native: 962494a9edbe3f6c5829e3b471484c85c304601c SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 - sovran-react-native: 26dc5e42e618e311bb93d6e825e80af1934b8887 + sovran-react-native: e6a9c963a8a6b9ebc3563394c39c30f33ab1453f Yoga: eddf2bbe4a896454c248a8f23b4355891eb720a6 YogaKit: f782866e155069a2cca2517aafea43200b01fd5a PODFILE CHECKSUM: 9d352ca8db1e31a063d2585ed47fdadabf87fe90 -COCOAPODS: 1.14.3 +COCOAPODS: 1.15.2 diff --git a/examples/E2E/package.json b/examples/E2E/package.json index b4f829adb..59421d276 100644 --- a/examples/E2E/package.json +++ b/examples/E2E/package.json @@ -47,7 +47,7 @@ "body-parser": "^1.20.0", "detox": "^20.17.0", "eslint": "^8.19.0", - "express": "^4.17.1", + "express": "^4.19.2", "jest": "^29.7.0", "jest-circus": "^29.3.1", "metro-react-native-babel-preset": "0.76.8", diff --git a/examples/E2E/yarn.lock b/examples/E2E/yarn.lock index 53dcb3c54..e50ee2c96 100644 --- a/examples/E2E/yarn.lock +++ b/examples/E2E/yarn.lock @@ -2962,7 +2962,7 @@ __metadata: body-parser: "npm:^1.20.0" detox: "npm:^20.17.0" eslint: "npm:^8.19.0" - express: "npm:^4.17.1" + express: "npm:^4.19.2" jest: "npm:^29.7.0" jest-circus: "npm:^29.3.1" metro-react-native-babel-preset: "npm:0.76.8" @@ -3543,27 +3543,7 @@ __metadata: languageName: node linkType: hard -"body-parser@npm:1.20.1": - version: 1.20.1 - resolution: "body-parser@npm:1.20.1" - dependencies: - bytes: "npm:3.1.2" - content-type: "npm:~1.0.4" - debug: "npm:2.6.9" - depd: "npm:2.0.0" - destroy: "npm:1.2.0" - http-errors: "npm:2.0.0" - iconv-lite: "npm:0.4.24" - on-finished: "npm:2.4.1" - qs: "npm:6.11.0" - raw-body: "npm:2.5.1" - type-is: "npm:~1.6.18" - unpipe: "npm:1.0.0" - checksum: 10c0/a202d493e2c10a33fb7413dac7d2f713be579c4b88343cd814b6df7a38e5af1901fc31044e04de176db56b16d9772aa25a7723f64478c20f4d91b1ac223bf3b8 - languageName: node - linkType: hard - -"body-parser@npm:^1.20.0": +"body-parser@npm:1.20.2, body-parser@npm:^1.20.0": version: 1.20.2 resolution: "body-parser@npm:1.20.2" dependencies: @@ -4161,10 +4141,10 @@ __metadata: languageName: node linkType: hard -"cookie@npm:0.5.0": - version: 0.5.0 - resolution: "cookie@npm:0.5.0" - checksum: 10c0/c01ca3ef8d7b8187bae434434582288681273b5a9ed27521d4d7f9f7928fe0c920df0decd9f9d3bbd2d14ac432b8c8cf42b98b3bdd5bfe0e6edddeebebe8b61d +"cookie@npm:0.6.0": + version: 0.6.0 + resolution: "cookie@npm:0.6.0" + checksum: 10c0/f2318b31af7a31b4ddb4a678d024514df5e705f9be5909a192d7f116cfb6d45cbacf96a473fa733faa95050e7cff26e7832bb3ef94751592f1387b71c8956686 languageName: node linkType: hard @@ -5075,16 +5055,16 @@ __metadata: languageName: node linkType: hard -"express@npm:^4.17.1": - version: 4.18.2 - resolution: "express@npm:4.18.2" +"express@npm:^4.19.2": + version: 4.19.2 + resolution: "express@npm:4.19.2" dependencies: accepts: "npm:~1.3.8" array-flatten: "npm:1.1.1" - body-parser: "npm:1.20.1" + body-parser: "npm:1.20.2" content-disposition: "npm:0.5.4" content-type: "npm:~1.0.4" - cookie: "npm:0.5.0" + cookie: "npm:0.6.0" cookie-signature: "npm:1.0.6" debug: "npm:2.6.9" depd: "npm:2.0.0" @@ -5110,7 +5090,7 @@ __metadata: type-is: "npm:~1.6.18" utils-merge: "npm:1.0.1" vary: "npm:~1.1.2" - checksum: 10c0/75af556306b9241bc1d7bdd40c9744b516c38ce50ae3210658efcbf96e3aed4ab83b3432f06215eae5610c123bc4136957dc06e50dfc50b7d4d775af56c4c59c + checksum: 10c0/e82e2662ea9971c1407aea9fc3c16d6b963e55e3830cd0ef5e00b533feda8b770af4e3be630488ef8a752d7c75c4fcefb15892868eeaafe7353cb9e3e269fdcb languageName: node linkType: hard @@ -5945,9 +5925,9 @@ __metadata: linkType: hard "ip@npm:^1.1.5": - version: 1.1.8 - resolution: "ip@npm:1.1.8" - checksum: 10c0/ab32a5ecfa678d4c158c1381c4c6744fce89a1d793e1b6635ba79d0753c069030b672d765887b6fff55670c711dfa47475895e5d6013efbbcf04687c51cb8db9 + version: 1.1.9 + resolution: "ip@npm:1.1.9" + checksum: 10c0/5af58bfe2110c9978acfd77a2ffcdf9d33a6ce1c72f49edbaf16958f7a8eb979b5163e43bb18938caf3aaa55cdacde4e470874c58ca3b4b112ea7a30461a0c27 languageName: node linkType: hard @@ -8730,18 +8710,6 @@ __metadata: languageName: node linkType: hard -"raw-body@npm:2.5.1": - version: 2.5.1 - resolution: "raw-body@npm:2.5.1" - dependencies: - bytes: "npm:3.1.2" - http-errors: "npm:2.0.0" - iconv-lite: "npm:0.4.24" - unpipe: "npm:1.0.0" - checksum: 10c0/5dad5a3a64a023b894ad7ab4e5c7c1ce34d3497fc7138d02f8c88a3781e68d8a55aa7d4fd3a458616fa8647cc228be314a1c03fb430a07521de78b32c4dd09d2 - languageName: node - linkType: hard - "raw-body@npm:2.5.2": version: 2.5.2 resolution: "raw-body@npm:2.5.2" @@ -9893,8 +9861,8 @@ __metadata: linkType: hard "tar@npm:^6.1.11, tar@npm:^6.1.2": - version: 6.2.0 - resolution: "tar@npm:6.2.0" + version: 6.2.1 + resolution: "tar@npm:6.2.1" dependencies: chownr: "npm:^2.0.0" fs-minipass: "npm:^2.0.0" @@ -9902,7 +9870,7 @@ __metadata: minizlib: "npm:^2.1.1" mkdirp: "npm:^1.0.3" yallist: "npm:^4.0.0" - checksum: 10c0/02ca064a1a6b4521fef88c07d389ac0936730091f8c02d30ea60d472e0378768e870769ab9e986d87807bfee5654359cf29ff4372746cc65e30cbddc352660d8 + checksum: 10c0/a5eca3eb50bc11552d453488344e6507156b9193efd7635e98e867fab275d527af53d8866e2370cd09dfe74378a18111622ace35af6a608e5223a7d27fe99537 languageName: node linkType: hard diff --git a/packages/core/package.json b/packages/core/package.json index a687b3cad..f37d375cc 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-react-native", - "version": "2.19.1", + "version": "2.19.4", "description": "The hassle-free way to add Segment analytics to your React-Native app.", "keywords": [ "segment", @@ -46,7 +46,8 @@ "prepack": "yarn prebuild" }, "dependencies": { - "@segment/tsub": "^2", + "@segment/tsub": "2.0.0", + "@stdlib/number-float64-base-normalize": "0.0.8", "deepmerge": "^4.3.1", "js-base64": "^3.7.5", "uuid": "^9.0.1" diff --git a/packages/core/src/__tests__/analytics.test.ts b/packages/core/src/__tests__/analytics.test.ts index f2bd05959..afe3c6b93 100644 --- a/packages/core/src/__tests__/analytics.test.ts +++ b/packages/core/src/__tests__/analytics.test.ts @@ -129,7 +129,6 @@ describe('SegmentClient', () => { it('resets all userInfo except anonymousId', async () => { client = new SegmentClient(clientArgs); const setUserInfo = jest.spyOn(store.userInfo, 'set'); - await client.reset(false); expect(setUserInfo).toHaveBeenCalledWith({ @@ -142,7 +141,6 @@ describe('SegmentClient', () => { it('resets user data, identity, traits', async () => { client = new SegmentClient(clientArgs); const setUserInfo = jest.spyOn(store.userInfo, 'set'); - await client.reset(); expect(setUserInfo).toHaveBeenCalledWith({ @@ -172,7 +170,7 @@ describe('SegmentClient', () => { }); describe('Flush Policies', () => { - it('creates the default flush policies when config is empty', () => { + it('creates the default flush policies when config is empty', async () => { client = new SegmentClient({ ...clientArgs, config: { @@ -181,11 +179,12 @@ describe('SegmentClient', () => { flushInterval: undefined, }, }); + await client.init(); const flushPolicies = client.getFlushPolicies(); expect(flushPolicies.length).toBe(2); }); - it('setting flush policies is mutually exclusive with flushAt/Interval', () => { + it('setting flush policies is mutually exclusive with flushAt/Interval', async () => { client = new SegmentClient({ ...clientArgs, config: { @@ -195,11 +194,12 @@ describe('SegmentClient', () => { flushPolicies: [new CountFlushPolicy(1)], }, }); + await client.init(); const flushPolicies = client.getFlushPolicies(); expect(flushPolicies.length).toBe(1); }); - it('setting flushAt/Interval to 0 should make the client have no uploads', () => { + it('setting flushAt/Interval to 0 should make the client have no uploads', async () => { client = new SegmentClient({ ...clientArgs, config: { @@ -208,11 +208,12 @@ describe('SegmentClient', () => { flushInterval: 0, }, }); + await client.init(); const flushPolicies = client.getFlushPolicies(); expect(flushPolicies.length).toBe(0); }); - it('setting an empty array of policies should make the client have no uploads', () => { + it('setting an empty array of policies should make the client have no uploads', async () => { client = new SegmentClient({ ...clientArgs, config: { @@ -222,11 +223,12 @@ describe('SegmentClient', () => { flushPolicies: [], }, }); + await client.init(); const flushPolicies = client.getFlushPolicies(); expect(flushPolicies.length).toBe(0); }); - it('can add and remove policies, does not mutate original array', () => { + it('can add and remove policies, does not mutate original array', async () => { const policies = [new CountFlushPolicy(1), new TimerFlushPolicy(200)]; client = new SegmentClient({ ...clientArgs, @@ -237,6 +239,7 @@ describe('SegmentClient', () => { flushPolicies: policies, }, }); + await client.init(); expect(client.getFlushPolicies().length).toBe(policies.length); client.removeFlushPolicy(...policies); diff --git a/packages/core/src/analytics.ts b/packages/core/src/analytics.ts index 2970277a4..eca237883 100644 --- a/packages/core/src/analytics.ts +++ b/packages/core/src/analytics.ts @@ -88,7 +88,12 @@ export class SegmentClient { private pluginsToAdd: Plugin[] = []; - private flushPolicyExecuter!: FlushPolicyExecuter; + private flushPolicyExecuter: FlushPolicyExecuter = new FlushPolicyExecuter( + [], + () => { + void this.flush(); + } + ); private onPluginAddedObservers: OnPluginAddedCallback[] = []; @@ -233,9 +238,6 @@ export class SegmentClient { // Setup platform specific plugins this.platformPlugins.forEach((plugin) => this.add({ plugin: plugin })); - // Start flush policies - this.setupFlushPolicies(); - // set up tracking for lifecycle events this.setupLifecycleEvents(); } @@ -278,9 +280,6 @@ export class SegmentClient { await this.onReady(); this.isReady.value = true; - - // flush any stored events - this.flushPolicyExecuter.manualFlush(); } catch (error) { this.reportInternalError( new SegmentError( @@ -494,13 +493,18 @@ export class SegmentClient { } } + // Start flush policies + // This should be done before any pending events are added to the queue so that any policies that rely on events queued can trigger accordingly + this.setupFlushPolicies(); + // Send all events in the queue const pending = await this.store.pendingEvents.get(true); for (const e of pending) { await this.startTimelineProcessing(e); await this.store.pendingEvents.remove(e); } - // this.store.pendingEvents.set([]); + + this.flushPolicyExecuter.manualFlush(); } async flush(): Promise { @@ -756,9 +760,9 @@ export class SegmentClient { } } - this.flushPolicyExecuter = new FlushPolicyExecuter(flushPolicies, () => { - void this.flush(); - }); + for (const fp of flushPolicies) { + this.flushPolicyExecuter.add(fp); + } } /** diff --git a/packages/core/src/flushPolicies/flush-policy-executer.ts b/packages/core/src/flushPolicies/flush-policy-executer.ts index b84dd43d8..a705cacf2 100644 --- a/packages/core/src/flushPolicies/flush-policy-executer.ts +++ b/packages/core/src/flushPolicies/flush-policy-executer.ts @@ -89,12 +89,12 @@ export class FlushPolicyExecuter { } private startPolicy(policy: FlushPolicy) { - policy.start(); const unsubscribe = policy.shouldFlush.onChange((shouldFlush) => { if (shouldFlush) { this.onFlush(); } }); this.observers.push(unsubscribe); + policy.start(); } } diff --git a/packages/core/src/flushPolicies/startup-flush-policy.ts b/packages/core/src/flushPolicies/startup-flush-policy.ts index fdce72cae..7d29a4ec2 100644 --- a/packages/core/src/flushPolicies/startup-flush-policy.ts +++ b/packages/core/src/flushPolicies/startup-flush-policy.ts @@ -5,10 +5,15 @@ import { FlushPolicyBase } from './types'; * StatupFlushPolicy triggers a flush right away on client startup */ export class StartupFlushPolicy extends FlushPolicyBase { - start() { + constructor() { + super(); this.shouldFlush.value = true; } + start(): void { + // Nothing to do + } + onEvent(_event: SegmentEvent): void { // Nothing to do } diff --git a/packages/core/src/plugins/ConsentPlugin.ts b/packages/core/src/plugins/ConsentPlugin.ts index 3846f73cf..c457e8faa 100644 --- a/packages/core/src/plugins/ConsentPlugin.ts +++ b/packages/core/src/plugins/ConsentPlugin.ts @@ -5,6 +5,8 @@ import type { SegmentAPIIntegration, SegmentEvent, TrackEventType, + UpdateType, + SegmentAPISettings, } from '../types'; import type { DestinationPlugin } from '../plugin'; import type { SegmentClient } from '../analytics'; @@ -31,22 +33,25 @@ export interface CategoryConsentStatusProvider { export class ConsentPlugin extends Plugin { type = PluginType.before; private consentCategoryProvider: CategoryConsentStatusProvider; - private categories: string[]; + private categories: string[] = []; + queuedEvents: SegmentEvent[] = []; + consentStarted = false; - constructor( - consentCategoryProvider: CategoryConsentStatusProvider, - categories: string[] - ) { + constructor(consentCategoryProvider: CategoryConsentStatusProvider) { super(); this.consentCategoryProvider = consentCategoryProvider; - this.categories = categories; + } + + update(_settings: SegmentAPISettings, _type: UpdateType): void { + const consentSettings = this.analytics?.consentSettings.get(); + this.categories = consentSettings?.allCategories || []; + this.consentCategoryProvider.setApplicableCategories(this.categories); } configure(analytics: SegmentClient): void { super.configure(analytics); analytics.getPlugins().forEach(this.injectConsentFilterIfApplicable); analytics.onPluginLoaded(this.injectConsentFilterIfApplicable); - this.consentCategoryProvider.setApplicableCategories(this.categories); this.consentCategoryProvider.onConsentChange(() => { this.notifyConsentChange(); }); @@ -65,16 +70,27 @@ export class ConsentPlugin extends Plugin { }); } - async execute(event: SegmentEvent): Promise { - event.context = { - ...event.context, - consent: { - categoryPreferences: - await this.consentCategoryProvider.getConsentStatus(), - }, - }; + async execute(event: SegmentEvent): Promise { + if (this.consentStarted === true) { + event.context = { + ...event.context, + consent: { + categoryPreferences: + await this.consentCategoryProvider.getConsentStatus(), + }, + }; + return event; + } - return event; + if (this.consentStarted === false) { + // constrain the queue to avoid running out of memory if consent is never started + if (this.queuedEvents.length <= 1000) { + this.queuedEvents.push(event); + return; + } + return; + } + return; } shutdown(): void { @@ -103,7 +119,6 @@ export class ConsentPlugin extends Plugin { } const integrationSettings = settings?.[plugin.key]; - if (this.containsConsentSettings(integrationSettings)) { const categories = integrationSettings.consentSettings.categories; return ( @@ -151,6 +166,19 @@ export class ConsentPlugin extends Plugin { throw e; }); } + + public start() { + this.consentStarted = true; + + this.sendQueuedEvents(); + } + + sendQueuedEvents() { + this.queuedEvents.forEach((event) => { + this.analytics?.process(event); + }); + this.queuedEvents = []; + } } /** diff --git a/packages/core/src/plugins/QueueFlushingPlugin.ts b/packages/core/src/plugins/QueueFlushingPlugin.ts index ac0d6ddb0..a501a66ba 100644 --- a/packages/core/src/plugins/QueueFlushingPlugin.ts +++ b/packages/core/src/plugins/QueueFlushingPlugin.ts @@ -3,6 +3,7 @@ import type { SegmentClient } from '../analytics'; import { defaultConfig } from '../constants'; import { UtilityPlugin } from '../plugin'; import { PluginType, SegmentEvent } from '../types'; +import { createPromise } from '../util'; /** * This plugin manages a queue where all events get added to after timeline processing. @@ -17,17 +18,25 @@ export class QueueFlushingPlugin extends UtilityPlugin { private isPendingUpload = false; private queueStore: Store<{ events: SegmentEvent[] }> | undefined; private onFlush: (events: SegmentEvent[]) => Promise; + private isRestoredResolve: () => void; + private isRestored: Promise; /** * @param onFlush callback to execute when the queue is flushed (either by reaching the limit or manually) e.g. code to upload events to your destination + * @param storeKey key to store the queue in the store. Must be unique per destination instance + * @param restoreTimeout time in ms to wait for the queue to be restored from the store before uploading events (default: 500ms) */ constructor( onFlush: (events: SegmentEvent[]) => Promise, - storeKey = 'events' + storeKey = 'events', + restoreTimeout = 1000 ) { super(); this.onFlush = onFlush; this.storeKey = storeKey; + const { promise, resolve } = createPromise(restoreTimeout); + this.isRestored = promise; + this.isRestoredResolve = resolve; } configure(analytics: SegmentClient): void { @@ -43,6 +52,9 @@ export class QueueFlushingPlugin extends UtilityPlugin { storeId: `${config.writeKey}-${this.storeKey}`, persistor: config.storePersistor, saveDelay: config.storePersistorSaveDelay ?? 0, + onInitialized: () => { + this.isRestoredResolve(); + }, }, } ); @@ -60,6 +72,15 @@ export class QueueFlushingPlugin extends UtilityPlugin { * Calls the onFlush callback with the events in the queue */ async flush() { + // Wait for the queue to be restored + try { + await this.isRestored; + } catch (e) { + // If the queue is not restored before the timeout, we will notify but not block flushing events + console.info( + 'Flush triggered but queue restoration and settings loading not complete. Flush will be retried.' + ); + } const events = (await this.queueStore?.getState(true))?.events ?? []; if (!this.isPendingUpload) { try { diff --git a/packages/core/src/plugins/SegmentDestination.ts b/packages/core/src/plugins/SegmentDestination.ts index 93f0cbcb0..b3717b8ef 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 } from '../util'; +import { chunk, createPromise } from '../util'; import { uploadEvents } from '../api'; import type { SegmentClient } from '../analytics'; import { DestinationMetadataEnrichment } from './DestinationMetadataEnrichment'; @@ -23,18 +23,25 @@ export class SegmentDestination extends DestinationPlugin { type = PluginType.destination; key = SEGMENT_DESTINATION_KEY; private apiHost?: string; - private isReady = false; + private settingsResolve: () => void; + private settingsPromise: Promise; + + constructor() { + super(); + // We don't timeout this promise. We strictly need the response from Segment before sending things + const { promise, resolve } = createPromise(); + this.settingsPromise = promise; + this.settingsResolve = resolve; + } private sendEvents = async (events: SegmentEvent[]): Promise => { - if (!this.isReady) { - // We're not sending events until Segment has loaded all settings - return Promise.resolve(); - } - if (events.length === 0) { return Promise.resolve(); } + // We're not sending events until Segment has loaded all settings + await this.settingsPromise; + const config = this.analytics?.getConfig() ?? defaultConfig; const chunkedEvents: SegmentEvent[][] = chunk( @@ -89,6 +96,12 @@ export class SegmentDestination extends DestinationPlugin { configure(analytics: SegmentClient): void { super.configure(analytics); + // If the client has a proxy we don't need to await for settings apiHost, we can send events directly + // Important! If new settings are required in the future you probably want to change this! + if (analytics.getConfig().proxy !== undefined) { + this.settingsResolve(); + } + // Enrich events with the Destination metadata this.add(new DestinationMetadataEnrichment(SEGMENT_DESTINATION_KEY)); this.add(this.queuePlugin); @@ -105,7 +118,7 @@ export class SegmentDestination extends DestinationPlugin { ) { this.apiHost = `https://${segmentSettings.apiHost}/b`; } - this.isReady = true; + this.settingsResolve(); } execute(event: SegmentEvent): Promise { @@ -115,6 +128,7 @@ export class SegmentDestination extends DestinationPlugin { } async flush() { + // Wait until the queue is done restoring before flushing return this.queuePlugin.flush(); } } diff --git a/packages/core/src/plugins/__tests__/consent/consentNotEnabledAtSegment.test.ts b/packages/core/src/plugins/__tests__/consent/consentNotEnabledAtSegment.test.ts index 4d194b22f..386ae2588 100644 --- a/packages/core/src/plugins/__tests__/consent/consentNotEnabledAtSegment.test.ts +++ b/packages/core/src/plugins/__tests__/consent/consentNotEnabledAtSegment.test.ts @@ -27,13 +27,16 @@ describe('Consent not enabled at Segment', () => { C0005: false, }; + const consentPlugin = new ConsentPlugin( + createConsentProvider(mockConsentStatuses) + ); + client.add({ - plugin: new ConsentPlugin( - createConsentProvider(mockConsentStatuses), - Object.keys(mockConsentStatuses) - ), + plugin: consentPlugin, }); + consentPlugin.start(); + await client.init(); const segmentDestination = createSegmentWatcher(client); @@ -59,13 +62,16 @@ describe('Consent not enabled at Segment', () => { C0005: true, }; + const consentPlugin = new ConsentPlugin( + createConsentProvider(mockConsentStatuses) + ); + client.add({ - plugin: new ConsentPlugin( - createConsentProvider(mockConsentStatuses), - Object.keys(mockConsentStatuses) - ), + plugin: consentPlugin, }); + consentPlugin.start(); + await client.init(); const segmentDestination = createSegmentWatcher(client); @@ -91,13 +97,16 @@ describe('Consent not enabled at Segment', () => { C0005: true, }; + const consentPlugin = new ConsentPlugin( + createConsentProvider(mockConsentStatuses) + ); + client.add({ - plugin: new ConsentPlugin( - createConsentProvider(mockConsentStatuses), - Object.keys(mockConsentStatuses) - ), + plugin: consentPlugin, }); + consentPlugin.start(); + await client.init(); const segmentDestination = createSegmentWatcher(client); diff --git a/packages/core/src/plugins/__tests__/consent/destinationMultipleCategories.test.ts b/packages/core/src/plugins/__tests__/consent/destinationMultipleCategories.test.ts index 5335e62e4..17dd665df 100644 --- a/packages/core/src/plugins/__tests__/consent/destinationMultipleCategories.test.ts +++ b/packages/core/src/plugins/__tests__/consent/destinationMultipleCategories.test.ts @@ -28,13 +28,16 @@ describe('Destinations multiple categories', () => { C0005: false, }; + const consentPlugin = new ConsentPlugin( + createConsentProvider(mockConsentStatuses) + ); + client.add({ - plugin: new ConsentPlugin( - createConsentProvider(mockConsentStatuses), - Object.keys(mockConsentStatuses) - ), + plugin: consentPlugin, }); + consentPlugin.start(); + await client.init(); const segmentDestination = createSegmentWatcher(client); @@ -57,13 +60,16 @@ describe('Destinations multiple categories', () => { C0005: false, }; + const consentPlugin = new ConsentPlugin( + createConsentProvider(mockConsentStatuses) + ); + client.add({ - plugin: new ConsentPlugin( - createConsentProvider(mockConsentStatuses), - Object.keys(mockConsentStatuses) - ), + plugin: consentPlugin, }); + consentPlugin.start(); + await client.init(); const segmentDestination = createSegmentWatcher(client); @@ -86,15 +92,18 @@ describe('Destinations multiple categories', () => { C0005: false, }; + const consentPlugin = new ConsentPlugin( + createConsentProvider(mockConsentStatuses) + ); + client.add({ - plugin: new ConsentPlugin( - createConsentProvider(mockConsentStatuses), - Object.keys(mockConsentStatuses) - ), + plugin: consentPlugin, }); await client.init(); + consentPlugin.start(); + const segmentDestination = createSegmentWatcher(client); await client.track('test'); @@ -115,15 +124,18 @@ describe('Destinations multiple categories', () => { C0005: true, }; + const consentPlugin = new ConsentPlugin( + createConsentProvider(mockConsentStatuses) + ); + client.add({ - plugin: new ConsentPlugin( - createConsentProvider(mockConsentStatuses), - Object.keys(mockConsentStatuses) - ), + plugin: consentPlugin, }); await client.init(); + consentPlugin.start(); + const segmentDestination = createSegmentWatcher(client); await client.track('test'); diff --git a/packages/core/src/plugins/__tests__/consent/idfa.test.ts b/packages/core/src/plugins/__tests__/consent/idfa.test.ts index fa40aae0e..db6ca2817 100644 --- a/packages/core/src/plugins/__tests__/consent/idfa.test.ts +++ b/packages/core/src/plugins/__tests__/consent/idfa.test.ts @@ -40,15 +40,21 @@ describe('IDFA x Consent', () => { C0005: false, }; + const consentPlugin = new ConsentPlugin( + createConsentProvider(mockConsentStatuses) + ); + client.add({ - plugin: new ConsentPlugin( - createConsentProvider(mockConsentStatuses), - Object.keys(mockConsentStatuses) - ), + plugin: consentPlugin, }); + consentPlugin.start(); + const idfaPlugin = new IdfaPlugin(false); client.add({ + // type of segmentClient is different in idfaPlugin vs. mockClient used here + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore plugin: idfaPlugin, }); diff --git a/packages/core/src/plugins/__tests__/consent/noUnmapped.test.ts b/packages/core/src/plugins/__tests__/consent/noUnmapped.test.ts index 8ad2d6357..5371eb07e 100644 --- a/packages/core/src/plugins/__tests__/consent/noUnmapped.test.ts +++ b/packages/core/src/plugins/__tests__/consent/noUnmapped.test.ts @@ -21,15 +21,18 @@ describe('No unmapped destinations', () => { C0005: false, }; + const consentPlugin = new ConsentPlugin( + createConsentProvider(mockConsentStatuses) + ); + client.add({ - plugin: new ConsentPlugin( - createConsentProvider(mockConsentStatuses), - Object.keys(mockConsentStatuses) - ), + plugin: consentPlugin, }); await client.init(); + consentPlugin.start(); + await client.track('test'); Object.values(testDestinations).forEach((testDestination) => { @@ -48,13 +51,16 @@ describe('No unmapped destinations', () => { C0005: false, }; + const consentPlugin = new ConsentPlugin( + createConsentProvider(mockConsentStatuses) + ); + client.add({ - plugin: new ConsentPlugin( - createConsentProvider(mockConsentStatuses), - Object.keys(mockConsentStatuses) - ), + plugin: consentPlugin, }); + consentPlugin.start(); + await client.init(); await client.track('test'); @@ -77,13 +83,16 @@ describe('No unmapped destinations', () => { C0005: false, }; + const consentPlugin = new ConsentPlugin( + createConsentProvider(mockConsentStatuses) + ); + client.add({ - plugin: new ConsentPlugin( - createConsentProvider(mockConsentStatuses), - Object.keys(mockConsentStatuses) - ), + plugin: consentPlugin, }); + consentPlugin.start(); + await client.init(); await client.track('test'); @@ -106,13 +115,16 @@ describe('No unmapped destinations', () => { C0005: false, }; + const consentPlugin = new ConsentPlugin( + createConsentProvider(mockConsentStatuses) + ); + client.add({ - plugin: new ConsentPlugin( - createConsentProvider(mockConsentStatuses), - Object.keys(mockConsentStatuses) - ), + plugin: consentPlugin, }); + consentPlugin.start(); + await client.init(); await client.track('test'); @@ -135,13 +147,16 @@ describe('No unmapped destinations', () => { C0005: false, }; + const consentPlugin = new ConsentPlugin( + createConsentProvider(mockConsentStatuses) + ); + client.add({ - plugin: new ConsentPlugin( - createConsentProvider(mockConsentStatuses), - Object.keys(mockConsentStatuses) - ), + plugin: consentPlugin, }); + consentPlugin.start(); + await client.init(); await client.track('test'); @@ -164,13 +179,16 @@ describe('No unmapped destinations', () => { C0005: false, }; + const consentPlugin = new ConsentPlugin( + createConsentProvider(mockConsentStatuses) + ); + client.add({ - plugin: new ConsentPlugin( - createConsentProvider(mockConsentStatuses), - Object.keys(mockConsentStatuses) - ), + plugin: consentPlugin, }); + consentPlugin.start(); + await client.init(); await client.track('test'); @@ -193,13 +211,16 @@ describe('No unmapped destinations', () => { C0005: true, }; + const consentPlugin = new ConsentPlugin( + createConsentProvider(mockConsentStatuses) + ); + client.add({ - plugin: new ConsentPlugin( - createConsentProvider(mockConsentStatuses), - Object.keys(mockConsentStatuses) - ), + plugin: consentPlugin, }); + consentPlugin.start(); + await client.init(); await client.track('test'); diff --git a/packages/core/src/plugins/__tests__/consent/unmapped.test.ts b/packages/core/src/plugins/__tests__/consent/unmapped.test.ts index 2d4c7cc25..de01bc795 100644 --- a/packages/core/src/plugins/__tests__/consent/unmapped.test.ts +++ b/packages/core/src/plugins/__tests__/consent/unmapped.test.ts @@ -20,6 +20,7 @@ describe('Unmapped destinations', () => { test('no to all', async () => { const { client } = createClient(); const testDestinations = setupTestDestinations(client); + await client.init(); const mockConsentStatuses = { C0001: false, C0002: false, @@ -28,13 +29,16 @@ describe('Unmapped destinations', () => { C0005: false, }; + const consentPlugin = new ConsentPlugin( + createConsentProvider(mockConsentStatuses) + ); + client.add({ - plugin: new ConsentPlugin( - createConsentProvider(mockConsentStatuses), - Object.keys(mockConsentStatuses) - ), + plugin: consentPlugin, }); + consentPlugin.start(); + await client.init(); const segmentDestination = createSegmentWatcher(client); @@ -60,13 +64,16 @@ describe('Unmapped destinations', () => { C0005: false, }; + const consentPlugin = new ConsentPlugin( + createConsentProvider(mockConsentStatuses) + ); + client.add({ - plugin: new ConsentPlugin( - createConsentProvider(mockConsentStatuses), - Object.keys(mockConsentStatuses) - ), + plugin: consentPlugin, }); + consentPlugin.start(); + await client.init(); const segmentDestination = createSegmentWatcher(client); @@ -92,13 +99,16 @@ describe('Unmapped destinations', () => { C0005: false, }; + const consentPlugin = new ConsentPlugin( + createConsentProvider(mockConsentStatuses) + ); + client.add({ - plugin: new ConsentPlugin( - createConsentProvider(mockConsentStatuses), - Object.keys(mockConsentStatuses) - ), + plugin: consentPlugin, }); + consentPlugin.start(); + await client.init(); const segmentDestination = createSegmentWatcher(client); @@ -124,13 +134,16 @@ describe('Unmapped destinations', () => { C0005: false, }; + const consentPlugin = new ConsentPlugin( + createConsentProvider(mockConsentStatuses) + ); + client.add({ - plugin: new ConsentPlugin( - createConsentProvider(mockConsentStatuses), - Object.keys(mockConsentStatuses) - ), + plugin: consentPlugin, }); + consentPlugin.start(); + await client.init(); const segmentDestination = createSegmentWatcher(client); @@ -156,13 +169,16 @@ describe('Unmapped destinations', () => { C0005: false, }; + const consentPlugin = new ConsentPlugin( + createConsentProvider(mockConsentStatuses) + ); + client.add({ - plugin: new ConsentPlugin( - createConsentProvider(mockConsentStatuses), - Object.keys(mockConsentStatuses) - ), + plugin: consentPlugin, }); + consentPlugin.start(); + await client.init(); const segmentDestination = createSegmentWatcher(client); @@ -188,13 +204,16 @@ describe('Unmapped destinations', () => { C0005: false, }; + const consentPlugin = new ConsentPlugin( + createConsentProvider(mockConsentStatuses) + ); + client.add({ - plugin: new ConsentPlugin( - createConsentProvider(mockConsentStatuses), - Object.keys(mockConsentStatuses) - ), + plugin: consentPlugin, }); + consentPlugin.start(); + await client.init(); const segmentDestination = createSegmentWatcher(client); @@ -220,13 +239,16 @@ describe('Unmapped destinations', () => { C0005: true, }; + const consentPlugin = new ConsentPlugin( + createConsentProvider(mockConsentStatuses) + ); + client.add({ - plugin: new ConsentPlugin( - createConsentProvider(mockConsentStatuses), - Object.keys(mockConsentStatuses) - ), + plugin: consentPlugin, }); + consentPlugin.start(); + await client.init(); const segmentDestination = createSegmentWatcher(client); diff --git a/packages/core/src/util.ts b/packages/core/src/util.ts index ae229abba..c2e2ccca8 100644 --- a/packages/core/src/util.ts +++ b/packages/core/src/util.ts @@ -232,3 +232,28 @@ export function deepCompare(a: T, b: T): boolean { return true; } + +export const createPromise = ( + timeout: number | undefined = undefined, + _errorHandler: (err: Error) => void = (_: Error) => { + // + } +): { promise: Promise; resolve: (value: T) => void } => { + let resolver: (value: T) => void; + const promise = new Promise((resolve, reject) => { + resolver = resolve; + if (timeout !== undefined) { + setTimeout(() => { + reject(new Error('Promise timed out')); + }, timeout); + } + }); + + promise.catch(_errorHandler); + + return { + promise: promise, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + resolve: resolver!, + }; +}; diff --git a/packages/plugins/plugin-advertising-id/README.md b/packages/plugins/plugin-advertising-id/README.md index 977341649..3dcdcfef5 100644 --- a/packages/plugins/plugin-advertising-id/README.md +++ b/packages/plugins/plugin-advertising-id/README.md @@ -12,7 +12,6 @@ yarn add @segment/analytics-react-native-plugin-advertising-id This plugin requires a `compileSdkVersion` of at least 19. -See [Google Play Services documentation](https://developers.google.com/admob/android/quick-start) for `advertisingId` setup ## Usage Follow the instructions for adding plugins on the main Analytics client: diff --git a/packages/plugins/plugin-appsflyer/package.json b/packages/plugins/plugin-appsflyer/package.json index 764acde3f..a75f3e9a5 100644 --- a/packages/plugins/plugin-appsflyer/package.json +++ b/packages/plugins/plugin-appsflyer/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-react-native-plugin-appsflyer", - "version": "0.6.0", + "version": "0.8.0", "description": "The hassle-free way to add Segment analytics to your React-Native app.", "main": "lib/commonjs/index", "scripts": { diff --git a/packages/plugins/plugin-appsflyer/src/AppsflyerPlugin.tsx b/packages/plugins/plugin-appsflyer/src/AppsflyerPlugin.tsx index 7f3a3c739..c3c2ab997 100644 --- a/packages/plugins/plugin-appsflyer/src/AppsflyerPlugin.tsx +++ b/packages/plugins/plugin-appsflyer/src/AppsflyerPlugin.tsx @@ -14,21 +14,40 @@ import identify from './methods/identify'; import track from './methods/track'; export class AppsflyerPlugin extends DestinationPlugin { + constructor(props?: { + timeToWaitForATTUserAuthorization: number; + is_adset: boolean; + is_adset_id: boolean; + is_ad_id: boolean; + }) { + super(); + if (props != null) { + this.timeToWaitForATTUserAuthorization = + props.timeToWaitForATTUserAuthorization; + this.is_adset = props.is_adset === undefined ? false : props.is_adset; + this.is_ad_id = props.is_ad_id === undefined ? false : props.is_ad_id; + this.is_adset_id = + props.is_adset_id === undefined ? false : props.is_adset_id; + } + } type = PluginType.destination; key = 'AppsFlyer'; - + is_adset = false; + is_adset_id = false; + is_ad_id = false; private settings: SegmentAppsflyerSettings | null = null; private hasRegisteredInstallCallback = false; private hasRegisteredDeepLinkCallback = false; private hasInitialized = false; + timeToWaitForATTUserAuthorization = 60; + async update(settings: SegmentAPISettings, _: UpdateType): Promise { const defaultOpts = { isDebug: false, - timeToWaitForATTUserAuthorization: 60, + timeToWaitForATTUserAuthorization: this.timeToWaitForATTUserAuthorization, onInstallConversionDataListener: true, }; - const appsflyerSettings = settings.integrations[ this.key ] as SegmentAppsflyerSettings; @@ -88,7 +107,15 @@ export class AppsflyerPlugin extends DestinationPlugin { registerConversionCallback = () => { appsFlyer.onInstallConversionData((res) => { - const { af_status, media_source, campaign, is_first_launch } = res?.data; + const { + af_status, + media_source, + campaign, + is_first_launch, + adset_id, + ad_id, + adset, + } = res?.data; const properties = { provider: this.key, campaign: { @@ -96,7 +123,15 @@ export class AppsflyerPlugin extends DestinationPlugin { name: campaign, }, }; - + if (this.is_adset_id) { + Object.assign(properties, { adset_id: adset_id }); + } + if (this.is_ad_id) { + Object.assign(properties, { ad_id: ad_id }); + } + if (this.is_adset) { + Object.assign(properties, { adset: adset }); + } if (Boolean(is_first_launch) && JSON.parse(is_first_launch) === true) { if (af_status === 'Non-organic') { void this.analytics?.track('Install Attributed', properties); diff --git a/packages/plugins/plugin-appsflyer/src/__tests__/AppsflyerPlugin.test.ts b/packages/plugins/plugin-appsflyer/src/__tests__/AppsflyerPlugin.test.ts new file mode 100644 index 000000000..145c45996 --- /dev/null +++ b/packages/plugins/plugin-appsflyer/src/__tests__/AppsflyerPlugin.test.ts @@ -0,0 +1,20 @@ +import { AppsflyerPlugin } from '../AppsflyerPlugin'; + +describe('#appsflyerPlugin', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it('Appsflyer plugin without timeToWait', () => { + const plugin = new AppsflyerPlugin(); + expect(plugin.timeToWaitForATTUserAuthorization).toEqual(60); + }); + it('Appsflyer plugin with timeToWait', () => { + const plugin = new AppsflyerPlugin({ + timeToWaitForATTUserAuthorization: 90, + is_ad_id: true, + is_adset: true, + is_adset_id: true, + }); + expect(plugin.timeToWaitForATTUserAuthorization).toEqual(90); + }); +}); diff --git a/packages/plugins/plugin-branch/package.json b/packages/plugins/plugin-branch/package.json index 2f7c543d4..5125238e5 100644 --- a/packages/plugins/plugin-branch/package.json +++ b/packages/plugins/plugin-branch/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-react-native-plugin-branch", - "version": "1.1.1", + "version": "1.1.2", "description": "The hassle-free way to add Segment analytics to your React-Native app.", "main": "lib/commonjs/index", "scripts": { diff --git a/packages/plugins/plugin-branch/src/BranchPlugin.ts b/packages/plugins/plugin-branch/src/BranchPlugin.ts index 639b141e5..5cb082934 100644 --- a/packages/plugins/plugin-branch/src/BranchPlugin.ts +++ b/packages/plugins/plugin-branch/src/BranchPlugin.ts @@ -14,7 +14,7 @@ import reset from './methods/reset'; export class BranchPlugin extends DestinationPlugin { type = PluginType.destination; - key = 'Branch'; + key = 'Branch Metrics'; identify(event: IdentifyEventType) { identify(event); diff --git a/packages/plugins/plugin-braze/package.json b/packages/plugins/plugin-braze/package.json index de7bc3a2e..943e2376b 100644 --- a/packages/plugins/plugin-braze/package.json +++ b/packages/plugins/plugin-braze/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-react-native-plugin-braze", - "version": "0.6.1", + "version": "0.7.0", "description": "The hassle-free way to add Segment analytics to your React-Native app.", "main": "lib/commonjs/index", "scripts": { @@ -46,11 +46,11 @@ }, "homepage": "https://github.com/segmentio/analytics-react-native/tree/master/packages/plugins/plugin-braze#readme", "peerDependencies": { - "@braze/react-native-sdk": "^5.x", + "@braze/react-native-sdk": "^10.x", "@segment/analytics-react-native": "^2.18.0" }, "devDependencies": { - "@braze/react-native-sdk": "^5.x", + "@braze/react-native-sdk": "^10.x", "@segment/analytics-react-native": "^2.18.0", "@segment/analytics-rn-shared": "workspace:^", "@segment/sovran-react-native": "^1.1.0", diff --git a/packages/plugins/plugin-facebook-app-events/package.json b/packages/plugins/plugin-facebook-app-events/package.json index 0fad679ca..67d08ca17 100644 --- a/packages/plugins/plugin-facebook-app-events/package.json +++ b/packages/plugins/plugin-facebook-app-events/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-react-native-plugin-facebook-app-events", - "version": "0.6.0", + "version": "0.7.0", "description": "The hassle-free way to add Segment analytics to your React-Native app.", "main": "lib/commonjs/index", "scripts": { diff --git a/packages/plugins/plugin-facebook-app-events/src/FacebookAppEventsPlugin.ts b/packages/plugins/plugin-facebook-app-events/src/FacebookAppEventsPlugin.ts index 9a4e826f4..7486835c1 100644 --- a/packages/plugins/plugin-facebook-app-events/src/FacebookAppEventsPlugin.ts +++ b/packages/plugins/plugin-facebook-app-events/src/FacebookAppEventsPlugin.ts @@ -168,6 +168,8 @@ export class FacebookAppEventsPlugin extends DestinationPlugin { const purchasePrice = safeProps._valueToSum as number; AppEventsLogger.logPurchase(purchasePrice, currency, safeProps); + } else if (typeof safeProps._valueToSum === "number") { + AppEventsLogger.logEvent(safeName, safeProps._valueToSum, safeProps); } else { AppEventsLogger.logEvent(safeName, safeProps); } diff --git a/packages/plugins/plugin-firebase/package.json b/packages/plugins/plugin-firebase/package.json index a899e9967..7610ab9a6 100644 --- a/packages/plugins/plugin-firebase/package.json +++ b/packages/plugins/plugin-firebase/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-react-native-plugin-firebase", - "version": "0.4.1", + "version": "0.4.2", "description": "The hassle-free way to add Segment analytics to your React-Native app.", "main": "lib/commonjs/index", "scripts": { diff --git a/packages/plugins/plugin-firebase/src/methods/parameterMapping.ts b/packages/plugins/plugin-firebase/src/methods/parameterMapping.ts index 503dd7905..c8dd47a0c 100644 --- a/packages/plugins/plugin-firebase/src/methods/parameterMapping.ts +++ b/packages/plugins/plugin-firebase/src/methods/parameterMapping.ts @@ -27,6 +27,12 @@ export const mapEventProps: { [key: string]: string } = { productId: 'item_id', category: 'item_category', query: 'search_term', + order_id: 'transaction_id', + quantity: 'quantity', + shipping: 'shipping', + tax: 'tax', + revenue: 'revenue', + currency: 'currency', }; export const transformMap: { [key: string]: (value: unknown) => unknown } = { diff --git a/packages/plugins/plugin-idfa/ios/AnalyticsReactNativePluginIdfa.swift b/packages/plugins/plugin-idfa/ios/AnalyticsReactNativePluginIdfa.swift index 9dcb8a903..e58c302d7 100644 --- a/packages/plugins/plugin-idfa/ios/AnalyticsReactNativePluginIdfa.swift +++ b/packages/plugins/plugin-idfa/ios/AnalyticsReactNativePluginIdfa.swift @@ -16,21 +16,28 @@ class AnalyticsReactNativePluginIdfa: NSObject { ) -> Void { if #available(iOS 14, *) { ATTrackingManager.requestTrackingAuthorization { status in - let idfa = status == .authorized ? ASIdentifierManager.shared().advertisingIdentifier.uuidString : self.fallbackValue - resolve([ - "adTrackingEnabled": status == .authorized, - "advertisingId": idfa!, - "trackingStatus": self.statusToString(status) - ]) + if status == .authorized { + let idfa = ASIdentifierManager.shared().advertisingIdentifier.uuidString + resolve([ + "adTrackingEnabled": status == .authorized, + "advertisingId": idfa, + "trackingStatus": self.statusToString(status) + ]) + } else { + resolve([ + "adTrackingEnabled": false, + "trackingStatus": self.statusToString(status) + ]) + } } } else { let adTrackingEnabled: Bool = true let trackingStatus: String = "authorized" - let idfa = adTrackingEnabled ? ASIdentifierManager.shared().advertisingIdentifier.uuidString : fallbackValue + let idfa = ASIdentifierManager.shared().advertisingIdentifier.uuidString let context: [String: Any] = [ "adTrackingEnabled": adTrackingEnabled, - "advertisingId": idfa!, + "advertisingId": idfa, "trackingStatus": trackingStatus ] @@ -40,14 +47,6 @@ class AnalyticsReactNativePluginIdfa: NSObject { } } - var fallbackValue: String? { - get { - // fallback to the IDFV value. - // this is also sent in event.context.device.id, - // feel free to use a value that is more useful to you. - return UIDevice.current.identifierForVendor?.uuidString - } - } @available(iOS 14, *) func statusToString(_ status: ATTrackingManager.AuthorizationStatus) -> String { @@ -66,4 +65,4 @@ class AnalyticsReactNativePluginIdfa: NSObject { } return result } -} +} \ No newline at end of file diff --git a/packages/plugins/plugin-idfa/package.json b/packages/plugins/plugin-idfa/package.json index cd78a8af0..66bf27136 100644 --- a/packages/plugins/plugin-idfa/package.json +++ b/packages/plugins/plugin-idfa/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-react-native-plugin-idfa", - "version": "0.7.2", + "version": "0.7.3", "description": "The hassle-free way to add Segment analytics to your React-Native app.", "main": "lib/commonjs/index", "scripts": { diff --git a/packages/plugins/plugin-onetrust/README.md b/packages/plugins/plugin-onetrust/README.md index 85a9a88ef..f34b13bcc 100644 --- a/packages/plugins/plugin-onetrust/README.md +++ b/packages/plugins/plugin-onetrust/README.md @@ -2,6 +2,10 @@ Plugin for adding support for [OneTrust](https://onetrust.com/) CMP to your React Native application. +⚠️ version 1.2.0 and below are out of date and upgrading will require a re-implementation of the plugin configuration. + +⚠️ The SDK version used must match the version of the JSON published from your OneTrust instance. This Provider was built using `202405.1.0`. This is not a "plugin" as defined by the SDK's [plugin-timeline architecture](https://github.com/segmentio/analytics-react-native/tree/master?tab=readme-ov-file#plugins--timeline-architecture). Instead, it must be used with the `ConsentPlugin` included in the core SDK. This is deliberate as it is impossible to support every possible version of the OneTrust SDK. Therefore, the `OneTrustConsentProvider` should serve as a reference/example for users on a different version. + ## Installation You will need to install the `@segment/analytics-react-native-plugin-onetrust` package as a dependency in your project: @@ -22,20 +26,95 @@ yarn add @segment/analytics-react-native-plugin-onetrust react-native-onetrust-c Follow the [instructions for adding plugins](https://github.com/segmentio/analytics-react-native#adding-plugins) on the main Analytics client: -After you create your segment client add `OneTrustPlugin` as a plugin, order doesn't matter, this plugin will apply to all device mode destinations you add before and after this plugin is added: +After you create your segment client there are a few steps you must follow to complete your One Trust integration. + +1. Initialize the `OneTrust` SDK: + +```ts +import OTPublishersNativeSDK from 'react-native-onetrust-cmp'; + +... + +OTPublishersNativeSDK.startSDK( + 'storageLocation', + 'domainIdentifier', + 'languageCode', + {countryCode: 'us', regionCode:'ca'}, + true, +) + .then((responseObject) => { + console.info('Download status is ' + responseObject.status); + // get full JSON object from responseObject.responseString + }) + .catch((error) => { + console.error(`OneTrust download failed with error ${error}`); + }); +``` + +2. Create a new `OneTrustConsentProvider` and pass the `OneTrust` SDK to it: + +```ts +import { createClient, ConsentPlugin } from '@segment/analytics-react-native'; +import OTPublishersNativeSDK from 'react-native-onetrust-cmp'; +import { OTCategoryConsentProvider } from '@segment/analytics-react-native-plugin-onetrust' + +... + + const oneTrustProvider = new OneTrustConsentProvider(OTPublishersNativeSDK) +``` + +3. Initialize a new `ConsentPlugin` and pass the `OneTrustConsentProvider` to it: ```ts -import { createClient } from '@segment/analytics-react-native'; -import { OneTrustPlugin } from '@segment/analytics-react-native-plugin-onetrust'; import OTPublishersNativeSDK from 'react-native-onetrust-cmp'; +import { OneTrustConsentProvider } from '@segment/analytics-react-native-plugin-onetrust' + +... + +const oneTrustProvider = new OneTrustConsentProvider(OTPublisherNativeSDK) +const oneTrustPlugin = new ConsentPlugin(oneTrustProvider); +``` + +4. Add `oneTrustPlugin` as a plugin, order doesn't matter, this plugin will apply to all device mode destinations you add before and after this plugin is added. + +5. Call `oneTrustPlugin.start()` to start event flow. + + Full example below: + +```ts +import { createClient, ConsentPlugin} from '@segment/analytics-react-native'; +import { OneTrustConsentProvider } from '@segment/analytics-react-native-plugin-onetrust'; +import OTPublishersNativeSDK from 'react-native-onetrust-cmp'; + +OTPublishersNativeSDK.startSDK( + 'storageLocation', + 'domainIdentifier', + 'languageCode', + {countryCode: 'us', regionCode:'ca'}, + true, +) + .then((responseObject) => { + console.info('Download status is ' + responseObject.status); + // get full JSON object from responseObject.responseString + }) + .catch((error) => { + console.error(`OneTrust download failed with error ${error}`); + }); const segment = createClient({ writeKey: 'SEGMENT_KEY', }); -segment.add({ - plugin: new OneTrust(OTPublishersNativeSDK, ['C001', 'C002', '...']), -}); + +const oneTrustProvider = new OneTrustConsentProvider(OTPublisherNativeSDK) +const oneTrustPlugin = new ConsentPlugin(oneTrustProvider); + +segment.add({ plugin: oneTrustPlugin }); + +// NOTE: You might want to wait until CMP is ready before you call start() +// so that events are held until the CMP is ready to provide the current consent status. +onetrustPlugin.start() + // device mode destinations segment.add({ plugin: new BrazePlugin() }); diff --git a/packages/plugins/plugin-onetrust/package.json b/packages/plugins/plugin-onetrust/package.json index 6a67e21fb..f2ea804de 100644 --- a/packages/plugins/plugin-onetrust/package.json +++ b/packages/plugins/plugin-onetrust/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-react-native-plugin-onetrust", - "version": "1.2.0", + "version": "1.2.1", "description": "Add OneTrust to Segment analytics in your React-Native app.", "main": "lib/commonjs/index", "module": "lib/module/index", @@ -50,7 +50,7 @@ "peerDependencies": { "@segment/analytics-react-native": "^2.18.0", "@segment/sovran-react-native": "*", - "react-native-onetrust-cmp": "^202308.2.0" + "react-native-onetrust-cmp": "^202308.1.0" }, "devDependencies": { "@segment/analytics-react-native": "^2.18.0", diff --git a/packages/plugins/plugin-onetrust/src/OneTrust.ts b/packages/plugins/plugin-onetrust/src/OneTrust.ts deleted file mode 100644 index 2287283c9..000000000 --- a/packages/plugins/plugin-onetrust/src/OneTrust.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ConsentPlugin } from '@segment/analytics-react-native'; - -import { OTPublishersNativeSDK, OTCategoryConsentProvider } from './OTProvider'; - -export class OneTrustPlugin extends ConsentPlugin { - constructor(oneTrustSDK: OTPublishersNativeSDK, categories: string[]) { - super(new OTCategoryConsentProvider(oneTrustSDK), categories); - } -} diff --git a/packages/plugins/plugin-onetrust/src/OTProvider.ts b/packages/plugins/plugin-onetrust/src/OneTrustProvider.ts similarity index 96% rename from packages/plugins/plugin-onetrust/src/OTProvider.ts rename to packages/plugins/plugin-onetrust/src/OneTrustProvider.ts index 7aa29172f..45f0ee1d5 100644 --- a/packages/plugins/plugin-onetrust/src/OTProvider.ts +++ b/packages/plugins/plugin-onetrust/src/OneTrustProvider.ts @@ -19,9 +19,7 @@ export interface OTPublishersNativeSDK { stopListeningForConsentChanges(): void; } -export class OTCategoryConsentProvider - implements CategoryConsentStatusProvider -{ +export class OneTrustConsentProvider implements CategoryConsentStatusProvider { getConsentStatus!: () => Promise>; private onConsentChangeCallback!: OnConsentChangeCb; @@ -42,15 +40,12 @@ export class OTCategoryConsentProvider ]) ) ).then((entries) => Object.fromEntries(entries)); - let latestStatuses: Record | null; this.getConsentStatus = () => Promise.resolve(latestStatuses ?? initialStatusesP); - this.oneTrust.stopListeningForConsentChanges(); this.oneTrust.setBroadcastAllowedValues(categories); - categories.forEach((categoryId) => { this.oneTrust.listenForConsentChanges(categoryId, (_, status) => { initialStatusesP diff --git a/packages/plugins/plugin-onetrust/src/__tests__/OneTrust.test.ts b/packages/plugins/plugin-onetrust/src/__tests__/OneTrust.test.ts deleted file mode 100644 index d5e342a38..000000000 --- a/packages/plugins/plugin-onetrust/src/__tests__/OneTrust.test.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { - Context, - DestinationPlugin, - Plugin, - PluginType, - SegmentClient, - SegmentEvent, -} from '@segment/analytics-react-native'; -import { createTestClient } from '@segment/analytics-react-native/src/test-helpers'; -import onChange from 'on-change'; - -import { OneTrustPlugin } from '../OneTrust'; - -import type { OTPublishersNativeSDK } from '../OTProvider'; -class MockDestination extends DestinationPlugin { - track = jest.fn(); - - constructor(public readonly key: string) { - super(); - } -} - -class MockOneTrustSDK implements OTPublishersNativeSDK { - private readonly DEFAULT_CONSENT_STATUSES = { - C001: 1, - C002: 0, - C003: 1, - C004: -1, - }; - - private changeCallbacks = new Map< - string, - ((cid: string, status: number) => void)[] - >(); - - mockConsentStatuses: Record = onChange( - this.DEFAULT_CONSENT_STATUSES, - (key, value) => { - this.changeCallbacks.get(key)?.forEach((cb) => cb(key, value as number)); - } - ); - - getConsentStatusForCategory(categoryId: string): Promise { - return Promise.resolve(this.mockConsentStatuses[categoryId]); - } - - setBroadcastAllowedValues(): void { - return; - } - - listenForConsentChanges( - categoryId: string, - callback: (cid: string, status: number) => void - ): void { - this.changeCallbacks.set(categoryId, [ - ...(this.changeCallbacks.get(categoryId) || []), - callback, - ]); - } - - stopListeningForConsentChanges(): void { - this.changeCallbacks.clear(); - } -} - -describe('OneTrustPlugin', () => { - let client: SegmentClient; - let expectEvent: (event: Partial) => void; - let mockOneTrust: MockOneTrustSDK; - const mockBraze = new MockDestination('Braze'); - const mockAmplitude = new MockDestination('Amplitude'); - - beforeEach(async () => { - const testClient = createTestClient(); - testClient.store.reset(); - jest.clearAllMocks(); - client = testClient.client as unknown as SegmentClient; - expectEvent = testClient.expectEvent; - mockOneTrust = new MockOneTrustSDK(); - client.add({ - plugin: new OneTrustPlugin( - mockOneTrust, - Object.keys(mockOneTrust.mockConsentStatuses) - ), - }); - - client.add({ - plugin: mockBraze, - settings: { - consentSettings: { - categories: ['C002', 'C004'], - }, - }, - }); - - client.add({ - plugin: mockAmplitude, - settings: { - consentSettings: { - categories: ['C002'], - }, - }, - }); - - await client.init(); - }); - - it('stamps each event with consent statuses as provided by onetrust', async () => { - // we'll use a before plugin to tap into the timeline and confirm the stamps are applied as early as possible - class TapPlugin extends Plugin { - type = PluginType.before; - execute = jest.fn(); - } - - const tapPlugin = new TapPlugin(); - client.add({ - plugin: tapPlugin, - }); - - await client.track('Test event'); - - expect(tapPlugin.execute).toHaveBeenCalledWith( - expect.objectContaining({ - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - context: expect.objectContaining({ - consent: { - categoryPreferences: { - C001: true, - C002: false, - C003: true, - C004: false, - }, - }, - }), - }) - ); - }); - - it('prevents an event from reaching non-compliant destinations', async () => { - await client.track('Test event'); - - expect(mockBraze.track).not.toHaveBeenCalled(); - expect(mockAmplitude.track).not.toHaveBeenCalled(); - }); - - it('allows an event to reach destinations once consent is granted later on', async () => { - await client.track('Test event'); - - expect(mockBraze.track).not.toHaveBeenCalled(); - expect(mockAmplitude.track).not.toHaveBeenCalled(); - - mockOneTrust.mockConsentStatuses.C002 = 1; - - await client.track('Test event'); - - // this destination will now receive events - expect(mockAmplitude.track).toHaveBeenCalledTimes(1); - // but one of the tagged categories on this destination is still not consented - expect(mockBraze.track).not.toHaveBeenCalled(); - - mockOneTrust.mockConsentStatuses.C004 = 1; - - await client.track('Test event'); - - // now both have been consented - expect(mockAmplitude.track).toHaveBeenCalledTimes(2); - expect(mockBraze.track).toHaveBeenCalledTimes(1); - }); - - it('relays consent change within onetrust to Segment', async () => { - const spy = jest.spyOn(client, 'track'); - - await client.track('Test event'); - - mockOneTrust.mockConsentStatuses.C002 = 1; - - // await one tick - await new Promise(process.nextTick); - - // this is to make sure there are no unneccessary Consent Preference track calls - expect(spy).toHaveBeenCalledTimes(2); - - expect(spy).toHaveBeenLastCalledWith('Segment Consent Preference'); - - expectEvent({ - event: 'Segment Consent Preference', - context: expect.objectContaining({ - consent: { - categoryPreferences: { - C001: true, - C002: true, - C003: true, - C004: false, - }, - }, - }) as unknown as Context, - }); - }); -}); diff --git a/packages/plugins/plugin-onetrust/src/index.tsx b/packages/plugins/plugin-onetrust/src/index.tsx index 878a3ba42..1fe83897b 100644 --- a/packages/plugins/plugin-onetrust/src/index.tsx +++ b/packages/plugins/plugin-onetrust/src/index.tsx @@ -1 +1 @@ -export { OneTrustPlugin } from './OneTrust'; +export { OneTrustConsentProvider } from './OneTrustProvider'; diff --git a/packages/sovran/android/build.gradle b/packages/sovran/android/build.gradle index c90221f69..7a3291007 100644 --- a/packages/sovran/android/build.gradle +++ b/packages/sovran/android/build.gradle @@ -1,6 +1,6 @@ buildscript { // Buildscript is evaluated before everything else so we can't use getExtOrDefault - def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["AnalyticsReactNative_kotlinVersion"] + def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : (project.properties["Sovran_kotlinVersion"]).toInteger() repositories { google() diff --git a/packages/sovran/package.json b/packages/sovran/package.json index 322ec4a03..8282f38ba 100644 --- a/packages/sovran/package.json +++ b/packages/sovran/package.json @@ -1,6 +1,6 @@ { "name": "@segment/sovran-react-native", - "version": "1.1.1", + "version": "1.1.2", "description": "A cross-platform state management system", "main": "lib/commonjs/index", "module": "lib/module/index", @@ -78,7 +78,9 @@ "dependencies": { "ansi-regex": "5.0.1", "deepmerge": "^4.2.2", - "shell-quote": "1.8.0" + "react-native-get-random-values": "1.x", + "shell-quote": "1.8.0", + "uuid": "^9.0.1" }, "resolutions": { "shell-quote": "1.7.3", diff --git a/packages/sovran/src/store.ts b/packages/sovran/src/store.ts index 99967853a..76e4472b6 100644 --- a/packages/sovran/src/store.ts +++ b/packages/sovran/src/store.ts @@ -205,7 +205,7 @@ export const createStore = ( } } } catch { - console.warn('Promise not handled correctly'); + console.log('Promise not handled correctly'); } finally { action?.finally?.(state); } diff --git a/yarn.lock b/yarn.lock index 3cee12ebd..8aaa7cb13 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1702,10 +1702,10 @@ __metadata: languageName: node linkType: hard -"@braze/react-native-sdk@npm:^5.x": - version: 5.2.0 - resolution: "@braze/react-native-sdk@npm:5.2.0" - checksum: 10c0/5b3eb46d9a3b1df58a8aeff80ae09eae9d0f68dac3402d6168272fae258625dfddab212729861494e940d457bccc4e15828604e4bb09ec7bc8432f1fc59aaa89 +"@braze/react-native-sdk@npm:^10.x": + version: 10.0.0 + resolution: "@braze/react-native-sdk@npm:10.0.0" + checksum: 10c0/711eceb505225536db32066a9c39433706c0d5de15f501fadf0dfe17d56e4f81a20f88e3256df38c39880617d6046391094b675ffc04445b4d3306f87d8796ef languageName: node linkType: hard @@ -3450,7 +3450,7 @@ __metadata: version: 0.0.0-use.local resolution: "@segment/analytics-react-native-plugin-braze@workspace:packages/plugins/plugin-braze" dependencies: - "@braze/react-native-sdk": "npm:^5.x" + "@braze/react-native-sdk": "npm:^10.x" "@segment/analytics-react-native": "npm:^2.18.0" "@segment/analytics-rn-shared": "workspace:^" "@segment/sovran-react-native": "npm:^1.1.0" @@ -3459,7 +3459,7 @@ __metadata: rimraf: "npm:^5.0.5" typescript: "npm:^5.2.2" peerDependencies: - "@braze/react-native-sdk": ^5.x + "@braze/react-native-sdk": ^10.x "@segment/analytics-react-native": ^2.18.0 languageName: unknown linkType: soft @@ -3611,7 +3611,7 @@ __metadata: peerDependencies: "@segment/analytics-react-native": ^2.18.0 "@segment/sovran-react-native": "*" - react-native-onetrust-cmp: ^202308.2.0 + react-native-onetrust-cmp: ^202308.1.0 languageName: unknown linkType: soft @@ -3620,7 +3620,8 @@ __metadata: resolution: "@segment/analytics-react-native@workspace:packages/core" dependencies: "@segment/sovran-react-native": "npm:^1.1.0" - "@segment/tsub": "npm:^2" + "@segment/tsub": "npm:2.0.0" + "@stdlib/number-float64-base-normalize": "npm:0.0.8" "@types/uuid": "npm:^9.0.7" deepmerge: "npm:^4.3.1" jest: "npm:^29.7.0" @@ -3660,9 +3661,11 @@ __metadata: ansi-regex: "npm:5.0.1" deepmerge: "npm:^4.2.2" jest: "npm:^29.7.0" + react-native-get-random-values: "npm:1.x" semantic-release: "npm:^22.0.8" shell-quote: "npm:1.8.0" typescript: "npm:^5.2.2" + uuid: "npm:^9.0.1" peerDependencies: "@react-native-async-storage/async-storage": 1.x react: "*" @@ -3673,7 +3676,7 @@ __metadata: languageName: unknown linkType: soft -"@segment/tsub@npm:^2": +"@segment/tsub@npm:2.0.0, @segment/tsub@npm:^2": version: 2.0.0 resolution: "@segment/tsub@npm:2.0.0" dependencies: @@ -4752,6 +4755,21 @@ __metadata: languageName: node linkType: hard +"@stdlib/number-float64-base-normalize@npm:0.0.8": + version: 0.0.8 + resolution: "@stdlib/number-float64-base-normalize@npm:0.0.8" + dependencies: + "@stdlib/constants-float64-smallest-normal": "npm:^0.0.x" + "@stdlib/math-base-assert-is-infinite": "npm:^0.0.x" + "@stdlib/math-base-assert-is-nan": "npm:^0.0.x" + "@stdlib/math-base-special-abs": "npm:^0.0.x" + "@stdlib/types": "npm:^0.0.x" + "@stdlib/utils-define-nonenumerable-read-only-property": "npm:^0.0.x" + checksum: 10c0/8b0f4d9d4cb7ca66636d22799ebb157a7b682374f1595f1c9124bf432d628a34984fd98ab9d73224b52e6d64584f4d33589adf4dbd50c839cc4a9920536bc7a6 + conditions: (os=aix | os=darwin | os=freebsd | os=linux | os=macos | os=openbsd | os=sunos | os=win32 | os=windows) + languageName: node + linkType: hard + "@stdlib/number-float64-base-normalize@npm:^0.0.x": version: 0.0.9 resolution: "@stdlib/number-float64-base-normalize@npm:0.0.9" @@ -6246,11 +6264,11 @@ __metadata: linkType: hard "braces@npm:^3.0.2": - version: 3.0.2 - resolution: "braces@npm:3.0.2" + version: 3.0.3 + resolution: "braces@npm:3.0.3" dependencies: - fill-range: "npm:^7.0.1" - checksum: 10c0/321b4d675791479293264019156ca322163f02dc06e3c4cab33bb15cd43d80b51efef69b0930cfde3acd63d126ebca24cd0544fa6f261e093a0fb41ab9dda381 + fill-range: "npm:^7.1.1" + checksum: 10c0/7c6dfd30c338d2997ba77500539227b9d1f85e388a5f43220865201e407e076783d0881f2d297b9f80951b4c957fcf0b51c1d2d24227631643c3f7c284b0aa04 languageName: node linkType: hard @@ -8068,6 +8086,13 @@ __metadata: languageName: node linkType: hard +"fast-base64-decode@npm:^1.0.0": + version: 1.0.0 + resolution: "fast-base64-decode@npm:1.0.0" + checksum: 10c0/6d8feab513222a463d1cb58d24e04d2e04b0791ac6559861f99543daaa590e2636d040d611b40a50799bfb5c5304265d05e3658b5adf6b841a50ef6bf833d821 + languageName: node + linkType: hard + "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -8181,12 +8206,12 @@ __metadata: languageName: node linkType: hard -"fill-range@npm:^7.0.1": - version: 7.0.1 - resolution: "fill-range@npm:7.0.1" +"fill-range@npm:^7.1.1": + version: 7.1.1 + resolution: "fill-range@npm:7.1.1" dependencies: to-regex-range: "npm:^5.0.1" - checksum: 10c0/7cdad7d426ffbaadf45aeb5d15ec675bbd77f7597ad5399e3d2766987ed20bda24d5fac64b3ee79d93276f5865608bb22344a26b9b1ae6c4d00bd94bf611623f + checksum: 10c0/b75b691bbe065472f38824f694c2f7449d7f5004aa950426a2c28f0306c60db9b880c0b0e4ed819997ffb882d1da02cfcfc819bddc94d71627f5269682edf018 languageName: node linkType: hard @@ -9217,9 +9242,9 @@ __metadata: linkType: hard "ip@npm:^1.1.5": - version: 1.1.8 - resolution: "ip@npm:1.1.8" - checksum: 10c0/ab32a5ecfa678d4c158c1381c4c6744fce89a1d793e1b6635ba79d0753c069030b672d765887b6fff55670c711dfa47475895e5d6013efbbcf04687c51cb8db9 + version: 1.1.9 + resolution: "ip@npm:1.1.9" + checksum: 10c0/5af58bfe2110c9978acfd77a2ffcdf9d33a6ce1c72f49edbaf16958f7a8eb979b5163e43bb18938caf3aaa55cdacde4e470874c58ca3b4b112ea7a30461a0c27 languageName: node linkType: hard @@ -13136,6 +13161,17 @@ __metadata: languageName: node linkType: hard +"react-native-get-random-values@npm:1.x": + version: 1.11.0 + resolution: "react-native-get-random-values@npm:1.11.0" + dependencies: + fast-base64-decode: "npm:^1.0.0" + peerDependencies: + react-native: ">=0.56" + checksum: 10c0/2ce71f1ab7f5b36d4a9dd59cc80b4aa75526f047c6680a7f1a388fa8b9a62efdacaf7b7de3be593c73e882773b2eee74916b00f7c8b158e40b46388998218586 + languageName: node + linkType: hard + "react-native@npm:^0.72.7": version: 0.72.10 resolution: "react-native@npm:0.72.10" @@ -14564,8 +14600,8 @@ __metadata: linkType: hard "tar@npm:^6.1.11, tar@npm:^6.1.2, tar@npm:^6.2.0": - version: 6.2.0 - resolution: "tar@npm:6.2.0" + version: 6.2.1 + resolution: "tar@npm:6.2.1" dependencies: chownr: "npm:^2.0.0" fs-minipass: "npm:^2.0.0" @@ -14573,7 +14609,7 @@ __metadata: minizlib: "npm:^2.1.1" mkdirp: "npm:^1.0.3" yallist: "npm:^4.0.0" - checksum: 10c0/02ca064a1a6b4521fef88c07d389ac0936730091f8c02d30ea60d472e0378768e870769ab9e986d87807bfee5654359cf29ff4372746cc65e30cbddc352660d8 + checksum: 10c0/a5eca3eb50bc11552d453488344e6507156b9193efd7635e98e867fab275d527af53d8866e2370cd09dfe74378a18111622ace35af6a608e5223a7d27fe99537 languageName: node linkType: hard