Unify layout and fix tracking of ask_question (#3610)

This commit is contained in:
Samy Pessé
2025-08-29 16:10:28 +02:00
committed by GitHub
parent abbae3ec4d
commit 4f3588240c
8 changed files with 179 additions and 121 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Fix event ask_question not being tracked
@@ -5,6 +5,8 @@ import {
generateEmbeddableViewport,
} from '@/components/Embeddable';
import { getEmbeddableStaticContext } from '@/lib/embeddable';
import { shouldTrackEvents } from '@/lib/tracking';
import { headers } from 'next/headers';
interface SiteStaticLayoutProps {
params: Promise<RouteLayoutParams>;
@@ -14,9 +16,18 @@ export default async function RootLayout({
params,
children,
}: React.PropsWithChildren<SiteStaticLayoutProps>) {
const { context } = await getEmbeddableStaticContext(await params);
const { context, visitorAuthClaims } = await getEmbeddableStaticContext(await params);
const withTracking = shouldTrackEvents(await headers());
return <EmbeddableRootLayout context={context}>{children}</EmbeddableRootLayout>;
return (
<EmbeddableRootLayout
context={context}
withTracking={withTracking}
visitorAuthClaims={visitorAuthClaims}
>
{children}
</EmbeddableRootLayout>
);
}
export async function generateViewport({ params }: SiteStaticLayoutProps) {
@@ -5,6 +5,7 @@ import {
generateEmbeddableViewport,
} from '@/components/Embeddable';
import { getEmbeddableStaticContext } from '@/lib/embeddable';
import { shouldTrackEvents } from '@/lib/tracking';
interface SiteStaticLayoutProps {
params: Promise<RouteLayoutParams>;
@@ -14,9 +15,18 @@ export default async function RootLayout({
params,
children,
}: React.PropsWithChildren<SiteStaticLayoutProps>) {
const { context } = await getEmbeddableStaticContext(await params);
const { context, visitorAuthClaims } = await getEmbeddableStaticContext(await params);
const withTracking = shouldTrackEvents();
return <EmbeddableRootLayout context={context}>{children}</EmbeddableRootLayout>;
return (
<EmbeddableRootLayout
context={context}
withTracking={withTracking}
visitorAuthClaims={visitorAuthClaims}
>
{children}
</EmbeddableRootLayout>
);
}
export async function generateViewport({ params }: SiteStaticLayoutProps) {
@@ -1,2 +1,3 @@
export * from './useAI';
export * from './useAIChat';
export type { RenderAIMessageOptions } from './server-actions';
@@ -1,16 +1,20 @@
import { AIChatProvider, AIContextProvider } from '@/components/AI';
import { AIContextProvider } from '@/components/AI';
import { CustomizationRootLayout } from '@/components/RootLayout';
import {
SiteLayoutClientContexts,
generateSiteLayoutMetadata,
generateSiteLayoutViewport,
} from '@/components/SiteLayout';
import type { VisitorAuthClaims } from '@/lib/adaptive';
import type { GitBookSiteContext } from '@/lib/context';
import { CustomizationAIMode } from '@gitbook/api';
import { SpaceLayoutServerContext } from '../SpaceLayout';
import { EmbeddableIframeAPI } from './EmbeddableIframeAPI';
type EmbeddableRootLayoutProps = {
context: GitBookSiteContext;
withTracking: boolean;
visitorAuthClaims: VisitorAuthClaims;
};
/**
@@ -18,6 +22,8 @@ type EmbeddableRootLayoutProps = {
*/
export async function EmbeddableRootLayout({
context,
withTracking,
visitorAuthClaims,
children,
}: React.PropsWithChildren<EmbeddableRootLayoutProps>) {
return (
@@ -31,8 +37,11 @@ export async function EmbeddableRootLayout({
aiMode={CustomizationAIMode.Assistant}
trademark={context.customization.trademark.enabled}
>
<AIChatProvider
renderMessageOptions={{
<SpaceLayoutServerContext
context={context}
withTracking={withTracking}
visitorAuthClaims={visitorAuthClaims}
aiChatRenderMessageOptions={{
withLinkPreviews: false,
asEmbeddable: true,
}}
@@ -41,17 +50,17 @@ export async function EmbeddableRootLayout({
<EmbeddableIframeAPI
baseURL={context.linker.toPathInSpace('~gitbook/embed/')}
/>
</AIChatProvider>
</SpaceLayoutServerContext>
</AIContextProvider>
</SiteLayoutClientContexts>
</CustomizationRootLayout>
);
}
export async function generateEmbeddableViewport({ context }: EmbeddableRootLayoutProps) {
export async function generateEmbeddableViewport({ context }: { context: GitBookSiteContext }) {
return generateSiteLayoutViewport(context);
}
export async function generateEmbeddableMetadata({ context }: EmbeddableRootLayoutProps) {
export async function generateEmbeddableMetadata({ context }: { context: GitBookSiteContext }) {
return generateSiteLayoutMetadata(context);
}
@@ -46,7 +46,9 @@ type TrackEventCallback = <EventName extends InsightsEventName>(
options?: InsightsEventOptions
) => void;
const InsightsContext = React.createContext<TrackEventCallback>(() => {});
const InsightsContext = React.createContext<TrackEventCallback>(() => {
console.error('useTrackEvent must be used within an InsightsProvider');
});
interface InsightsProviderProps {
/** If true, the events will be sent to the server. */
@@ -13,7 +13,7 @@ import { buildVersion } from '@/lib/build';
import { GITBOOK_API_PUBLIC_URL, GITBOOK_ASSETS_URL, GITBOOK_ICONS_URL } from '@/lib/env';
import { getResizedImageURL } from '@/lib/images';
import { isSiteIndexable } from '@/lib/seo';
import { AIChatProvider, AIContextProvider } from '../AI';
import { AIContextProvider } from '../AI';
import { RocketLoaderDetector } from './RocketLoaderDetector';
import { SiteLayoutClientContexts } from './SiteLayoutClientContexts';
@@ -58,15 +58,13 @@ export async function SiteLayout(props: {
aiMode={customization.ai?.mode}
trademark={customization.trademark.enabled}
>
<AIChatProvider>
<SpaceLayout
context={context}
withTracking={withTracking}
visitorAuthClaims={visitorAuthClaims}
>
{children}
</SpaceLayout>
</AIChatProvider>
<SpaceLayout
context={context}
withTracking={withTracking}
visitorAuthClaims={visitorAuthClaims}
>
{children}
</SpaceLayout>
</AIContextProvider>
{scripts.length > 0 ? (
@@ -14,6 +14,8 @@ import { tcls } from '@/lib/tailwind';
import type { VisitorAuthClaims } from '@/lib/adaptive';
import { GITBOOK_APP_URL } from '@/lib/env';
import { AIChatProvider } from '../AI';
import type { RenderAIMessageOptions } from '../AI';
import { AIChat } from '../AIChat';
import { Announcement } from '../Announcement';
import { SpacesDropdown } from '../Header/SpacesDropdown';
@@ -23,10 +25,7 @@ import { SiteSectionList, encodeClientSiteSections } from '../SiteSections';
import { CurrentContentProvider } from '../hooks';
import { SpaceLayoutContextProvider } from './SpaceLayoutContext';
/**
* Render the entire layout of the space (header, table of contents, footer).
*/
export function SpaceLayout(props: {
type SpaceLayoutProps = {
context: GitBookSiteContext;
/** Whether to enable tracking of events into site insights. */
@@ -35,22 +34,21 @@ export function SpaceLayout(props: {
/** The visitor auth claims. */
visitorAuthClaims: VisitorAuthClaims;
/** The options for rendering AI messages. */
aiChatRenderMessageOptions?: RenderAIMessageOptions;
/** The children of the layout. */
children: React.ReactNode;
}) {
const { context, withTracking, visitorAuthClaims, children } = props;
const { siteSpace, customization, sections, siteSpaces } = context;
};
const withTopHeader = customization.header.preset !== CustomizationHeaderPreset.None;
/**
* Provide all contexts for a space.
*/
export function SpaceLayoutServerContext(props: SpaceLayoutProps) {
const { context, withTracking, visitorAuthClaims, aiChatRenderMessageOptions, children } =
props;
const withSections = Boolean(sections && sections.list.length > 1);
const isMultiVariants = Boolean(siteSpaces.length > 1);
const withFooter =
customization.themes.toggeable ||
customization.footer.copyright ||
customization.footer.logo ||
customization.footer.groups?.length;
const { customization } = context;
const eventUrl = new URL(
context.linker.toAbsoluteURL(context.linker.toPathInSite('/~gitbook/__evt'))
@@ -76,92 +74,116 @@ export function SpaceLayout(props: {
eventUrl={eventUrl.toString()}
visitorCookieTrackingEnabled={customization.insights?.trackingCookie}
>
<Announcement context={context} />
<Header withTopHeader={withTopHeader} context={context} />
{customization.ai?.mode === CustomizationAIMode.Assistant ? (
<AIChat trademark={customization.trademark.enabled} />
) : null}
<div className="motion-safe:transition-all motion-safe:duration-300 lg:chat-open:mr-80 xl:chat-open:mr-96">
<div
className={tcls(
'flex',
'flex-col',
'lg:flex-row',
CONTAINER_STYLE,
'site-width-wide:max-w-full',
// Ensure the footer is display below the viewport even if the content is not enough
withFooter && [
'site-header:min-h-[calc(100vh-64px)]',
'site-header-sections:min-h-[calc(100vh-108px)]',
],
withTopHeader ? null : 'lg:min-h-screen'
)}
>
<TableOfContents
context={context}
header={
withTopHeader ? null : (
<div
className={tcls(
'hidden',
'pr-4',
'lg:flex',
'grow-0',
'flex-wrap',
'dark:shadow-light/1',
'text-base/tight'
)}
>
<HeaderLogo context={context} />
</div>
)
}
innerHeader={
// displays the search button and/or the space dropdown in the ToC according to the header/variant settings. E.g if there is no header, the search button will be displayed in the ToC.
<>
{!withTopHeader && (
<SearchContainer
style={CustomizationSearchStyle.Subtle}
isMultiVariants={siteSpaces.length > 1}
spaceTitle={siteSpace.title}
siteSpaceId={siteSpace.id}
className="max-lg:hidden"
viewport="desktop"
/>
)}
{!withTopHeader && withSections && sections && (
<SiteSectionList
className={tcls('hidden', 'lg:block')}
sections={encodeClientSiteSections(
context,
sections
)}
/>
)}
{isMultiVariants && !sections && (
<SpacesDropdown
context={context}
siteSpace={siteSpace}
siteSpaces={siteSpaces}
className={tcls(
'w-full',
'page-no-toc:hidden',
'page-no-toc:site-header-none:flex'
)}
/>
)}
</>
}
/>
<div className="flex min-w-0 flex-1 flex-col">{children}</div>
</div>
</div>
{withFooter ? <Footer context={context} /> : null}
<AIChatProvider renderMessageOptions={aiChatRenderMessageOptions}>
{children}
</AIChatProvider>
</InsightsProvider>
</CurrentContentProvider>
</SpaceLayoutContextProvider>
);
}
/**
* Render the entire layout of the space (header, table of contents, footer).
*/
export function SpaceLayout(props: SpaceLayoutProps) {
const { context, children } = props;
const { siteSpace, customization, sections, siteSpaces } = context;
const withTopHeader = customization.header.preset !== CustomizationHeaderPreset.None;
const withSections = Boolean(sections && sections.list.length > 1);
const isMultiVariants = Boolean(siteSpaces.length > 1);
const withFooter =
customization.themes.toggeable ||
customization.footer.copyright ||
customization.footer.logo ||
customization.footer.groups?.length;
return (
<SpaceLayoutServerContext {...props}>
<Announcement context={context} />
<Header withTopHeader={withTopHeader} context={context} />
{customization.ai?.mode === CustomizationAIMode.Assistant ? (
<AIChat trademark={customization.trademark.enabled} />
) : null}
<div className="motion-safe:transition-all motion-safe:duration-300 lg:chat-open:mr-80 xl:chat-open:mr-96">
<div
className={tcls(
'flex',
'flex-col',
'lg:flex-row',
CONTAINER_STYLE,
'site-width-wide:max-w-full',
// Ensure the footer is display below the viewport even if the content is not enough
withFooter && [
'site-header:min-h-[calc(100vh-64px)]',
'site-header-sections:min-h-[calc(100vh-108px)]',
],
withTopHeader ? null : 'lg:min-h-screen'
)}
>
<TableOfContents
context={context}
header={
withTopHeader ? null : (
<div
className={tcls(
'hidden',
'pr-4',
'lg:flex',
'grow-0',
'flex-wrap',
'dark:shadow-light/1',
'text-base/tight'
)}
>
<HeaderLogo context={context} />
</div>
)
}
innerHeader={
// displays the search button and/or the space dropdown in the ToC according to the header/variant settings. E.g if there is no header, the search button will be displayed in the ToC.
<>
{!withTopHeader && (
<SearchContainer
style={CustomizationSearchStyle.Subtle}
isMultiVariants={siteSpaces.length > 1}
spaceTitle={siteSpace.title}
siteSpaceId={siteSpace.id}
className="max-lg:hidden"
viewport="desktop"
/>
)}
{!withTopHeader && withSections && sections && (
<SiteSectionList
className={tcls('hidden', 'lg:block')}
sections={encodeClientSiteSections(context, sections)}
/>
)}
{isMultiVariants && !sections && (
<SpacesDropdown
context={context}
siteSpace={siteSpace}
siteSpaces={siteSpaces}
className={tcls(
'w-full',
'page-no-toc:hidden',
'page-no-toc:site-header-none:flex'
)}
/>
)}
</>
}
/>
<div className="flex min-w-0 flex-1 flex-col">{children}</div>
</div>
</div>
{withFooter ? <Footer context={context} /> : null}
</SpaceLayoutServerContext>
);
}