Skip to content

Commit 009e4c2

Browse files
authored
feat(cookies): add consent middleware (#1905)
1 parent 34e7830 commit 009e4c2

File tree

14 files changed

+122
-20
lines changed

14 files changed

+122
-20
lines changed

.changeset/wet-horses-prove.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@scaleway/cookie-consent": minor
3+
---
4+
5+
Add Consent Middleware, fix an amplitude issue with session_id not forward correctly to the destination

packages/cookie-consent/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,13 @@ export function PanelConsent() {
7373
}
7474
```
7575

76+
### Segment Consent Middleware
77+
78+
As it's necessary now to have a consent management.
79+
https://segment.com/docs/privacy/consent-management/configure-consent-management/
80+
81+
you will have the possibility to add the SegmentConsentMiddleware, be aware that there is a dependency with SegmenttProvider.
82+
7683
### User flow
7784

7885
```mermaid

packages/cookie-consent/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@
2929
},
3030
"devDependencies": {
3131
"@types/cookie": "0.6.0",
32-
"react": "18.2.0"
32+
"react": "18.2.0",
33+
"@scaleway/use-segment": "1.0.1"
3334
},
3435
"peerDependencies": {
35-
"react": "18.x || 18"
36+
"react": "18.x || 18",
37+
"@scaleway/use-segment": "1.0.1"
3638
}
3739
}

packages/cookie-consent/src/CookieConsentProvider/CookieConsentProvider.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import {
1111
} from 'react'
1212
import { uniq } from '../helpers/array'
1313
import { stringToHash } from '../helpers/misc'
14-
import type { CategoryKind, Config, Consent, Integrations } from './types'
14+
import { isCategoryKind } from './helpers'
15+
import type { Config, Consent, Integrations } from './types'
1516
import { useSegmentIntegrations } from './useSegmentIntegrations'
1617

1718
const COOKIE_PREFIX = '_scw_rgpd'
@@ -147,8 +148,12 @@ export const CookieConsentProvider = ({
147148
(categoriesConsent: Partial<Consent>) => {
148149
for (const [consentName, consentValue] of Object.entries(
149150
categoriesConsent,
150-
) as [CategoryKind, boolean][]) {
151-
const cookieName = `${cookiePrefix}_${consentName}`
151+
)) {
152+
const consentCategoryName = isCategoryKind(consentName)
153+
? consentName
154+
: 'unknown'
155+
156+
const cookieName = `${cookiePrefix}_${consentCategoryName}`
152157

153158
if (!consentValue) {
154159
// If consent is set to false we have to delete the cookie
@@ -158,12 +163,12 @@ export const CookieConsentProvider = ({
158163
})
159164
} else {
160165
document.cookie = cookie.serialize(
161-
`${cookiePrefix}_${consentName}`,
166+
`${cookiePrefix}_${consentCategoryName}`,
162167
consentValue.toString(),
163168
{
164169
...cookiesOptions,
165170
maxAge:
166-
consentName === 'advertising'
171+
consentCategoryName === 'advertising'
167172
? consentAdvertisingMaxAge
168173
: consentMaxAge,
169174
},
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { useSegment } from '@scaleway/use-segment'
2+
import cookie from 'cookie'
3+
import type { PropsWithChildren } from 'react'
4+
import { useCookieConsent } from './CookieConsentProvider'
5+
import { type CategoryKind, isCategoryKind } from './helpers'
6+
7+
export const AMPLITUDE_INTEGRATION_NAME = 'Amplitude (Actions)'
8+
const COOKIE_SESSION_ID_NAME = 'analytics_session_id'
9+
10+
export const getSessionId = () => {
11+
const sessionId = cookie.parse(document.cookie)[COOKIE_SESSION_ID_NAME]
12+
if (sessionId) {
13+
return Number.parseInt(sessionId, 10)
14+
}
15+
16+
return Date.now()
17+
}
18+
19+
/**
20+
* inspiration
21+
* https://github.com/segmentio/consent-manager/blob/f9d5166679b3c928b394b8ad50d517fdf43654b1/src/consent-manager-builder/analytics.ts#L20
22+
*/
23+
export const SegmentConsentMiddleware = ({
24+
children,
25+
amplitudeIntegrationName = AMPLITUDE_INTEGRATION_NAME,
26+
}: PropsWithChildren<{
27+
amplitudeIntegrationName: string
28+
}>) => {
29+
const { analytics } = useSegment()
30+
const { categoriesConsent } = useCookieConsent()
31+
32+
const categoriesPreferencesAccepted: CategoryKind[] = []
33+
34+
for (const [key, value] of Object.entries(categoriesConsent)) {
35+
if (value && isCategoryKind(key)) {
36+
categoriesPreferencesAccepted.push(key)
37+
}
38+
}
39+
40+
analytics
41+
?.addSourceMiddleware(({ payload, next }) => {
42+
if (payload.obj.context) {
43+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, no-param-reassign
44+
payload.obj.context['consent'] = {
45+
...payload.obj.context['consent'],
46+
defaultDestinationBehavior: null,
47+
// Need to be handle if we let the user choose per destination and not per categories.
48+
destinationPreferences: null,
49+
categoryPreferences: categoriesPreferencesAccepted,
50+
}
51+
}
52+
53+
// actually there is a bug on the default script.
54+
if (payload.integrations()[amplitudeIntegrationName]) {
55+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, no-param-reassign
56+
payload.obj.integrations = {
57+
...payload.obj.integrations,
58+
[amplitudeIntegrationName]: {
59+
session_id: getSessionId(),
60+
},
61+
}
62+
}
63+
64+
return next(payload)
65+
})
66+
.catch(() => null)
67+
68+
return children
69+
}

packages/cookie-consent/src/CookieConsentProvider/__tests__/useSegmentIntegrations/emptyConfig.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, it } from '@jest/globals'
22
import { renderHook, waitFor } from '@testing-library/react'
3-
import { useSegmentIntegrations } from '../..'
3+
import { useSegmentIntegrations } from '../../useSegmentIntegrations'
44

55
describe('CookieConsent - useSegmentIntegrations', () => {
66
it('should not call segment if config is empty and return empty array', async () => {

packages/cookie-consent/src/CookieConsentProvider/__tests__/useSegmentIntegrations/fetchError.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, it, jest } from '@jest/globals'
22
import { renderHook, waitFor } from '@testing-library/react'
3-
import { useSegmentIntegrations } from '../..'
3+
import { useSegmentIntegrations } from '../../useSegmentIntegrations'
44

55
globalThis.fetch = jest.fn<any>(() => Promise.resolve({ ok: false }))
66

packages/cookie-consent/src/CookieConsentProvider/__tests__/useSegmentIntegrations/networkError.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, it, jest } from '@jest/globals'
22
import { renderHook, waitFor } from '@testing-library/react'
3-
import { useSegmentIntegrations } from '../..'
3+
import { useSegmentIntegrations } from '../../useSegmentIntegrations'
44

55
globalThis.fetch = jest.fn<any>(() => Promise.reject(new Error('randomError')))
66

packages/cookie-consent/src/CookieConsentProvider/__tests__/useSegmentIntegrations/working.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, it, jest } from '@jest/globals'
22
import { renderHook, waitFor } from '@testing-library/react'
3-
import { useSegmentIntegrations } from '../..'
3+
import { useSegmentIntegrations } from '../../useSegmentIntegrations'
44

55
globalThis.fetch = jest.fn<any>(() =>
66
Promise.resolve({
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export const categories = [
2+
'essential',
3+
'functional',
4+
'marketing',
5+
'analytics',
6+
'advertising',
7+
] as const
8+
9+
export type CategoryKind = (typeof categories)[number]
10+
11+
export const isCategoryKind = (key: string): key is CategoryKind =>
12+
categories.includes(key as CategoryKind)

0 commit comments

Comments
 (0)