Request insights and visitor endpoints relative to the served origin (#4603)

This commit is contained in:
Nolann B.
2026-09-16 20:54:20 +02:00
committed by GitHub
parent a9e6814744
commit b967b248e7
11 changed files with 58 additions and 15 deletions
@@ -0,0 +1,7 @@
---
"gitbook": patch
---
Fix analytics and adaptive content silently breaking on sites served from a different host than the one configured (apex vs www, domain alias, CDN). The insights and visitor-claims endpoints are now requested relative to the page's own origin instead of the configured host, which a prerendered page cannot know.
Keep the traffic of GitBook's own preview deployments out of the sites' analytics, now that those requests reach the events endpoint.
@@ -69,6 +69,7 @@ runs:
GITBOOK_RUNTIME: cloudflare
GITBOOK_BLOCK_SEARCH_INDEXATION: ${{ inputs.environment == 'preview' && 'true' || '' }}
GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE: ${{ inputs.environment == 'preview' && 'true' || '' }}
GITBOOK_DISABLE_INSIGHTS: ${{ inputs.environment == 'preview' && 'true' || '' }}
shell: bash
- name: Upload the DO worker
@@ -75,6 +75,7 @@ runs:
echo "GITBOOK_RUNTIME=vercel" >> .vercel/.env.${{ inputs.environment }}.local
echo "GITBOOK_BLOCK_SEARCH_INDEXATION=true" >> .vercel/.env.${{ inputs.environment }}.local
echo "GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE=true" >> .vercel/.env.${{ inputs.environment }}.local
echo "GITBOOK_DISABLE_INSIGHTS=true" >> .vercel/.env.${{ inputs.environment }}.local
echo "--- .vercel/.env.${{ inputs.environment }}.local after inject ---"
cat .vercel/.env.${{ inputs.environment }}.local
- name: Build Project Artifacts
+3
View File
@@ -29,3 +29,6 @@ yarn-error.log*
# Bun pack artifacts
packages/*/*.tgz
# Playwright MCP artifacts
.playwright-mcp/
+1
View File
@@ -94,6 +94,7 @@ const nextConfig = {
GITBOOK_RUNTIME: process.env.GITBOOK_RUNTIME,
GITBOOK_BLOCK_SEARCH_INDEXATION: process.env.GITBOOK_BLOCK_SEARCH_INDEXATION,
GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE: process.env.GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE,
GITBOOK_DISABLE_INSIGHTS: process.env.GITBOOK_DISABLE_INSIGHTS,
// Next.js envs
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY,
+2
View File
@@ -6,6 +6,7 @@ import {
GITBOOK_API_URL,
GITBOOK_APP_URL,
GITBOOK_ASSETS_URL,
GITBOOK_DISABLE_INSIGHTS,
GITBOOK_DISABLE_TRACKING,
GITBOOK_FONTS_URL,
GITBOOK_ICONS_URL,
@@ -35,6 +36,7 @@ export async function GET(_req: NextRequest) {
GITBOOK_INTEGRATIONS_HOST,
GITBOOK_INTEGRATIONS_CONTENT_HOST,
GITBOOK_DISABLE_TRACKING,
GITBOOK_DISABLE_INSIGHTS,
// Secret envs
GITBOOK_SECRET: !!GITBOOK_SECRET,
@@ -58,15 +58,15 @@ export function SpaceLayoutServerContext(props: SpaceLayoutProps) {
? context.linker.toPathInSite('~gitbook/auth/login')
: null;
const eventUrl = new URL(
context.linker.toAbsoluteURL(context.linker.toPathInSite('/~gitbook/__evt'))
);
eventUrl.searchParams.set('o', context.organizationId);
eventUrl.searchParams.set('s', context.site.id);
// Kept relative: a prerendered page has no request to read the host from, so an absolute URL
// pins the configured host and turns these fetches cross-origin when it differs (apex vs www).
const eventParams = new URLSearchParams({
o: context.organizationId,
s: context.site.id,
});
const eventUrl = `${context.linker.toPathInSite('/~gitbook/__evt')}?${eventParams}`;
const getVisitorClaimsUrl = context.linker.toAbsoluteURL(
context.linker.toPathInSite('/~gitbook/visitor')
);
const getVisitorClaimsUrl = context.linker.toPathInSite('/~gitbook/visitor');
return (
<SpaceLayoutContextProvider
@@ -92,7 +92,7 @@ export function SpaceLayoutServerContext(props: SpaceLayoutProps) {
appURL={GITBOOK_APP_URL}
visitorCookieTrackingEnabled={customization.insights?.trackingCookie}
>
<InsightsProvider enabled={withTracking} eventUrl={eventUrl.toString()}>
<InsightsProvider enabled={withTracking} eventUrl={eventUrl}>
<AIChatProvider
renderMessageOptions={aiChatRenderMessageOptions}
withPageFeedback={customization.feedback.enabled}
+6
View File
@@ -78,6 +78,12 @@ export const GITBOOK_DISABLE_TRACKING = Boolean(
!!process.env.GITBOOK_DISABLE_TRACKING || process.env.NODE_ENV !== 'production'
);
/**
* Whether insights events must not reach the sites' analytics, while the pages otherwise behave
* like production. Set on the preview deployments, whose traffic is e2e runs, not the sites'.
*/
export const GITBOOK_DISABLE_INSIGHTS = process.env.GITBOOK_DISABLE_INSIGHTS === 'true';
/**
* Hostname serving the integrations.
*/
+3 -5
View File
@@ -3,7 +3,7 @@ import type { headers as nextHeaders } from 'next/headers';
import type * as api from '@gitbook/api';
import { apiClient } from './data/api';
import { GITBOOK_DISABLE_TRACKING } from './env';
import { GITBOOK_DISABLE_INSIGHTS, GITBOOK_DISABLE_TRACKING } from './env';
import { getLogger } from './logger';
/**
@@ -18,9 +18,7 @@ export function shouldTrackEvents(headers?: Awaited<ReturnType<typeof nextHeader
return false;
}
const disableTrackingHeader = headers?.get('x-gitbook-disable-tracking');
if (disableTrackingHeader === 'true') {
if (headers?.get('x-gitbook-disable-tracking') === 'true') {
return false;
}
@@ -85,7 +83,7 @@ export async function trackServerInsightsEvents(args: {
`Tracking ${args.events.length} events at ${request.url} for site ${args.siteId} (enabled=${!GITBOOK_DISABLE_TRACKING})`
);
if (GITBOOK_DISABLE_TRACKING) {
if (GITBOOK_DISABLE_TRACKING || GITBOOK_DISABLE_INSIGHTS) {
return;
}
+4 -1
View File
@@ -34,7 +34,7 @@ import {
normalizeRequestURL,
throwIfDataError,
} from '@/lib/data';
import { isGitBookAssetsHostURL, isGitBookHostURL } from '@/lib/env';
import { GITBOOK_DISABLE_INSIGHTS, isGitBookAssetsHostURL, isGitBookHostURL } from '@/lib/env';
import { getImageResizingContextId } from '@/lib/images';
import { isAITrainingOrIndexingRequest } from '@/lib/indexing-crawlers';
import { MiddlewareHeaders } from '@/lib/middleware';
@@ -185,6 +185,9 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
//Forwards analytics events
if (siteRequestURL.pathname.endsWith('/~gitbook/__evt')) {
if (GITBOOK_DISABLE_INSIGHTS) {
return new Response(null, { status: 204 });
}
return await serveProxyAnalyticsEvent(request);
}
+21
View File
@@ -0,0 +1,21 @@
import { describe, expect, it } from 'bun:test';
import { getContentTestURL } from './utils';
describe('Insights', () => {
// The harness serves the site from BASE_URL while it is configured on gitbook.gitbook.io,
// reproducing the split (apex vs www, alias, CDN) that turns an absolute URL cross-origin.
const TEST_URL = getContentTestURL('https://gitbook.gitbook.io/test-gitbook-open');
it('should reference the insights endpoints relative to the served origin', async () => {
const response = await fetch(TEST_URL);
expect(response.status).toBe(200);
const html = await response.text();
for (const endpoint of ['__evt', 'visitor']) {
expect(html).toContain(`~gitbook/${endpoint}`);
expect(html).not.toMatch(new RegExp(`https?://[^"'\\\\\\s]*~gitbook/${endpoint}`));
}
});
});