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

feat: wrap vercel flags #903

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
10 changes: 9 additions & 1 deletion e2e/nextjs/app-router/app/app/normal/ServerComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import { getAllFeatures, getAllVariables, getVariableValue } from './devcycle'
import {
getAllFeatures,
getAllVariables,
getFlag,
getVariableValue,
} from './devcycle'
export const ServerComponent = async () => {
const enabledVar = await getVariableValue('enabled-feature', false)
const disabledVar = await getVariableValue('disabled-feature', false)
const allVariables = await getAllVariables()
const allFeatures = await getAllFeatures()

const vercelFlag = await getFlag('enabled-feature', false)

return (
<div>
<h1>Server Component</h1>
<p>Server Enabled Variable: {JSON.stringify(enabledVar)}</p>
<p>Server Disabled Variable: {JSON.stringify(disabledVar)}</p>
<p>Server All Variables: {JSON.stringify(allVariables)}</p>
<p>Server All Features: {JSON.stringify(allFeatures)}</p>
<p>Vercel Flag: {JSON.stringify(vercelFlag)}</p>
</div>
)
}
7 changes: 7 additions & 0 deletions e2e/nextjs/app-router/app/app/normal/devcycle.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { setupDevCycle } from '@devcycle/nextjs-sdk/server'
import { setupDevCycleVercelFlagHelper } from '@devcycle/nextjs-sdk/vercel'
import { headers } from 'next/headers'
export const {
getVariableValue,
getClientContext,
getAllVariables,
getAllFeatures,
getConfig,
} = setupDevCycle({
clientSDKKey: process.env.NEXT_PUBLIC_E2E_NEXTJS_CLIENT_KEY ?? '',
serverSDKKey: process.env.E2E_NEXTJS_SERVER_KEY ?? '',
Expand All @@ -20,3 +22,8 @@ export const {
},
options: { enableStreaming: false },
})

export const getFlag = setupDevCycleVercelFlagHelper({
Copy link
Member

Choose a reason for hiding this comment

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

Why as a separate import and not a part of setupDevCycle()?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

because it depends on the @vercel/flags package which I wanted to make an optional dependency. So the idea is that if you want to use it with Vercel, you install their flags package and import this helper from @devcycle/nextjs-sdk/vercel where it expects the package to be available.

getConfig,
getAllFeatures,
})
1 change: 1 addition & 0 deletions e2e/nextjs/app-router/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
},
"dependencies": {
"@devcycle/nextjs-sdk": "file:../../../../dist/sdk/nextjs",
"@vercel/flags": "^2.6.0",
"next": "14.1.4",
"react": "^18.2.0",
"react-dom": "^18.2.0"
Expand Down
79 changes: 56 additions & 23 deletions e2e/nextjs/app-router/app/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6,67 +6,69 @@ __metadata:
cacheKey: 10

"@devcycle/bucketing@file:../../../../dist/lib/shared/bucketing::locator=app%40workspace%3A.":
version: 1.20.1
resolution: "@devcycle/bucketing@file:../../../../dist/lib/shared/bucketing#../../../../dist/lib/shared/bucketing::hash=eb1537&locator=app%40workspace%3A."
version: 1.21.0
resolution: "@devcycle/bucketing@file:../../../../dist/lib/shared/bucketing#../../../../dist/lib/shared/bucketing::hash=b36b1b&locator=app%40workspace%3A."
dependencies:
"@devcycle/types": "npm:^1.15.1"
"@devcycle/types": "npm:^1.16.0"
lodash: "npm:^4.17.21"
murmurhash: "npm:^2.0.0"
ua-parser-js: "npm:^1.0.36"
checksum: 8eda6f4a4850688daa24fccf60fe60ac75166b56936b531044544871f621e50a0d2a35cd335f4c7647ab9a711ab37b2a6ff2fba300eecab646ebec0abb5cb1fb
checksum: b139dd3fd1e5a17df583c3535177e21f0e917d6c906868c3e8c8ab9b11d818bcb1af2759888f8f380d6d1ba6f79ff9e4ec1207e1b3e1050705b805afe025977b
languageName: node
linkType: hard

"@devcycle/js-client-sdk@file:../../../../dist/sdk/js::locator=app%40workspace%3A.":
version: 1.27.2
resolution: "@devcycle/js-client-sdk@file:../../../../dist/sdk/js#../../../../dist/sdk/js::hash=64ee10&locator=app%40workspace%3A."
version: 1.28.0
resolution: "@devcycle/js-client-sdk@file:../../../../dist/sdk/js#../../../../dist/sdk/js::hash=732860&locator=app%40workspace%3A."
dependencies:
"@devcycle/types": "npm:^1.15.1"
"@devcycle/types": "npm:^1.16.0"
fetch-retry: "npm:^5.0.6"
lodash: "npm:^4.17.21"
ua-parser-js: "npm:^1.0.36"
uuid: "npm:^8.3.2"
checksum: f5935ed79a48037ad6196baaefa503969af5bcf666ff77c91c4e9b6ab56e2afa80c61bab2d5ad1de8b05932231384abc0b1657af4e0e88315ca14cc2c7863b06
checksum: b6badef6a53fb98d59e39f96d4e5c0d9101737ea25c83ab7d1f11d3d9e1f924f212db5ead2d22ef11a807f1ebfa8e48194406fec5cd2a4226bfbbbbd75808d30
languageName: node
linkType: hard

"@devcycle/nextjs-sdk@file:../../../../dist/sdk/nextjs::locator=app%40workspace%3A.":
version: 2.2.3
resolution: "@devcycle/nextjs-sdk@file:../../../../dist/sdk/nextjs#../../../../dist/sdk/nextjs::hash=87eaec&locator=app%40workspace%3A."
version: 2.3.0
resolution: "@devcycle/nextjs-sdk@file:../../../../dist/sdk/nextjs#../../../../dist/sdk/nextjs::hash=8381c4&locator=app%40workspace%3A."
dependencies:
"@devcycle/bucketing": "npm:^1.20.1"
"@devcycle/js-client-sdk": "npm:^1.27.2"
"@devcycle/react-client-sdk": "npm:^1.25.2"
"@devcycle/types": "npm:^1.15.1"
"@devcycle/bucketing": "npm:^1.21.0"
"@devcycle/js-client-sdk": "npm:^1.28.0"
"@devcycle/react-client-sdk": "npm:^1.26.0"
"@devcycle/types": "npm:^1.16.0"
hoist-non-react-statics: "npm:^3.3.2"
server-only: "npm:^0.0.1"
checksum: 7df9a63d591febf951f20284bc4f0ba3f0502ceaf0fa185df19df1c6acc1fe49394114528b20aa804ccc1ba4a07a383a03926de1ff515bba491b1c46e054c0b0
peerDependencies:
"@vercel/flags": ^2.6.0
checksum: 2c8150c427aa00d3fb047288039e379231ac1660f374410cf14ef5feda306b63139fd7b149f841575dc666fa37c968cb35521b06130c65e8728a83ff75088a57
languageName: node
linkType: hard

"@devcycle/react-client-sdk@file:../../../../dist/sdk/react::locator=app%40workspace%3A.":
version: 1.25.2
resolution: "@devcycle/react-client-sdk@file:../../../../dist/sdk/react#../../../../dist/sdk/react::hash=325d5b&locator=app%40workspace%3A."
version: 1.26.0
resolution: "@devcycle/react-client-sdk@file:../../../../dist/sdk/react#../../../../dist/sdk/react::hash=b0d87c&locator=app%40workspace%3A."
dependencies:
"@devcycle/js-client-sdk": "npm:^1.27.2"
"@devcycle/types": "npm:^1.15.1"
"@devcycle/js-client-sdk": "npm:^1.28.0"
"@devcycle/types": "npm:^1.16.0"
hoist-non-react-statics: "npm:^3.3.2"
peerDependencies:
react: ">=16.8.0"
checksum: 8f52bba5ae5f11f323e3dceb9c9081ac2ff2c48db0abca2bdea8626907546f3b53d57afd5794db69089b8b39668bc50188693a105c743229e507f2f3951c08a0
checksum: 106509453887e09f429d369bcf8b325f9a3287c596b739566d36c52bf5958e0ae770ebdfdd979093458a74e039daeae1e4c5fd0ec5c94f5195f27784620f5a4c
languageName: node
linkType: hard

"@devcycle/types@file:../../../../dist/lib/shared/types::locator=app%40workspace%3A.":
version: 1.15.1
resolution: "@devcycle/types@file:../../../../dist/lib/shared/types#../../../../dist/lib/shared/types::hash=42e659&locator=app%40workspace%3A."
version: 1.16.0
resolution: "@devcycle/types@file:../../../../dist/lib/shared/types#../../../../dist/lib/shared/types::hash=3c371a&locator=app%40workspace%3A."
dependencies:
class-transformer: "npm:0.5.1"
class-validator: "npm:0.14.1"
iso-639-1: "npm:^2.1.13"
lodash: "npm:^4.17.21"
reflect-metadata: "npm:^0.1.13"
checksum: 33e5cab6d0056329bf55d7ee15019e2ad8fcef16d3fe6b6593ed14ae0deafa6d3cdf3e2b391727195443a60dec27858d88db72413038ff7c22d378cf39dd512d
checksum: 8bedb0432d5eca94ef30180116260a5dcb463127bf6efbce401da1928a76bef1ea84cffbc07f62c00c8ca70a3c614e99c52575f2e492cb0fe2a34f85cc153d9f
languageName: node
linkType: hard

Expand Down Expand Up @@ -199,6 +201,29 @@ __metadata:
languageName: node
linkType: hard

"@vercel/flags@npm:^2.6.0":
version: 2.6.0
resolution: "@vercel/flags@npm:2.6.0"
dependencies:
jose: "npm:5.2.1"
peerDependencies:
"@sveltejs/kit": "*"
next: "*"
react: "*"
react-dom: "*"
peerDependenciesMeta:
"@sveltejs/kit":
optional: true
next:
optional: true
react:
optional: true
react-dom:
optional: true
checksum: 31a70385d3f40dd4f922f2425d4f8fc3b22736282716503ee699739301e4f3c8706ac87b68707a2942399f56f3d6ceaa513edd1284ac6d144efc0bae89e5e72c
languageName: node
linkType: hard

"app@workspace:.":
version: 0.0.0-use.local
resolution: "app@workspace:."
Expand All @@ -207,6 +232,7 @@ __metadata:
"@types/node": "npm:^20"
"@types/react": "npm:18.2.20"
"@types/react-dom": "npm:18.2.6"
"@vercel/flags": "npm:^2.6.0"
next: "npm:14.1.4"
react: "npm:^18.2.0"
react-dom: "npm:^18.2.0"
Expand Down Expand Up @@ -292,6 +318,13 @@ __metadata:
languageName: node
linkType: hard

"jose@npm:5.2.1":
version: 5.2.1
resolution: "jose@npm:5.2.1"
checksum: a3526af02f1ea713b48743aadb4198080c8eb3281d53907224e70021d8c02e69ae43e27ace4ef9a196fec8fa6fa22272e2a6aa035cba16adf631af36564a07d2
languageName: node
linkType: hard

"js-tokens@npm:^3.0.0 || ^4.0.0":
version: 4.0.0
resolution: "js-tokens@npm:4.0.0"
Expand Down
2 changes: 2 additions & 0 deletions e2e/nextjs/app-router/tests/app-router.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ test('has expected page elements', async ({ page }) => {
await expect(
page.getByText('Client Component Conditionally Bundled'),
).toBeVisible()

await expect(page.getByText('Vercel Flag: true')).toBeVisible()
})

test('works after a client side navigation', async ({ page }) => {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@swc/helpers": "~0.5.2",
"@types/express": "^4.17.17",
"@vercel/edge-config": "^1.2.0",
"@vercel/flags": "^2.6.0",
"async": "^3.2.1",
"bootstrap": "5.1.3",
"class-transformer": "0.5.1",
Expand Down
10 changes: 7 additions & 3 deletions sdk/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,26 @@
"hoist-non-react-statics": "^3.3.2",
"server-only": "^0.0.1"
},
"peerDependencies": {
"@vercel/flags": "^2.6.0"
},
"types": "./src/index.d.ts",
"exports": {
".": {
"import": "./index.js",
"require": "./index.js",
"types": "./index.d.ts"
},
"./server": {
"import": "./server.js",
"require": "./server.js",
"types": "./server.d.ts"
},
"./pages": {
"import": "./pages.js",
"require": "./pages.js",
"types": "./pages.d.ts"
},
"./vercel": {
"import": "./vercel.js",
"types": "./vercel.d.ts"
}
}
}
48 changes: 33 additions & 15 deletions sdk/nextjs/src/server/bucketing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ const generateBucketedConfigCached = cache(
)
return {
bucketedConfig: {
...generateBucketedConfig({ user: populatedUser, config }),
...generateBucketedConfig({
user: populatedUser,
config,
}),
// clientSDKKey is always defined for bootstrap config
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
clientSDKKey: config.clientSDKKey!,
Expand All @@ -41,6 +44,17 @@ const generateBucketedConfigCached = cache(
},
)

const cachedConfig = cache(async (cdnConfig: Response) => {
if (!cdnConfig.ok) {
const responseText = await cdnConfig.text()
throw new Error('Could not fetch config: ' + responseText)
}
return {
config: await cdnConfig.json(),
lastModified: cdnConfig.headers.get('last-modified'),
}
})

class CDNConfigSource extends ConfigSource {
constructor(private clientSDKKey: string) {
super()
Expand All @@ -52,17 +66,24 @@ class CDNConfigSource extends ConfigSource {
this.clientSDKKey,
obfuscated,
)
if (!cdnConfig.ok) {
const responseText = await cdnConfig.text()
throw new Error('Could not fetch config: ' + responseText)
}
return {
config: await cdnConfig.json(),
lastModified: cdnConfig.headers.get('last-modified'),
}
return cachedConfig(cdnConfig)
}
}

export const getProjectConfig = async (
sdkKey: string,
clientSDKKey: string,
options: DevCycleNextOptions,
): Promise<{ config: ConfigBody; lastModified: string | null }> => {
const cdnConfigSource = new CDNConfigSource(clientSDKKey)
const configSource = options.configSource ?? cdnConfigSource
return await configSource.getConfig(
sdkKey,
'bootstrap',
!!options.enableObfuscation,
)
}

/**
* Retrieve the config from CDN for the current request's SDK Key. This data will often be cached
* Compute the bucketed config for the current request's user using that data, with local bucketing library
Expand All @@ -75,13 +96,10 @@ export const getBucketedConfig = async (
options: DevCycleNextOptions,
userAgent?: string,
): Promise<BucketedConfigWithAdditionalFields> => {
const cdnConfigSource = new CDNConfigSource(clientSDKKey)

const configSource = options.configSource ?? cdnConfigSource
const { config, lastModified } = await configSource.getConfig(
const { config, lastModified } = await getProjectConfig(
sdkKey,
'bootstrap',
!!options.enableObfuscation,
clientSDKKey,
options,
)

const { bucketedConfig } = await generateBucketedConfigCached(
Expand Down
6 changes: 6 additions & 0 deletions sdk/nextjs/src/server/setupDevCycle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getUserAgent } from './userAgent'
import { getAllVariables } from './getAllVariables'
import { getAllFeatures } from './allFeatures'
import { DevCycleNextOptions } from '../common/types'
import { getProjectConfig } from './bucketing'

// server-side users must always be "identified" with a user id
type ServerUser = Omit<DevCycleUser, 'user_id' | 'isAnonymous'> & {
Expand Down Expand Up @@ -46,6 +47,10 @@ export const setupDevCycle = ({
return getAllFeatures()
}

const _getConfig = async () => {
return getProjectConfig(serverSDKKey, clientSDKKey, options)
}

const _getClientContext = () => {
const serverDataPromise = initialize(
serverSDKKey,
Expand Down Expand Up @@ -89,5 +94,6 @@ export const setupDevCycle = ({
getAllVariables: _getAllVariables,
getAllFeatures: _getAllFeatures,
getClientContext: _getClientContext,
getConfig: _getConfig,
}
}
1 change: 1 addition & 0 deletions sdk/nextjs/tsconfig.lib.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"index.ts",
"pages.ts",
"server.ts",
"vercel.ts",
"next-env.d.ts"
]
}
Loading
Loading