Skip to content

Commit

Permalink
rename 'shouldLoad' -> 'shouldLoadSegment' and shouldLoadWrapper API
Browse files Browse the repository at this point in the history
  • Loading branch information
silesky committed Oct 27, 2023
1 parent fca97a2 commit a3d61e8
Show file tree
Hide file tree
Showing 10 changed files with 133 additions and 95 deletions.
5 changes: 5 additions & 0 deletions .changeset/fair-pillows-sin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@segment/analytics-consent-tools': major
---

Rename shouldLoad -> shouldLoadSegment and add shouldLoadWrapper API
25 changes: 10 additions & 15 deletions packages/consent/consent-tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@
import { createWrapper, resolveWhen } from '@segment/analytics-consent-tools'

export const withCMP = createWrapper({
// Wait to load wrapper or call "shouldLoadSegment" until window.CMP exists.
shouldLoadWrapper: async () => {
await resolveWhen(() => window.CMP !== undefined, 500)
},

// Wrapper waits to load segment / get categories until this function returns / resolves
shouldLoad: async (ctx) => {
const CMP = await getCMP()
shouldLoadSegment: async (ctx) => {
await resolveWhen(
() => !CMP.popUpVisible(),
() => !window.CMP.popUpVisible(),
500
)

Expand All @@ -24,24 +27,16 @@ export const withCMP = createWrapper({
}
},

getCategories: async () => {
const CMP = await getCMP()
return normalizeCategories(CMP.consentedCategories()) // Expected format: { foo: true, bar: false }
getCategories: () => {
return normalizeCategories(window.CMP.consentedCategories()) // Expected format: { foo: true, bar: false }
},

registerOnConsentChanged: async (setCategories) => {
const CMP = await getCMP()
CMP.onConsentChanged((event) => {
registerOnConsentChanged: (setCategories) => {
window.CMP.onConsentChanged((event) => {
setCategories(normalizeCategories(event.detail))
})
},
})


const getCMP = async () => {
await resolveWhen(() => window.CMP !== undefined, 500)
return window.CMP
}
```

## Wrapper Usage API
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,29 +123,31 @@ describe(createWrapper, () => {
expect(args.length).toBeTruthy()
})

describe('shouldLoad', () => {
describe('shouldLoadSegment', () => {
describe('Throwing errors / aborting load', () => {
const createShouldLoadThatThrows = (
...args: Parameters<LoadContext['abort']>
) => {
let err: Error
const shouldLoad = jest.fn().mockImplementation((ctx: LoadContext) => {
try {
ctx.abort(...args)
throw new Error('Fail')
} catch (_err: any) {
err = _err
}
})
return { shouldLoad, getError: () => err }
const shouldLoadSegment = jest
.fn()
.mockImplementation((ctx: LoadContext) => {
try {
ctx.abort(...args)
throw new Error('Fail')
} catch (_err: any) {
err = _err
}
})
return { shouldLoadSegment, getError: () => err }
}

it('should throw a special error if ctx.abort is called', async () => {
const { shouldLoad, getError } = createShouldLoadThatThrows({
const { shouldLoadSegment, getError } = createShouldLoadThatThrows({
loadSegmentNormally: true,
})
wrapTestAnalytics({
shouldLoad,
shouldLoadSegment,
})
await analytics.load(DEFAULT_LOAD_SETTINGS)
expect(getError() instanceof AbortLoadError).toBeTruthy()
Expand All @@ -155,7 +157,7 @@ describe(createWrapper, () => {
`should not log a console error or throw an error if ctx.abort is called (%p)`,
async (args) => {
wrapTestAnalytics({
shouldLoad: (ctx) => ctx.abort(args),
shouldLoadSegment: (ctx) => ctx.abort(args),
})
const result = await analytics.load(DEFAULT_LOAD_SETTINGS)
expect(result).toBeUndefined()
Expand All @@ -165,7 +167,7 @@ describe(createWrapper, () => {

it('should allow segment to be loaded normally (with all consent wrapper behavior disabled) via ctx.abort', async () => {
wrapTestAnalytics({
shouldLoad: (ctx) => {
shouldLoadSegment: (ctx) => {
ctx.abort({
loadSegmentNormally: true, // magic config option
})
Expand All @@ -178,7 +180,7 @@ describe(createWrapper, () => {

it('should allow segment loading to be completely aborted via ctx.abort', async () => {
wrapTestAnalytics({
shouldLoad: (ctx) => {
shouldLoadSegment: (ctx) => {
ctx.abort({
loadSegmentNormally: false, // magic config option
})
Expand All @@ -189,11 +191,11 @@ describe(createWrapper, () => {
expect(analyticsLoadSpy).not.toBeCalled()
})
it('should throw a validation error if ctx.abort is called incorrectly', async () => {
const { getError, shouldLoad } = createShouldLoadThatThrows(
const { getError, shouldLoadSegment } = createShouldLoadThatThrows(
undefined as any
)
wrapTestAnalytics({
shouldLoad,
shouldLoadSegment,
})
await analytics.load(DEFAULT_LOAD_SETTINGS)
expect(getError().message).toMatch(/validation/i)
Expand All @@ -202,7 +204,7 @@ describe(createWrapper, () => {
it('An unrecognized Error (non-consent) error should bubble up, but we should not log any additional console error', async () => {
const err = new Error('hello')
wrapTestAnalytics({
shouldLoad: () => {
shouldLoadSegment: () => {
throw err
},
})
Expand All @@ -215,7 +217,7 @@ describe(createWrapper, () => {
expect(analyticsLoadSpy).not.toBeCalled()
})
})
it('should first call shouldLoad(), then wait for it to resolve/return before calling analytics.load()', async () => {
it('should first call shouldLoadSegment(), then wait for it to resolve/return before calling analytics.load()', async () => {
const fnCalls: string[] = []
analyticsLoadSpy.mockImplementationOnce(() => {
fnCalls.push('analytics.load')
Expand All @@ -224,31 +226,31 @@ describe(createWrapper, () => {
const shouldLoadMock: jest.Mock<undefined> = jest
.fn()
.mockImplementationOnce(async () => {
fnCalls.push('shouldLoad')
fnCalls.push('shouldLoadSegment')
})

wrapTestAnalytics({
shouldLoad: shouldLoadMock,
shouldLoadSegment: shouldLoadMock,
})

await analytics.load(DEFAULT_LOAD_SETTINGS)
expect(fnCalls).toEqual(['shouldLoad', 'analytics.load'])
expect(fnCalls).toEqual(['shouldLoadSegment', 'analytics.load'])
})
})

describe('getCategories', () => {
test.each([
{
shouldLoad: () => undefined,
shouldLoadSegment: () => undefined,
returnVal: 'undefined',
},
{
shouldLoad: () => Promise.resolve(undefined),
shouldLoadSegment: () => Promise.resolve(undefined),
returnVal: 'Promise<undefined>',
},
])(
'if shouldLoad() returns nil ($returnVal), intial categories will come from getCategories()',
async ({ shouldLoad }) => {
'if shouldLoadSegment() returns nil ($returnVal), intial categories will come from getCategories()',
async ({ shouldLoadSegment }) => {
const mockCdnSettings = {
integrations: {
mockIntegration: {
Expand All @@ -258,7 +260,7 @@ describe(createWrapper, () => {
}

wrapTestAnalytics({
shouldLoad: shouldLoad,
shouldLoadSegment: shouldLoadSegment,
})
await analytics.load({
...DEFAULT_LOAD_SETTINGS,
Expand All @@ -282,7 +284,7 @@ describe(createWrapper, () => {
returnVal: 'Promise<Categories>',
},
])(
'if shouldLoad() returns categories ($returnVal), those will be the initial categories',
'if shouldLoadSegment() returns categories ($returnVal), those will be the initial categories',
async ({ getCategories }) => {
const mockCdnSettings = {
integrations: {
Expand All @@ -296,7 +298,7 @@ describe(createWrapper, () => {

wrapTestAnalytics({
getCategories: mockGetCategories,
shouldLoad: () => undefined,
shouldLoadSegment: () => undefined,
})
await analytics.load({
...DEFAULT_LOAD_SETTINGS,
Expand All @@ -321,7 +323,7 @@ describe(createWrapper, () => {

test('analytics.load should reject if categories are in the wrong format', async () => {
wrapTestAnalytics({
shouldLoad: () => Promise.resolve('sup' as any),
shouldLoadSegment: () => Promise.resolve('sup' as any),
})
await expect(() => analytics.load(DEFAULT_LOAD_SETTINGS)).rejects.toThrow(
/validation/i
Expand All @@ -331,7 +333,7 @@ describe(createWrapper, () => {
test('analytics.load should reject if categories are undefined', async () => {
wrapTestAnalytics({
getCategories: () => undefined as any,
shouldLoad: () => undefined,
shouldLoadSegment: () => undefined,
})
await expect(() => analytics.load(DEFAULT_LOAD_SETTINGS)).rejects.toThrow(
/validation/i
Expand Down Expand Up @@ -390,7 +392,7 @@ describe(createWrapper, () => {
.build()

wrapTestAnalytics({
shouldLoad: () => ({ Foo: true }),
shouldLoadSegment: () => ({ Foo: true }),
})
await analytics.load({
...DEFAULT_LOAD_SETTINGS,
Expand All @@ -415,7 +417,7 @@ describe(createWrapper, () => {
.build()

wrapTestAnalytics({
shouldLoad: () => ({ Foo: true, Bar: true }),
shouldLoadSegment: () => ({ Foo: true, Bar: true }),
})
await analytics.load({
...DEFAULT_LOAD_SETTINGS,
Expand All @@ -440,7 +442,7 @@ describe(createWrapper, () => {
.build()

wrapTestAnalytics({
shouldLoad: () => ({ Foo: true }),
shouldLoadSegment: () => ({ Foo: true }),
})
await analytics.load({
...DEFAULT_LOAD_SETTINGS,
Expand All @@ -466,34 +468,34 @@ describe(createWrapper, () => {
expect(analyticsLoadSpy).toBeCalled()
})

it('should not call shouldLoad if called on first', async () => {
const shouldLoad = jest.fn()
it('should not call shouldLoadSegment if called on first', async () => {
const shouldLoadSegment = jest.fn()
wrapTestAnalytics({
shouldDisableConsentRequirement: () => true,
shouldLoad,
shouldLoadSegment,
})
await analytics.load(DEFAULT_LOAD_SETTINGS)
expect(shouldLoad).not.toBeCalled()
expect(shouldLoadSegment).not.toBeCalled()
})

it('should work with promises if false', async () => {
const shouldLoad = jest.fn()
const shouldLoadSegment = jest.fn()
wrapTestAnalytics({
shouldDisableConsentRequirement: () => Promise.resolve(false),
shouldLoad,
shouldLoadSegment,
})
await analytics.load(DEFAULT_LOAD_SETTINGS)
expect(shouldLoad).toBeCalled()
expect(shouldLoadSegment).toBeCalled()
})

it('should work with promises if true', async () => {
const shouldLoad = jest.fn()
const shouldLoadSegment = jest.fn()
wrapTestAnalytics({
shouldDisableConsentRequirement: () => Promise.resolve(true),
shouldLoad,
shouldLoadSegment,
})
await analytics.load(DEFAULT_LOAD_SETTINGS)
expect(shouldLoad).not.toBeCalled()
expect(shouldLoadSegment).not.toBeCalled()
})

it('should forward all arguments to the original analytics.load method', async () => {
Expand Down Expand Up @@ -567,7 +569,7 @@ describe(createWrapper, () => {

wrapTestAnalytics({
getCategories,
shouldLoad: () => {
shouldLoadSegment: () => {
// on first load, we should not get an error because this is a valid category setting
return { invalidCategory: true }
},
Expand Down
21 changes: 13 additions & 8 deletions packages/consent/consent-tools/src/domain/create-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,24 @@ export const createWrapper = <Analytics extends AnyAnalytics>(
shouldDisableSegment,
shouldDisableConsentRequirement,
getCategories,
shouldLoad,
shouldLoadSegment,
integrationCategoryMappings,
shouldEnableIntegration,
pruneUnmappedCategories,
registerOnConsentChanged,
shouldLoadWrapper,
} = createWrapperOptions

return (analytics: Analytics) => {
validateAnalyticsInstance(analytics)

// Call this function as early as possible. OnConsentChanged events can happen before .load is called.
registerOnConsentChanged?.((categories) =>
// whenever consent changes, dispatch a new event with the latest consent information
validateAndSendConsentChangedEvent(analytics, categories)
)
const loadWrapper = shouldLoadWrapper?.() || Promise.resolve()
void loadWrapper.then(() => {
// Call this function as early as possible. OnConsentChanged events can happen before .load is called.
registerOnConsentChanged?.((categories) =>
// whenever consent changes, dispatch a new event with the latest consent information
validateAndSendConsentChangedEvent(analytics, categories)
)
})

const ogLoad = analytics.load

Expand All @@ -53,6 +56,7 @@ export const createWrapper = <Analytics extends AnyAnalytics>(
return
}

await loadWrapper
const consentRequirementDisabled =
await shouldDisableConsentRequirement?.()
if (consentRequirementDisabled) {
Expand All @@ -64,7 +68,8 @@ export const createWrapper = <Analytics extends AnyAnalytics>(
let initialCategories: Categories
try {
initialCategories =
(await shouldLoad?.(new LoadContext())) || (await getCategories())
(await shouldLoadSegment?.(new LoadContext())) ||
(await getCategories())
} catch (e: unknown) {
// consumer can call ctx.abort({ loadSegmentNormally: true })
// to load Segment but disable consent requirement
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ export function validateSettings(options: {

assertIsFunction(options.getCategories, 'getCategories')

options.shouldLoad && assertIsFunction(options.shouldLoad, 'shouldLoad')
options.shouldLoadSegment &&
assertIsFunction(options.shouldLoadSegment, 'shouldLoadSegment')

options.shouldDisableConsentRequirement &&
assertIsFunction(
Expand Down
Loading

0 comments on commit a3d61e8

Please sign in to comment.