Skip to content

Commit 4e2788a

Browse files
committed
embed clientInitializeResponse and statsigUser
1 parent b17b9c7 commit 4e2788a

File tree

6 files changed

+95
-64
lines changed

6 files changed

+95
-64
lines changed

flags-sdk/experimentation-statsig/app/api/bootstrap/route.ts

Lines changed: 0 additions & 29 deletions
This file was deleted.

flags-sdk/experimentation-statsig/app/layout.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ export default function RootLayout({
2222
{children}
2323
<Toaster />
2424
<VercelToolbar />
25+
<script
26+
id="embed"
27+
// biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>
28+
dangerouslySetInnerHTML={{ __html: '' }}
29+
suppressHydrationWarning
30+
type="application/json"
31+
/>
2532
</body>
2633
</html>
2734
)
Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
import { type NextRequest, NextResponse } from 'next/server'
1+
import type { NextRequest } from 'next/server'
22
import { precompute } from 'flags/next'
33
import { productFlags } from '@/flags'
44
import { getStableId } from './lib/get-stable-id'
55
import { getCartId } from './lib/get-cart-id'
6+
import { HTMLRewriter } from 'htmlrewriter'
7+
import { statsigAdapter } from '@flags-sdk/statsig'
8+
import { identify } from './lib/identify'
9+
import { safeJsonStringify } from 'flags'
610

711
export const config = {
812
matcher: ['/', '/cart'],
@@ -19,19 +23,47 @@ export async function middleware(request: NextRequest) {
1923
request.url
2024
)
2125

22-
// Add a header to the request to indicate that the stable id is generated,
23-
// as it will not be present on the cookie request header on the first-ever request.
26+
// Create new headers with the original request headers
27+
const headers = new Headers(request.headers)
28+
29+
// Add new headers if needed
2430
if (cartId.isFresh) {
25-
request.headers.set('x-generated-cart-id', cartId.value)
31+
headers.set('x-generated-cart-id', cartId.value)
2632
}
2733

2834
if (stableId.isFresh) {
29-
request.headers.set('x-generated-stable-id', stableId.value)
35+
headers.set('x-generated-stable-id', stableId.value)
3036
}
3137

32-
// response headers
33-
const headers = new Headers()
34-
headers.append('set-cookie', `stable-id=${stableId.value}`)
35-
headers.append('set-cookie', `cart-id=${cartId.value}`)
36-
return NextResponse.rewrite(nextUrl, { request, headers })
38+
// Create a new request with the modified headers
39+
const modifiedRequest = new Request(nextUrl, { ...request, headers })
40+
41+
const [statsig, statsigUser] = await Promise.all([
42+
statsigAdapter.initialize(),
43+
identify(),
44+
])
45+
const clientInitializeResponse = statsig.getClientInitializeResponse(
46+
statsigUser,
47+
{ hash: 'djb2' }
48+
)
49+
50+
const response = await fetch(modifiedRequest)
51+
const rewriter = new HTMLRewriter()
52+
rewriter.on('script#embed', {
53+
element(element) {
54+
element.setInnerContent(
55+
safeJsonStringify({ clientInitializeResponse, statsigUser }),
56+
{ html: true }
57+
)
58+
// element.setAttribute('style', 'display: block')
59+
},
60+
})
61+
const modifiedResponse = rewriter.transform(response)
62+
const h = new Headers(modifiedResponse.headers)
63+
h.append('set-cookie', `stable-id=${stableId.value}`)
64+
h.append('set-cookie', `cart-id=${cartId.value}`)
65+
return new Response(modifiedResponse.body, {
66+
...modifiedResponse,
67+
headers: h,
68+
})
3769
}

flags-sdk/experimentation-statsig/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@
2121
"@vercel/edge": "1.2.1",
2222
"@vercel/edge-config": "1.4.0",
2323
"@vercel/toolbar": "0.1.33",
24-
"framer-motion": "12.4.7",
2524
"clsx": "2.1.1",
2625
"flags": "^3.1.1",
26+
"framer-motion": "12.4.7",
27+
"htmlrewriter": "^0.0.12",
2728
"nanoid": "5.1.2",
2829
"next": "15.2.1-canary.4",
2930
"react": "^19.0.0",

flags-sdk/experimentation-statsig/pnpm-lock.yaml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

flags-sdk/experimentation-statsig/statsig/statsig-provider.tsx

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
'use client'
22

3-
import { Statsig } from '@flags-sdk/statsig'
43
/**
54
* This file exports a StatsigProvider with a client-side bootstrap.
6-
* It requires a client-side fetch to retrieve the bootstrap payload.
5+
*
6+
* The bootstrap reads the data embedded by Edge Middleware.
7+
*
78
* Elements that determine page layout should have precomputed variants with flags-sdk.
89
* Exposures can be logged with helpers in the `statsig-exposure` module.
910
*/
10-
11+
import type { Statsig, StatsigUser } from '@flags-sdk/statsig'
1112
import {
1213
LogLevel,
1314
StatsigProvider,
1415
useClientBootstrapInit,
1516
} from '@statsig/react-bindings'
1617
import { StatsigAutoCapturePlugin } from '@statsig/web-analytics'
17-
import { createContext, useMemo } from 'react'
18-
import useSWR from 'swr'
18+
import { createContext, useMemo, useState, useEffect } from 'react'
1919

2020
export const StatsigAppBootstrapContext = createContext<{
2121
isLoading: boolean
@@ -25,6 +25,16 @@ export const StatsigAppBootstrapContext = createContext<{
2525
error: null,
2626
})
2727

28+
export function useEmbed<T>(id: string, initialData?: T) {
29+
const [data, setData] = useState<T | undefined>(initialData || undefined)
30+
useEffect(() => {
31+
// biome-ignore lint/style/noNonNullAssertion: <explanation>
32+
const text = document.getElementById(id)!.textContent
33+
setData(text ? JSON.parse(text) : undefined)
34+
}, [id])
35+
return data
36+
}
37+
2838
function BootstrappedStatsigProvider({
2939
user,
3040
values,
@@ -51,35 +61,37 @@ export function StaticStatsigProvider({
5161
}: {
5262
children: React.ReactNode
5363
}) {
54-
const { data, error } = useBootstrap()
55-
const values = useMemo(() => JSON.stringify(data), [data])
64+
// wait for the script#embed to appear and read its contents as json
65+
// TODO use actual embed library
66+
const data = useEmbed<{
67+
statsigUser: StatsigUser
68+
clientInitializeResponse: Awaited<
69+
ReturnType<typeof Statsig.getClientInitializeResponse>
70+
>
71+
}>('embed')
72+
73+
const values = useMemo(
74+
() => (data ? JSON.stringify(data.clientInitializeResponse) : null),
75+
[data]
76+
)
5677

57-
if (!data) {
78+
if (!data || !values) {
5879
return (
59-
<StatsigAppBootstrapContext.Provider value={{ isLoading: true, error }}>
80+
<StatsigAppBootstrapContext.Provider
81+
value={{ isLoading: true, error: null }}
82+
>
6083
{children}
6184
</StatsigAppBootstrapContext.Provider>
6285
)
6386
}
6487

6588
return (
66-
<StatsigAppBootstrapContext.Provider value={{ isLoading: false, error }}>
67-
<BootstrappedStatsigProvider user={data.user} values={values}>
89+
<StatsigAppBootstrapContext.Provider
90+
value={{ isLoading: false, error: null }}
91+
>
92+
<BootstrappedStatsigProvider user={data.statsigUser} values={values}>
6893
{children}
6994
</BootstrappedStatsigProvider>
7095
</StatsigAppBootstrapContext.Provider>
7196
)
7297
}
73-
74-
const fetcher = (url: string) =>
75-
fetch(url, {
76-
headers: {
77-
Vary: 'Cookie',
78-
},
79-
}).then((res) => res.json())
80-
81-
export function useBootstrap() {
82-
return useSWR<
83-
Awaited<ReturnType<typeof Statsig.getClientInitializeResponse>>
84-
>('/api/bootstrap', fetcher)
85-
}

0 commit comments

Comments
 (0)