Skip to content
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
6 changes: 6 additions & 0 deletions .syncpackrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@
"dependencies": ["radix-ui"],
"pinVersion": "latest"
},
{
"label": "Geistdocs uses the analytics canary for experiment exposures",
"packages": ["docs"],
"dependencies": ["@vercel/analytics"],
"pinVersion": "2.1.0-canary.5"
},
{
"label": "Next.js 15 compatibility fixture must stay on 15.x",
"packages": ["next-15"],
Expand Down
10 changes: 9 additions & 1 deletion apps/docs/app/[lang]/docs/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { MobileDocsBar } from "@vercel/geistdocs/mobile-docs-bar";
import { createDocsPage } from "@vercel/geistdocs/pages/docs";
import type { MDXComponents } from "mdx/types";
import { getMDXComponents } from "@/components/geistdocs/mdx-components";
import { FlagsAsCodeTitleExperiment } from "@/components/custom/flags-as-code-title-experiment";
import { IframeBrowser } from "@/components/custom/iframe-browser";
import { LearnMore } from "@/components/custom/learn-more";
import { ProviderList } from "@/components/custom/provider-list";
Expand All @@ -28,7 +29,14 @@ const docsPage = createDocsPage({
tableOfContentPopover: {
enabled: false,
},
renderTop: ({ data }) => <MobileDocsBar toc={data.toc} />,
renderTop: ({ data, page }) => (
<>
<MobileDocsBar toc={data.toc} />
{page.slugs.join("/") === "principles/flags-as-code" ? (
<FlagsAsCodeTitleExperiment />
) : null}
</>
),
});

export default docsPage.Page;
Expand Down
6 changes: 6 additions & 0 deletions apps/docs/app/actions/flags-as-code-title/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
'use server';

import { flagsAsCodeTitleFlag } from '@/experiment-flags';

export const getFlagsAsCodeTitle = async (): Promise<string> =>
flagsAsCodeTitleFlag();
72 changes: 72 additions & 0 deletions apps/docs/components/custom/flags-as-code-title-experiment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use client';

import { track } from '@vercel/analytics';
import { useEffect } from 'react';
import { getFlagsAsCodeTitle } from '@/app/actions/flags-as-code-title';

// Keep in sync with VISITOR_ID_COOKIE in experiment-flags.ts.
const VISITOR_ID_COOKIE = 'flags-visitor-id';
const oneYearInSeconds = 31_536_000;

const getVisitorId = (): string => {
const existing = document.cookie
.split('; ')
.find((entry) => entry.startsWith(`${VISITOR_ID_COOKIE}=`))
?.split('=')[1];
if (existing) {
return existing;
}
const id = crypto.randomUUID();
document.cookie = `${VISITOR_ID_COOKIE}=${id}; max-age=${oneYearInSeconds}; path=/; samesite=lax`;
return id;
};

const isCopyPageButton = (button: HTMLButtonElement): boolean => {
if (button.getAttribute('aria-label') === 'Copy page') {
return true;
}
const text = button.textContent?.trim() ?? '';
return text.startsWith('Copy page') || text.startsWith('Copy for LLM');
};

/**
* Swaps the page H1 for the treatment variant and tracks presses on the
* "Copy page" button for the `flags-as-code-title-experiment` experiment.
*/
export const FlagsAsCodeTitleExperiment = () => {
useEffect(() => {
const visitorId = getVisitorId();
let cancelled = false;

getFlagsAsCodeTitle()
.then((title) => {
if (cancelled) {
return;
}
const heading = document.querySelector(
'[data-geistdocs-article="title"]',
);
if (heading && heading.textContent !== title) {
heading.textContent = title;
}
})
.catch(() => {
// Keep the default title when the flag can not be evaluated.
});

const onClick = (event: MouseEvent) => {
const button = (event.target as Element | null)?.closest('button');
if (button && isCopyPageButton(button)) {
track('copy_page_clicked', { visitorId });
}
};
document.addEventListener('click', onClick, { capture: true });

return () => {
cancelled = true;
document.removeEventListener('click', onClick, { capture: true });
};
}, []);

return null;
};
53 changes: 53 additions & 0 deletions apps/docs/experiment-flags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { createVercelAdapter } from '@flags-sdk/vercel';
import { trackExposure } from '@vercel/analytics/server';
import { createClient } from '@vercel/flags-core';
import { flag } from 'flags/next';
import { headers } from 'next/headers';

export const VISITOR_ID_COOKIE = 'flags-visitor-id';

type Entities = { visitor?: { id: string } };

const flagsClient = createClient<Entities>(process.env.FLAGS, {
// Joins exposures to `track()` events through `event_data.visitorId`.
experimental_reportExposures: async (exposures, entities) => {
const visitorId = entities.visitor?.id;
if (!visitorId) {
return;
}
const requestHeaders = await headers();
await Promise.all(
exposures.map((exposure) => {
if (!exposure.variantId) {
return undefined;
}
return trackExposure(
{
experimentId: exposure.experimentId,
variantId: exposure.variantId,
unitKey: 'event_data.visitorId',
unitValue: visitorId,
assignmentReason: exposure.assignmentReason,
rampId: exposure.rampId,
rampPercentage: exposure.rampPercentage,
},
{ headers: requestHeaders },
);
}),
);
},
});

const vercelAdapter = createVercelAdapter(flagsClient);

export const flagsAsCodeTitleFlag = flag<string, Entities>({
key: 'flags-as-code-title',
description: 'H1 of the Flags as Code docs page (experiment)',
options: ['Flags as Code', 'Flags as a Gift'],
defaultValue: 'Flags as Code',
adapter: vercelAdapter(),
identify({ cookies }) {
const id = cookies.get(VISITOR_ID_COOKIE)?.value;
return id ? { visitor: { id } } : {};
},
});
4 changes: 3 additions & 1 deletion apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
"translate": "npx @vercel/geistdocs translate"
},
"dependencies": {
"@vercel/analytics": "^1.6.1",
"@flags-sdk/vercel": "workspace:*",
"@vercel/analytics": "2.1.0-canary.5",
"@vercel/flags-core": "workspace:*",
"@vercel/geistdocs": "2.9.2",
"@vercel/speed-insights": "^1.3.1",
"@vercel/toolbar": "0.2.7",
Expand Down
Loading
Loading