Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add consent management and support for onetrust cmp #882

Merged
merged 5 commits into from
Oct 2, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
node-version: '18'
cache: 'yarn'
- name: Install
run: yarn install --frozen-lockfile
Expand All @@ -39,7 +39,7 @@ jobs:

- uses: actions/setup-node@v2
with:
node-version: '16'
node-version: '18'
cache: 'yarn'

- uses: maxim-lobanov/setup-xcode@v1
Expand Down Expand Up @@ -116,7 +116,7 @@ jobs:

- uses: actions/setup-node@v2
with:
node-version: '16'
node-version: '18'
cache: 'yarn'

- name: Bootstrap
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
"conventional-changelog-conventionalcommits": "^5.0.0",
"eslint": "^8.2.0",
"jest": "^27.5.1",
"metro-react-native-babel-preset": "^0.66.2",
"metro-react-native-babel-preset": "^0.77.0",
"react": "^17.0.2",
"react-native": "^0.67.2",
"react-native-builder-bob": "^0.18.2",
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,9 @@ export {
} from './util';
export { SegmentClient } from './analytics';
export { SegmentDestination } from './plugins/SegmentDestination';
export {
CategoryConsentStatusProvider,
ConsentPlugin,
} from './plugins/ConsentPlugin';
export * from './flushPolicies';
export * from './errors';
119 changes: 119 additions & 0 deletions packages/core/src/plugins/ConsentPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import {
Plugin,
type SegmentClient,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't know about this syntax!

type DestinationPlugin,
IntegrationSettings,
PluginType,
SegmentAPIIntegration,
SegmentEvent,
} from '..';

const SEGMENT_PREF_UPDATE_EVENT = 'Segment Consent Preference';

export interface CategoryConsentStatusProvider {
setApplicableCategories(categories: string[]): void;
getConsentStatus(): Promise<Record<string, boolean>>;
onConsentChange(cb: (updConsent: Record<string, boolean>) => void): void;
shutdown?(): void;
}

/**
* This plugin interfaces with the consent provider and it:
*
* - stamps all events with the consent metadata.
* - augments all destinations with a consent filter plugin that prevents events from reaching them if
* they are not compliant current consent setup
* - listens for consent change from the provider and notifies Segment
*/
export class ConsentPlugin extends Plugin {
type = PluginType.before;

constructor(
private consentCategoryProvider: CategoryConsentStatusProvider,
private categories: string[]
) {
super();
}

configure(analytics: SegmentClient): void {
super.configure(analytics);
analytics.getPlugins().forEach(this.injectConsentFilterIfApplicable);
zikaari marked this conversation as resolved.
Show resolved Hide resolved
analytics.onPluginLoaded(this.injectConsentFilterIfApplicable);
this.consentCategoryProvider.setApplicableCategories(this.categories);
// eslint-disable-next-line @typescript-eslint/no-misused-promises
zikaari marked this conversation as resolved.
Show resolved Hide resolved
this.consentCategoryProvider.onConsentChange(this.handleConsentChange);
}

async execute(event: SegmentEvent): Promise<SegmentEvent> {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if ((event as any).event === 'Segment Consent Preference') {

Check failure on line 49 in packages/core/src/plugins/ConsentPlugin.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unexpected any. Specify a different type
zikaari marked this conversation as resolved.
Show resolved Hide resolved
zikaari marked this conversation as resolved.
Show resolved Hide resolved
return event;
}

(event.context ??= {}).consent = {
categoryPreferences:
await this.consentCategoryProvider.getConsentStatus(),
zikaari marked this conversation as resolved.
Show resolved Hide resolved
};

return event;
}

shutdown(): void {
this.consentCategoryProvider.shutdown?.();
}

private injectConsentFilterIfApplicable = (plugin: Plugin) => {
if (this.isDestinationPlugin(plugin)) {
const settings = this.analytics?.settings.get()?.[plugin.key];

// FIXME: What stance should we take when this `if` block is false?
if (this.containsConsentSettings(settings)) {
zikaari marked this conversation as resolved.
Show resolved Hide resolved
plugin.add(
new ConsentFilterPlugin(settings.consentSettings.categories)
);
}
}
};

private handleConsentChange = async (updConsent: Record<string, boolean>) => {
await this.analytics?.track(SEGMENT_PREF_UPDATE_EVENT, {
consent: {
categoryPreferences: updConsent,
},
});
zikaari marked this conversation as resolved.
Show resolved Hide resolved
};

private isDestinationPlugin(plugin: Plugin): plugin is DestinationPlugin {
return plugin.type === PluginType.destination;
}

private containsConsentSettings = (
settings: IntegrationSettings | undefined
): settings is Required<Pick<SegmentAPIIntegration, 'consentSettings'>> => {
return (
typeof (settings as SegmentAPIIntegration)?.consentSettings
?.categories === 'object'
);
};
}

/**
* This plugin reads the consent metadata set on the context object and then drops the events
* if they are going into a destination which violates's set consent preferences
*/
class ConsentFilterPlugin extends Plugin {
type = PluginType.before;

constructor(private categories: string[]) {
super();
}

execute(event: SegmentEvent): SegmentEvent | undefined {
const preferences = event.context?.consent?.categoryPreferences;

// all categories this destination is tagged with must be present, and allowed in consent preferences
return this.categories.every((category) => preferences?.[category])
? event
: undefined;
zikaari marked this conversation as resolved.
Show resolved Hide resolved
}
}
15 changes: 10 additions & 5 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,9 @@ export type Context = {
timezone: string;
traits: UserTraits;
instanceId: string;
consent?: {
categoryPreferences: Record<string, boolean>;
};
};

/**
Expand Down Expand Up @@ -248,10 +251,13 @@ export type NativeContextInfo = {
advertisingId?: string; // ios only
};

export type SegmentAPIIntegration = {
export type SegmentAPIIntegration<T = object> = {
apiKey: string;
apiHost: string;
};
consentSettings?: {
categories: string[];
};
} & T;

type SegmentAmplitudeIntegration = {
session_id: number;
Expand All @@ -273,9 +279,8 @@ export type SegmentBrazeSettings = {

export type IntegrationSettings =
// Strongly typed known integration settings
| SegmentAPIIntegration
| SegmentAmplitudeIntegration
| SegmentAdjustSettings
| SegmentAPIIntegration<SegmentAmplitudeIntegration>
| SegmentAPIIntegration<SegmentAdjustSettings>
// Support any kind of configuration in the future
| Record<string, unknown>
// enable/disable the integration at cloud level
Expand Down
21 changes: 21 additions & 0 deletions packages/plugins/plugin-onetrust/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2020 Segment

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
76 changes: 76 additions & 0 deletions packages/plugins/plugin-onetrust/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# @segment/analytics-react-native-plugin-onetrust

Plugin for adding support for [OneTrust](https://onetrust.com/) CMP to your React Native application.

## Installation

You will need to install the `@segment/analytics-react-native-plugin-onetrust` package as a dependency in your project:

Using NPM:

```bash
npm install --save @segment/analytics-react-native-plugin-onetrust react-native-onetrust-cmp
```

Using Yarn:

```bash
yarn add @segment/analytics-react-native-plugin-onetrust react-native-onetrust-cmp
```

## Usage

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:

```ts
import { createClient } from '@segment/analytics-react-native';
import { OneTrustPlugin } from '@segment/analytics-react-native-plugin-onetrust';
import OTPublishersNativeSDK from 'react-native-onetrust-cmp';

const segment = createClient({
writeKey: 'SEGMENT_KEY',
});

segment.add({
plugin: new OneTrust(OTPublishersNativeSDK, ['C001', 'C002', '...']),
});

// device mode destinations
segment.add({ plugin: new BrazePlugin() });
```

## Support

Please use Github issues, Pull Requests, or feel free to reach out to our [support team](https://segment.com/help/).

## Integrating with Segment

Interested in integrating your service with us? Check out our [Partners page](https://segment.com/partners/) for more details.

## License

```
MIT License

Copyright (c) 2021 Segment
zikaari marked this conversation as resolved.
Show resolved Hide resolved

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
3 changes: 3 additions & 0 deletions packages/plugins/plugin-onetrust/babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
presets: ['module:metro-react-native-babel-preset'],
};
16 changes: 16 additions & 0 deletions packages/plugins/plugin-onetrust/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const { pathsToModuleNameMapper } = require('ts-jest');
const { compilerOptions } = require('./tsconfig');

module.exports = {
preset: 'react-native',
roots: ['<rootDir>'],
setupFiles: ['../../core/src/__tests__/__helpers__/setup.ts'],
testPathIgnorePatterns: ['.../../core/src/__tests__/__helpers__/'],
modulePathIgnorePatterns: ['/lib/'],
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
modulePaths: [compilerOptions.baseUrl],
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't know about this helper!

};
Loading
Loading