From 380af10236609ce04ed67b6fedb6711de0b85d60 Mon Sep 17 00:00:00 2001 From: Peter White <1788320+peterwhite@users.noreply.github.com> Date: Tue, 29 Sep 2026 18:43:14 +0200 Subject: [PATCH] Send previousUrl with site insights events (#4640) --- .changeset/insights-previous-url.md | 5 ++++ bun.lock | 4 +-- package.json | 2 +- .../components/Insights/InsightsProvider.tsx | 27 +++++++++++++++++++ 4 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 .changeset/insights-previous-url.md diff --git a/.changeset/insights-previous-url.md b/.changeset/insights-previous-url.md new file mode 100644 index 000000000..d3ab5fd91 --- /dev/null +++ b/.changeset/insights-previous-url.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Send the previous page's URL with site insights events so broken links can be traced to the page linking to them. diff --git a/bun.lock b/bun.lock index 8f369bd34..98b2a4052 100644 --- a/bun.lock +++ b/bun.lock @@ -355,7 +355,7 @@ }, "catalog": { "@base-ui/react": "^1.7.0", - "@gitbook/api": "0.202.0", + "@gitbook/api": "0.203.0", "@scalar/api-client-react": "^1.3.46", "@tsconfig/node20": "^20.1.6", "@tsconfig/strictest": "^2.0.6", @@ -727,7 +727,7 @@ "@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@7.2.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "7.2.0" } }, "sha512-6639htZMjEkwskf3J+e6/iar+4cTNM9qhoWuRfj9F3eJD6r7iCzV1SWnQr2Mdv0QT0suuqU8BoJCZUyCtP9R4Q=="], - "@gitbook/api": ["@gitbook/api@0.202.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-qhrjEQbNNmCljR0AgP79+BsVh9yelh9TIBg9X3TyYx5a3lJwDiT3sw1ND9DFQSxGHp8hH5w+20KJHn/IDpTbaw=="], + "@gitbook/api": ["@gitbook/api@0.203.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-cSFUMM7cIMTHW9cdMpexGSkHHaXmc506fx+LQE2+KLqce8GlBDU/ElIP4RU0MGtN+SSYF31AUgvPD8ZerBVcMA=="], "@gitbook/browser-types": ["@gitbook/browser-types@workspace:packages/browser-types"], diff --git a/package.json b/package.json index a99c75d82..194a469ea 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "@tsconfig/strictest": "^2.0.6", "@tsconfig/node20": "^20.1.6", "@base-ui/react": "^1.7.0", - "@gitbook/api": "0.202.0", + "@gitbook/api": "0.203.0", "@scalar/api-client-react": "^1.3.46", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", diff --git a/packages/gitbook/src/components/Insights/InsightsProvider.tsx b/packages/gitbook/src/components/Insights/InsightsProvider.tsx index 40789941d..658c5c83f 100644 --- a/packages/gitbook/src/components/Insights/InsightsProvider.tsx +++ b/packages/gitbook/src/components/Insights/InsightsProvider.tsx @@ -4,6 +4,7 @@ import * as React from 'react'; import { useDebounceCallback, useEventCallback } from 'usehooks-ts'; import type * as api from '@gitbook/api'; +import { SiteInsightsDisplayContext } from '@gitbook/api'; import { OpenAPIOperationContextProvider } from '@gitbook/react-openapi'; import { type CurrentContentContext, useCurrentContent } from '../hooks'; @@ -75,12 +76,15 @@ export function InsightsProvider(props: InsightsProviderProps) { [pathname: string]: | { url: string; + previousUrl: string | null; events: TrackEventInput[]; context: CurrentContentContext; pageContext?: InsightsEventPageContext; } | undefined; }>({}); + // Href of the last page an event was tracked on; `undefined` until the first event. + const lastUrlRef = React.useRef(undefined); /** * Synchronously flush all the pending events. @@ -105,6 +109,7 @@ export function InsightsProvider(props: InsightsProviderProps) { allEvents.push( ...transformEvents({ url: eventsForPathname.url, + previousUrl: eventsForPathname.previousUrl, events: eventsForPathname.events, context: currentContent, pageContext: eventsForPathname.pageContext, @@ -154,6 +159,9 @@ export function InsightsProvider(props: InsightsProviderProps) { ) => { const pathname = window.location.pathname; const previous = eventsRef.current[pathname]; + const lastUrl = + lastUrlRef.current === undefined ? getSameOriginReferrer() : lastUrlRef.current; + lastUrlRef.current = window.location.href; eventsRef.current[pathname] = { // An explicitly-provided context wins so page-scoped events (e.g. feedback) can // attribute to their page even when the pathname's ambient context has none — such @@ -161,6 +169,7 @@ export function InsightsProvider(props: InsightsProviderProps) { // context keep the stored one. pageContext: ctx ?? previous?.pageContext, url: previous?.url ?? window.location.href, + previousUrl: previous ? previous.previousUrl : lastUrl, events: [ ...(previous?.events ?? []), { @@ -214,6 +223,18 @@ export function useTrackEvent(): TrackEventCallback { return React.useContext(InsightsContext); } +/** + * The referrer when it's another page of this origin, so a full page load (e.g. an absolute link + * to a missing page) still records the page it came from. + */ +function getSameOriginReferrer(): string | null { + if (document.referrer === window.location.href || !URL.canParse(document.referrer)) { + return null; + } + const referrer = new URL(document.referrer); + return referrer.origin === window.location.origin ? referrer.href : null; +} + /** * Post the events to the server. */ @@ -240,6 +261,7 @@ function sendEvents(args: { eventUrl: string; events: api.SiteInsightsEvent[] }) */ function transformEvents(input: { url: string; + previousUrl: string | null; events: TrackEventInput[]; context: CurrentContentContext; pageContext: InsightsEventPageContext; @@ -258,6 +280,11 @@ function transformEvents(input: { const location: api.SiteInsightsEventLocation = { url: input.url, + // The embed's navigation is between its own tabs, not pages of the site. + previousUrl: + input.pageContext.displayContext === SiteInsightsDisplayContext.Embed + ? null + : input.previousUrl, siteSection: input.context.siteSectionId ?? null, siteSpace: input.context.siteSpaceId ?? null, space: input.context.spaceId,