Skip to content

Commit

Permalink
Reduce size of create-wrapper.ts (#990)
Browse files Browse the repository at this point in the history
  • Loading branch information
silesky authored Nov 7, 2023
1 parent d8374bd commit a361575
Show file tree
Hide file tree
Showing 8 changed files with 99 additions and 89 deletions.
5 changes: 5 additions & 0 deletions .changeset/gold-impalas-kneel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@segment/analytics-consent-tools': patch
---

Refactor consent wrapper; export GetCategoriesFunction
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ describe(createConsentStampingMiddleware, () => {
let middlewareFn: MiddlewareFunction
const nextFn = jest.fn()
const getCategories = jest.fn()
// @ts-ignore
const payload = {
obj: {
type: 'track',
Expand Down Expand Up @@ -42,4 +41,30 @@ describe(createConsentStampingMiddleware, () => {
Advertising: true,
})
})

it('should throw an error if getCategories returns an invalid value', async () => {
middlewareFn = createConsentStampingMiddleware(getCategories)
getCategories.mockReturnValue(null as any)
await expect(() =>
middlewareFn({
next: nextFn,
// @ts-ignore
payload,
})
).rejects.toThrowError(/Validation/)
expect(nextFn).not.toHaveBeenCalled()
})

it('should throw an error if getCategories returns an invalid async value', async () => {
middlewareFn = createConsentStampingMiddleware(getCategories)
getCategories.mockResolvedValue(null as any)
await expect(() =>
middlewareFn({
next: nextFn,
// @ts-ignore
payload,
})
).rejects.toThrowError(/Validation/)
expect(nextFn).not.toHaveBeenCalled()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -487,45 +487,6 @@ describe(createWrapper, () => {
expect(analyticsLoadSpy).not.toBeCalled()
})
})
test.each([
{
getCategories: () =>
({
invalidCategory: 'hello',
} as any),
returnVal: 'Categories',
},
{
getCategories: () =>
Promise.resolve({
invalidCategory: 'hello',
}) as any,
returnVal: 'Promise<Categories>',
},
])(
'should throw an error if getCategories() returns invalid categories during consent stamping ($returnVal))',
async ({ getCategories }) => {
const fn = jest.spyOn(ConsentStamping, 'createConsentStampingMiddleware')
const mockCdnSettings = settingsBuilder.build()

wrapTestAnalytics({
getCategories,
shouldLoadSegment: () => {
// on first load, we should not get an error because this is a valid category setting
return { invalidCategory: true }
},
})
await analytics.load({
...DEFAULT_LOAD_SETTINGS,
cdnSettings: mockCdnSettings,
})

const getCategoriesFn = fn.mock.lastCall[0]
await expect(getCategoriesFn()).rejects.toMatchInlineSnapshot(
`[ValidationError: [Validation] Consent Categories should be {[categoryName: string]: boolean} (Received: {"invalidCategory":"hello"})]`
)
}
)

describe('shouldEnableIntegration', () => {
it('should let user customize the logic that determines whether or not a destination is enabled', async () => {
Expand Down
2 changes: 2 additions & 0 deletions packages/consent/consent-tools/src/domain/consent-stamping.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AnyAnalytics, Categories } from '../types'
import { validateCategories } from './validation'

type CreateConsentMw = (
getCategories: () => Promise<Categories>
Expand All @@ -11,6 +12,7 @@ export const createConsentStampingMiddleware: CreateConsentMw =
(getCategories) =>
async ({ payload, next }) => {
const categories = await getCategories()
validateCategories(categories)
payload.obj.context.consent = {
...payload.obj.context.consent,
categoryPreferences: categories,
Expand Down
64 changes: 18 additions & 46 deletions packages/consent/consent-tools/src/domain/create-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import {
validateSettings,
} from './validation'
import { createConsentStampingMiddleware } from './consent-stamping'
import { pipe, pick, uniq } from '../utils'
import { pipe } from '../utils'
import { AbortLoadError, LoadContext } from './load-cancellation'
import { ValidationError } from './validation/validation-error'
import { validateAndSendConsentChangedEvent } from './consent-changed'
import { getPrunedCategories } from './pruned-categories'

export const createWrapper = <Analytics extends AnyAnalytics>(
...[createWrapperOptions]: Parameters<CreateWrapper<Analytics>>
Expand Down Expand Up @@ -79,56 +79,28 @@ export const createWrapper = <Analytics extends AnyAnalytics>(

validateCategories(initialCategories)

const getPrunedCategories = async (
cdnSettingsP: Promise<CDNSettings>
): Promise<Categories> => {
const cdnSettings = await cdnSettingsP
// we don't want to send _every_ category to segment, only the ones that the user has explicitly configured in their integrations
let allCategories: string[]
// We need to get all the unique categories so we can prune the consent object down to only the categories that are configured
// There can be categories that are not included in any integration in the integrations object (e.g. 2 cloud mode categories), which is why we need a special allCategories array
if (integrationCategoryMappings) {
allCategories = uniq(
Object.values(integrationCategoryMappings).reduce((p, n) =>
p.concat(n)
)
)
} else {
allCategories = cdnSettings.consentSettings?.allCategories || []
}
// we need to register the listener before .load is called so we don't miss it.
// note: the 'initialize' API event is emitted so before the final flushing of events, so this promise won't block the pipeline.
const cdnSettings = new Promise<CDNSettings>((resolve) =>
analytics.on('initialize', resolve)
)

if (!allCategories.length) {
// No configured integrations found, so no categories will be sent (should not happen unless there's a configuration error)
throw new ValidationError(
'Invariant: No consent categories defined in Segment',
[]
// normalize getCategories pruning is turned on or off
const getCategoriesForConsentStamping = async (): Promise<Categories> => {
if (pruneUnmappedCategories) {
return getPrunedCategories(
getCategories,
await cdnSettings,
integrationCategoryMappings
)
} else {
return getCategories()
}

const categories = await getCategories()

return pick(categories, allCategories)
}

// create getCategories and validate them regardless of whether pruning is turned on or off
const getValidCategoriesForConsentStamping = pipe(
pruneUnmappedCategories
? getPrunedCategories.bind(
this,
new Promise<CDNSettings>((resolve) =>
analytics.on('initialize', resolve)
)
)
: getCategories,
async (categories) => {
validateCategories(await categories)
return categories
}
) as () => Promise<Categories>

// register listener to stamp all events with latest consent information
analytics.addSourceMiddleware(
createConsentStampingMiddleware(getValidCategoriesForConsentStamping)
createConsentStampingMiddleware(getCategoriesForConsentStamping)
)

const updateCDNSettings: InitOptions['updateCDNSettings'] = (
Expand All @@ -150,7 +122,7 @@ export const createWrapper = <Analytics extends AnyAnalytics>(
...options,
updateCDNSettings: pipe(
updateCDNSettings,
options?.updateCDNSettings ? options.updateCDNSettings : (f) => f
options?.updateCDNSettings || ((id) => id)
),
})
}
Expand Down
38 changes: 38 additions & 0 deletions packages/consent/consent-tools/src/domain/pruned-categories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { uniq, pick } from 'lodash'
import {
CDNSettings,
CreateWrapperSettings,
Categories,
GetCategoriesFunction,
} from '../types'
import { ValidationError } from './validation/validation-error'

export const getPrunedCategories = async (
getCategories: GetCategoriesFunction,
cdnSettings: CDNSettings,
integrationCategoryMappings?: CreateWrapperSettings['integrationCategoryMappings']
): Promise<Categories> => {
// we don't want to send _every_ category to segment, only the ones that the user has explicitly configured in their integrations
let allCategories: string[]
// We need to get all the unique categories so we can prune the consent object down to only the categories that are configured
// There can be categories that are not included in any integration in the integrations object (e.g. 2 cloud mode categories), which is why we need a special allCategories array
if (integrationCategoryMappings) {
allCategories = uniq(
Object.values(integrationCategoryMappings).reduce((p, n) => p.concat(n))
)
} else {
allCategories = cdnSettings.consentSettings?.allCategories || []
}

if (!allCategories.length) {
// No configured integrations found, so no categories will be sent (should not happen unless there's a configuration error)
throw new ValidationError(
'Invariant: No consent categories defined in Segment',
[]
)
}

const categories = await getCategories()

return pick(categories, allCategories)
}
1 change: 1 addition & 0 deletions packages/consent/consent-tools/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type {
CreateWrapperSettings,
IntegrationCategoryMappings,
Categories,
GetCategoriesFunction,
RegisterOnConsentChangedFunction,
AnyAnalytics,
} from './types'
12 changes: 9 additions & 3 deletions packages/consent/consent-tools/src/types/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,18 @@ import type {
CDNSettingsRemotePlugin,
} from './wrapper'

/**
* See {@link CreateWrapperSettings.registerOnConsentChanged}
*/
export type RegisterOnConsentChangedFunction = (
categoriesChangedCb: (categories: Categories) => void
) => void

/**
* See {@link CreateWrapperSettings.getCategories}
*/
export type GetCategoriesFunction = () => Categories | Promise<Categories>

/**
* Consent wrapper function configuration
*/
Expand Down Expand Up @@ -55,11 +63,9 @@ export interface CreateWrapperSettings {
/**
* Fetch the categories which stamp every event. Called each time a new Segment event is dispatched.
* @example
* ```ts
* () => ({ "Advertising": true, "Analytics": false })
* ```
**/
getCategories: () => Categories | Promise<Categories>
getCategories: GetCategoriesFunction

/**
* Function to register a listener for consent changes to programatically send a "Segment Consent Preference" event to Segment when consent preferences change.
Expand Down

0 comments on commit a361575

Please sign in to comment.