diff --git a/lib/shared/bucketing/__tests__/bucketing.test.ts b/lib/shared/bucketing/__tests__/bucketing.test.ts index 969356852..1733e27cc 100644 --- a/lib/shared/bucketing/__tests__/bucketing.test.ts +++ b/lib/shared/bucketing/__tests__/bucketing.test.ts @@ -1455,7 +1455,7 @@ describe('Rollout Logic', () => { jest.useRealTimers() }) - it.only('should handle stepped rollout with 50% start and 100% later stage', () => { + it('should handle stepped rollout with 50% start and 100% later stage', () => { const rollout: PublicRollout = { type: 'stepped', startDate: new Date( @@ -1533,6 +1533,55 @@ describe('Rollout Logic', () => { jest.useRealTimers() }) + + it('should handle stepped rollout with a future start date', () => { + const rollout: PublicRollout = { + type: 'stepped', + startDate: new Date( + new Date().getTime() + 1000 * 60 * 60 * 24 * 7, + ), + startPercentage: 1, + stages: [ + { + type: 'discrete', + date: new Date( + new Date().getTime() + 1000 * 60 * 60 * 24 * 14, + ), + percentage: 0, + }, + ], + } + + // Before next stage - should be no one + jest.useFakeTimers().setSystemTime(new Date()) + for (let i = 0; i < 100; i++) { + expect( + doesUserPassRollout({ rollout, boundedHash: i / 100 }), + ).toBeFalsy() + } + + // After start date - should pass all users + jest.useFakeTimers().setSystemTime( + new Date(new Date().getTime() + 1000 * 60 * 60 * 24 * 8), + ) + for (let i = 0; i < 100; i++) { + expect( + doesUserPassRollout({ rollout, boundedHash: i / 100 }), + ).toBeTruthy() + } + + // After next stage - should be no one + jest.useFakeTimers().setSystemTime( + new Date(new Date().getTime() + 1000 * 60 * 60 * 24 * 15), + ) + for (let i = 0; i < 100; i++) { + expect( + doesUserPassRollout({ rollout, boundedHash: i / 100 }), + ).toBeFalsy() + } + + jest.useRealTimers() + }) }) it('throws when given an empty rollout object', () => { diff --git a/sdk/nextjs/package.json b/sdk/nextjs/package.json index 2a92dbb7c..b3d69fede 100644 --- a/sdk/nextjs/package.json +++ b/sdk/nextjs/package.json @@ -23,7 +23,8 @@ "@devcycle/react-client-sdk": "^1.30.0", "@devcycle/types": "^1.19.0", "hoist-non-react-statics": "^3.3.2", - "server-only": "^0.0.1" + "server-only": "^0.0.1", + "class-transformer": "^0.5.1" }, "types": "./src/index.d.ts", "exports": { diff --git a/sdk/nextjs/src/common/transformConfig.ts b/sdk/nextjs/src/common/transformConfig.ts deleted file mode 100644 index 37c7e6afe..000000000 --- a/sdk/nextjs/src/common/transformConfig.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { ConfigBody } from '@devcycle/types' - -/** - * Transform the config to ensure all dates are valid Date objects - * @param configData - * @returns - */ -export const transformConfig = (configData: ConfigBody): ConfigBody => { - if (!configData.features || !configData.features.length) return configData - - configData.features = configData.features.map((feature) => { - if (!feature || !feature.configuration.targets.length) return feature - - feature.configuration.targets = feature.configuration.targets.map( - (target) => { - const { rollout } = target - if (!rollout) return target - - rollout.startDate = transformDate(rollout.startDate) - if (!rollout.stages || !rollout.stages.length) return target - - rollout.stages = rollout.stages.map((stage) => { - stage.date = transformDate(stage.date) - return stage - }) - return target - }, - ) - return feature - }) - return configData -} - -const transformDate = (date: unknown): Date => { - if (!date) throw new Error('Missing rollout date in config') - if (date instanceof Date) return date - if (typeof date === 'string' || typeof date === 'number') - return new Date(date) - throw new Error('Missing rollout date in config') -} diff --git a/sdk/nextjs/src/pages/bucketing.ts b/sdk/nextjs/src/pages/bucketing.ts index d7f78fe12..7be3b2d42 100644 --- a/sdk/nextjs/src/pages/bucketing.ts +++ b/sdk/nextjs/src/pages/bucketing.ts @@ -2,7 +2,7 @@ import { DevCycleUser, DVCPopulatedUser } from '@devcycle/js-client-sdk' import { generateBucketedConfig } from '@devcycle/bucketing' import { BucketedUserConfig, ConfigBody, ConfigSource } from '@devcycle/types' import { fetchCDNConfig, sdkConfigAPI } from './requests.js' -import { transformConfig } from '../common/transformConfig.js' +import { plainToInstance } from 'class-transformer' class CDNConfigSource extends ConfigSource { async getConfig( @@ -19,7 +19,7 @@ class CDNConfigSource extends ConfigSource { throw new Error('Could not fetch config') } return { - config: await configResponse.json(), + config: plainToInstance(ConfigBody, await configResponse.json()), lastModified: configResponse.headers.get('last-modified'), metaData: {}, } @@ -53,7 +53,7 @@ const bucketOrFetchConfig = async ( return generateBucketedConfig({ user, - config: transformConfig(config), + config, }) } diff --git a/sdk/nextjs/src/server/bucketing.ts b/sdk/nextjs/src/server/bucketing.ts index c2ac660f0..3c00a1b66 100644 --- a/sdk/nextjs/src/server/bucketing.ts +++ b/sdk/nextjs/src/server/bucketing.ts @@ -7,7 +7,6 @@ import { DevCycleNextOptions, } from '../common/types' import { ConfigBody, ConfigSource } from '@devcycle/types' -import { transformConfig } from '../common/transformConfig' const getPopulatedUser = cache((user: DevCycleUser, userAgent?: string) => { return new DVCPopulatedUser( @@ -51,7 +50,7 @@ const generateBucketedConfigCached = cache( bucketedConfig: { ...generateBucketedConfig({ user: populatedUser, - config: transformConfig(config), + config, }), clientSDKKey, sse: { @@ -77,7 +76,7 @@ class CDNConfigSource extends ConfigSource { obfuscated, ) return { - config: config, + config, lastModified: headers.get('last-modified'), metaData: {}, } diff --git a/sdk/nextjs/src/server/requests.ts b/sdk/nextjs/src/server/requests.ts index 5bb5f61b0..853d580a7 100644 --- a/sdk/nextjs/src/server/requests.ts +++ b/sdk/nextjs/src/server/requests.ts @@ -2,6 +2,7 @@ import { DVCPopulatedUser } from '@devcycle/js-client-sdk' import { serializeUserSearchParams } from '../common/serializeUser' import { cache } from 'react' import { BucketedUserConfig, ConfigBody } from '@devcycle/types' +import { plainToInstance } from 'class-transformer' const getFetchUrl = (sdkKey: string, obfuscated: boolean) => `https://config-cdn.devcycle.com/config/v2/server/bootstrap/${ @@ -30,7 +31,7 @@ export const fetchCDNConfig = cache( throw new Error('Could not fetch config: ' + responseText) } return { - config: (await response.json()) as ConfigBody, + config: plainToInstance(ConfigBody, await response.json()), headers: response.headers, } },