Integration with new site insights (#2777)

This commit is contained in:
Samy Pessé
2025-01-28 09:18:50 +01:00
committed by GitHub
parent 9524ff5f5a
commit 95f2aa45eb
29 changed files with 269 additions and 111 deletions
+1 -1
View File
@@ -1 +1 @@
1.2.0
1.2.1
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': minor
---
Track new events for site insights when ads are being clicked
+12 -16
View File
@@ -6,7 +6,7 @@
"devDependencies": {
"@changesets/cli": "^2.27.7",
"prettier": "^3.0.3",
"turbo": "^2.1.2",
"turbo": "^2.3.4",
},
},
"packages/cache-do": {
@@ -32,7 +32,7 @@
"name": "gitbook",
"version": "0.5.0",
"dependencies": {
"@gitbook/api": "^0.88.1",
"@gitbook/api": "^0.89.0",
"@gitbook/cache-do": "workspace:*",
"@gitbook/emoji-codepoints": "workspace:*",
"@gitbook/icons": "workspace:*",
@@ -145,7 +145,7 @@
"name": "@gitbook/react-contentkit",
"version": "0.5.1",
"dependencies": {
"@gitbook/api": "^0.85.0",
"@gitbook/api": "^0.89.0",
"assert-never": "^1.2.1",
"classnames": "^2.5.1",
},
@@ -425,7 +425,7 @@
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.6.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="],
"@gitbook/api": ["@gitbook/api@0.85.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^1.1.1" } }, "sha512-hhWFPxdX1zW8gQd7DYlf8R/LzaFWrC+jB9Lg4M4uz0Fcr5nrdbohufu1yMhGNNDTmo8qzv+fYyeTuoVJSV8G8A=="],
"@gitbook/api": ["@gitbook/api@0.89.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-htzPY5OrFrZz29ShhB535e/j0Z6BBRDmdc97qfprJKzUE4zTRv2L20ZZ0dYeSgNFTkSiVhsuv0sfd6njWfNk7w=="],
"@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"],
@@ -1475,7 +1475,7 @@
"events-intercept": ["events-intercept@2.0.0", "", {}, "sha512-blk1va0zol9QOrdZt0rFXo5KMkNPVSp92Eju/Qz8THwKWKRKeE0T8Br/1aW6+Edkyq9xHYgYxn2QtOnUKPUp+Q=="],
"eventsource-parser": ["eventsource-parser@1.1.2", "", {}, "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA=="],
"eventsource-parser": ["eventsource-parser@3.0.0", "", {}, "sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA=="],
"execa": ["execa@3.2.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "get-stream": "^5.0.0", "human-signals": "^1.1.1", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.0", "onetime": "^5.1.0", "p-finally": "^2.0.0", "signal-exit": "^3.0.2", "strip-final-newline": "^2.0.0" } }, "sha512-kJJfVbI/lZE1PZYDI5VPxp8zXPO9rtxOkhpZ0jMKha56AI9y2gGVC6bkukStQf0ka5Rh15BA5m7cCCH4jmHqkw=="],
@@ -2619,19 +2619,19 @@
"tslib": ["tslib@2.7.0", "", {}, "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA=="],
"turbo": ["turbo@2.1.3", "", { "optionalDependencies": { "turbo-darwin-64": "2.1.3", "turbo-darwin-arm64": "2.1.3", "turbo-linux-64": "2.1.3", "turbo-linux-arm64": "2.1.3", "turbo-windows-64": "2.1.3", "turbo-windows-arm64": "2.1.3" }, "bin": { "turbo": "bin/turbo" } }, "sha512-lY0yj2GH2a2a3NExZ3rGe+rHUVeFE2aXuRAue57n+08E7Z7N7YCmynju0kPC1grAQzERmoLpKrmzmWd+PNiADw=="],
"turbo": ["turbo@2.3.4", "", { "optionalDependencies": { "turbo-darwin-64": "2.3.4", "turbo-darwin-arm64": "2.3.4", "turbo-linux-64": "2.3.4", "turbo-linux-arm64": "2.3.4", "turbo-windows-64": "2.3.4", "turbo-windows-arm64": "2.3.4" }, "bin": { "turbo": "bin/turbo" } }, "sha512-1kiLO5C0Okh5ay1DbHsxkPsw9Sjsbjzm6cF85CpWjR0BIyBFNDbKqtUyqGADRS1dbbZoQanJZVj4MS5kk8J42Q=="],
"turbo-darwin-64": ["turbo-darwin-64@2.1.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-ouJOm0g0YyoBuhmikEujVCBGo3Zr0lbSOWFIsQtWUTItC88F2w2byhjtsYGPXQwMlTbXwmoBU2lOCfWNkeEwHQ=="],
"turbo-darwin-64": ["turbo-darwin-64@2.3.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-uOi/cUIGQI7uakZygH+cZQ5D4w+aMLlVCN2KTGot+cmefatps2ZmRRufuHrEM0Rl63opdKD8/JIu+54s25qkfg=="],
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.1.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-j2FOJsK4LAOtHQlb3Oom0yWB/Vi0nF1ljInr311mVzHoFAJRZtfW2fRvdZRb/lBUwjSp8be58qWHzANIcrA0OA=="],
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.3.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-IIM1Lq5R+EGMtM1YFGl4x8Xkr0MWb4HvyU8N4LNoQ1Be5aycrOE+VVfH+cDg/Q4csn+8bxCOxhRp5KmUflrVTQ=="],
"turbo-linux-64": ["turbo-linux-64@2.1.3", "", { "os": "linux", "cpu": "x64" }, "sha512-ubRHkI1gSel7H7wsmxKK8C9UlLWqg/2dkCC88LFupaK6TKgvBKqDqA0Z1M9C/escK0Jsle2k0H8bybV9OYIl4Q=="],
"turbo-linux-64": ["turbo-linux-64@2.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-1aD2EfR7NfjFXNH3mYU5gybLJEFi2IGOoKwoPLchAFRQ6OEJQj201/oNo9CDL75IIrQo64/NpEgVyZtoPlfhfA=="],
"turbo-linux-arm64": ["turbo-linux-arm64@2.1.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-LffUL+e5wv7BtD6DgnM2kKOlDkMo2eRjhbAjVnrCD3wi2ug0tl6NDzajnHHjtaMyOnIf4AvzSKdLWsBxafGBQA=="],
"turbo-linux-arm64": ["turbo-linux-arm64@2.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-MxTpdKwxCaA5IlybPxgGLu54fT2svdqTIxRd0TQmpRJIjM0s4kbM+7YiLk0mOh6dGqlWPUsxz/A0Mkn8Xr5o7Q=="],
"turbo-windows-64": ["turbo-windows-64@2.1.3", "", { "os": "win32", "cpu": "x64" }, "sha512-S9SvcZZoaq5jKr6kA6eF7/xgQhVn8Vh7PVy5lono9zybvhyL4eY++y2PaLToIgL8G9IcbLmgOC73ExNjFBg9XQ=="],
"turbo-windows-64": ["turbo-windows-64@2.3.4", "", { "os": "win32", "cpu": "x64" }, "sha512-yyCrWqcRGu1AOOlrYzRnizEtdkqi+qKP0MW9dbk9OsMDXaOI5jlWtTY/AtWMkLw/czVJ7yS9Ex1vi9DB6YsFvw=="],
"turbo-windows-arm64": ["turbo-windows-arm64@2.1.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-twlEo8lRrGbrR6T/ZklUIquW3IlFCEtywklgVA81aIrSBm56+GEVpSrHhIlsx1hiYeSNrs+GpDwZGe+V7fvEVQ=="],
"turbo-windows-arm64": ["turbo-windows-arm64@2.3.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-PggC3qH+njPfn1PDVwKrQvvQby8X09ufbqZ2Ha4uSu+5TvPorHHkAbZVHKYj2Y+tvVzxRzi4Sv6NdHXBS9Be5w=="],
"type": ["type@2.7.3", "", {}, "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="],
@@ -3163,8 +3163,6 @@
"fs-minipass/minipass": ["minipass@2.9.0", "", { "dependencies": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" } }, "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg=="],
"gitbook/@gitbook/api": ["@gitbook/api@0.88.1", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-u+xzuWKbuTe25VTEiFEcZ49Jst8QCVrc6R7MycU4HnS3KKazuCk8gA30JU/JKINkUElMmPLsTOQPY3ZAYvPkVQ=="],
"glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"google-auth-library/jws": ["jws@4.0.0", "", { "dependencies": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" } }, "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg=="],
@@ -3509,8 +3507,6 @@
"eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"gitbook/@gitbook/api/eventsource-parser": ["eventsource-parser@3.0.0", "", {}, "sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA=="],
"glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
"google-auth-library/jws/jwa": ["jwa@2.0.0", "", { "dependencies": { "buffer-equal-constant-time": "1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA=="],
+1 -1
View File
@@ -4,7 +4,7 @@
"devDependencies": {
"@changesets/cli": "^2.27.7",
"prettier": "^3.0.3",
"turbo": "^2.1.2"
"turbo": "^2.3.4"
},
"packageManager": "bun@1.1.18",
"overrides": {
+1 -1
View File
@@ -16,7 +16,7 @@
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static"
},
"dependencies": {
"@gitbook/api": "^0.88.1",
"@gitbook/api": "^0.89.0",
"@gitbook/cache-do": "workspace:*",
"@gitbook/emoji-codepoints": "workspace:*",
"@gitbook/icons": "workspace:*",
@@ -4,6 +4,7 @@ import {
RevisionPageDocument,
RevisionPageGroup,
SiteCustomizationSettings,
SiteInsightsTrademarkPlacement,
Space,
} from '@gitbook/api';
import { Icon } from '@gitbook/icons';
@@ -146,7 +147,11 @@ export default async function PDFHTMLOutput(props: {
limit={pdfParams.limit ?? DEFAULT_LIMIT}
trademark={
customization.trademark.enabled ? (
<TrademarkLink space={space} customization={customization} />
<TrademarkLink
space={space}
customization={customization}
placement={SiteInsightsTrademarkPlacement.Footer}
/>
) : null
}
/>
+36 -6
View File
@@ -1,12 +1,20 @@
'use client';
import { SiteAds, SiteAdsStatus } from '@gitbook/api';
import {
SiteAds,
SiteAdsStatus,
SiteInsightsAdPlacement,
SiteInsightsAd,
SiteInsightsTrademarkPlacement,
} from '@gitbook/api';
import * as React from 'react';
import { t, useLanguage } from '@/intl/client';
import { ClassValue, tcls } from '@/lib/tailwind';
import { renderAd } from './renderAd';
import { useTrackEvent } from '../Insights';
import { Link } from '../primitives';
/**
* Zone ID provided by BuySellAds for the preview.
@@ -28,7 +36,7 @@ export function Ad({
}: {
zoneId: string | null;
spaceId: string;
placement: string;
placement: SiteInsightsAdPlacement;
ignore: boolean;
style?: ClassValue;
siteAdsStatus?: SiteAds['status'];
@@ -36,7 +44,20 @@ export function Ad({
}) {
const containerRef = React.useRef<HTMLDivElement>(null);
const [visible, setVisible] = React.useState(false);
const [ad, setAd] = React.useState<React.ReactNode | undefined>(undefined);
const [ad, setAd] = React.useState<
{ children: React.ReactNode; insightsAd: SiteInsightsAd | null } | undefined
>(undefined);
const trackEvent = useTrackEvent();
// Track display of the ad
React.useEffect(() => {
if (ad?.insightsAd) {
trackEvent({
type: 'ad_display',
ad: ad.insightsAd,
});
}
}, [ad]);
// Observe the container visibility
React.useEffect(() => {
@@ -121,7 +142,7 @@ export function Ad({
<div ref={containerRef} className={tcls(style)} data-visual-test="removed">
{ad ? (
<>
{ad}
{ad.children}
<AdSponsoredLink spaceId={spaceId} />
</>
) : null}
@@ -132,6 +153,7 @@ export function Ad({
function AdSponsoredLink(props: { spaceId: string }) {
const { spaceId } = props;
const language = useLanguage();
const trackEvent = useTrackEvent();
const viaUrl = new URL('https://www.gitbook.com');
viaUrl.searchParams.set('utm_source', 'content');
@@ -149,9 +171,17 @@ function AdSponsoredLink(props: { spaceId: string }) {
'dark:text-light/5',
)}
>
<a target="_blank" href={viaUrl.toString()} className={tcls('hover:underline')}>
<Link
target="_blank"
href={viaUrl.toString()}
className={tcls('hover:underline')}
insights={{
type: 'trademark_click',
placement: SiteInsightsTrademarkPlacement.Ad,
}}
>
{t(language, 'sponsored_via_gitbook')}
</a>
</Link>
</p>
);
}
@@ -1,20 +1,38 @@
import { SiteInsightsAd } from '@gitbook/api';
import * as React from 'react';
import { getResizedImageURL } from '@/lib/images';
import { tcls } from '@/lib/tailwind';
import { AdItem } from './types';
import { Link } from '../primitives';
/**
* Classic rendering for an ad.
*/
export async function AdClassicRendering({ ad }: { ad: AdItem }) {
export async function AdClassicRendering({
ad,
insightsAd,
}: {
ad: AdItem;
insightsAd: SiteInsightsAd | null;
}) {
const smallImgSrc =
'smallImage' in ad ? await getResizedImageURL(ad.smallImage, { width: 192, dpr: 2 }) : null;
const logoSrc =
'logo' in ad ? await getResizedImageURL(ad.logo, { width: 192 - 48, dpr: 2 }) : null;
return (
<a
<Link
rel="sponsored noopener"
target="_blank"
insights={
insightsAd
? {
type: 'ad_click',
ad: insightsAd,
}
: undefined
}
className={tcls(
'flex',
'flex-col',
@@ -29,8 +47,6 @@ export async function AdClassicRendering({ ad }: { ad: AdItem }) {
'p-4',
)}
href={ad.statlink}
rel="sponsored noopener"
target="_blank"
>
{smallImgSrc && 'smallImage' in ad ? (
<div>
@@ -47,6 +63,6 @@ export async function AdClassicRendering({ ad }: { ad: AdItem }) {
<div className={tcls('flex', 'flex-col')}>
<div className={tcls('text-xs')}>{ad.description}</div>
</div>
</a>
</Link>
);
}
@@ -1,3 +1,4 @@
import { SiteInsightsAd } from '@gitbook/api';
import * as React from 'react';
import { hexToRgba } from '@/lib/colors';
@@ -5,15 +6,32 @@ import { getResizedImageURL } from '@/lib/images';
import { tcls } from '@/lib/tailwind';
import { AdCover } from './types';
import { Link } from '../primitives';
/**
* Cover rendering for an ad.
*/
export async function AdCoverRendering({ ad }: { ad: AdCover }) {
export async function AdCoverRendering({
ad,
insightsAd,
}: {
ad: AdCover;
insightsAd: SiteInsightsAd | null;
}) {
const largeImage = await getResizedImageURL(ad.largeImage, { width: 128, dpr: 2 });
return (
<a
<Link
rel="sponsored noopener"
target="_blank"
insights={
insightsAd
? {
type: 'ad_click',
ad: insightsAd,
}
: undefined
}
className={tcls(
'group/ad',
'relative',
@@ -33,8 +51,6 @@ export async function AdCoverRendering({ ad }: { ad: AdCover }) {
)}
style={{ backgroundColor: ad.backgroundColor, color: ad.textColor ?? '#ffffff' }}
href={ad.statlink}
rel="sponsored noopener"
target="_blank"
>
<div
className={tcls(
@@ -106,6 +122,6 @@ export async function AdCoverRendering({ ad }: { ad: AdCover }) {
backgroundColor: hexToRgba(ad.backgroundColor, 0.8),
}}
/>
</a>
</Link>
);
}
@@ -1,5 +1,6 @@
'use server';
import { SiteInsightsAd, SiteInsightsAdPlacement } from '@gitbook/api';
import { headers } from 'next/headers';
import { AdClassicRendering } from './AdClassicRendering';
@@ -20,7 +21,7 @@ interface FetchLiveAdOptions {
/** Mode to render the Ad */
mode: 'classic' | 'auto' | 'cover';
/** Name of the placement for the ad */
placement: string;
placement: SiteInsightsAdPlacement;
/** If true, we'll not track it as an impression */
ignore: boolean;
}
@@ -47,16 +48,28 @@ export async function renderAd(options: FetchAdOptions) {
const { ad } = result;
return (
<>
{mode === 'classic' || !('callToAction' in ad) ? (
<AdClassicRendering ad={ad} />
) : (
<AdCoverRendering ad={ad} />
)}
{ad.pixel ? <AdPixels rawPixel={ad.pixel} /> : null}
</>
);
const insightsAd: SiteInsightsAd | null =
options.source === 'live'
? {
placement: options.placement,
zoneId: options.zoneId,
domain: 'company' in ad ? ad.company : '',
}
: null;
return {
children: (
<>
{mode === 'classic' || !('callToAction' in ad) ? (
<AdClassicRendering ad={ad} insightsAd={insightsAd} />
) : (
<AdCoverRendering ad={ad} insightsAd={insightsAd} />
)}
{ad.pixel ? <AdPixels rawPixel={ad.pixel} /> : null}
</>
),
insightsAd,
};
}
async function fetchAd({
@@ -35,8 +35,11 @@ export async function BlockContentRef(props: BlockProps<DocumentBlockContentRef>
title={resolved.text}
style={style}
insights={{
target: block.data.ref,
position: SiteInsightsLinkPosition.Content,
type: 'link_click',
link: {
target: block.data.ref,
position: SiteInsightsLinkPosition.Content,
},
}}
/>
);
@@ -26,8 +26,11 @@ export async function File(props: BlockProps<DocumentBlockFile>) {
href={file.downloadURL}
download={file.name}
insights={{
target: block.data.ref,
position: SiteInsightsLinkPosition.Content,
type: 'link_click',
link: {
target: block.data.ref,
position: SiteInsightsLinkPosition.Content,
},
}}
className={tcls(
'group/file',
@@ -27,8 +27,11 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
href={resolved.href}
className="underline underline-offset-2 text-primary hover:text-primary-700 transition-colors"
insights={{
target: inline.data.ref,
position: SiteInsightsLinkPosition.Content,
type: 'link_click',
link: {
target: inline.data.ref,
position: SiteInsightsLinkPosition.Content,
},
}}
>
<Inlines
@@ -19,8 +19,11 @@ export async function Mention(props: InlineProps<DocumentInlineMention>) {
<StyledLink
href={resolved.href}
insights={{
target: inline.data.ref,
position: SiteInsightsLinkPosition.Content,
type: 'link_click',
link: {
target: inline.data.ref,
position: SiteInsightsLinkPosition.Content,
},
}}
>
{resolved.text}
@@ -166,8 +166,11 @@ export async function RecordCard(
'dark:hover:before:ring-light/4',
])}
insights={{
target: targetRef,
position: SiteInsightsLinkPosition.Content,
type: 'link_click',
link: {
target: targetRef,
position: SiteInsightsLinkPosition.Content,
},
}}
>
{body}
@@ -168,11 +168,14 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
insights={
ref.file
? {
target: {
kind: 'file',
file: ref.file.id,
type: 'link_click',
link: {
target: {
kind: 'file',
file: ref.file.id,
},
position: SiteInsightsLinkPosition.Content,
},
position: SiteInsightsLinkPosition.Content,
}
: undefined
}
@@ -225,8 +228,11 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
insights={
contentRef
? {
target: contentRef,
position: SiteInsightsLinkPosition.Content,
type: 'link_click',
link: {
target: contentRef,
position: SiteInsightsLinkPosition.Content,
},
}
: undefined
}
@@ -260,8 +266,11 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
key={index}
href={resolved.href}
insights={{
target: contentRef,
position: SiteInsightsLinkPosition.Content,
type: 'link_click',
link: {
target: contentRef,
position: SiteInsightsLinkPosition.Content,
},
}}
>
{resolved.text}
@@ -54,8 +54,11 @@ async function FooterLink(props: { link: CustomizationContentLink; context: Cont
'underline-offset-2',
)}
insights={{
target: link.to,
position: SiteInsightsLinkPosition.Footer,
type: 'link_click',
link: {
target: link.to,
position: SiteInsightsLinkPosition.Footer,
},
}}
>
{link.title}
@@ -145,8 +145,11 @@ function HeaderItemButton(
}[linkStyle],
)}
insights={{
target: linkTarget,
position: SiteInsightsLinkPosition.Header,
type: 'link_click',
link: {
target: linkTarget,
position: SiteInsightsLinkPosition.Header,
},
}}
{...rest}
>
@@ -175,8 +178,11 @@ function HeaderItemLink(props: Omit<HeaderLinkNavItemProps, 'linkStyle'>) {
href={href}
className={getHeaderLinkClassName({ headerPreset })}
insights={{
target: linkTarget,
position: SiteInsightsLinkPosition.Header,
type: 'link_click',
link: {
target: linkTarget,
position: SiteInsightsLinkPosition.Header,
},
}}
{...rest}
>
@@ -219,7 +225,13 @@ async function SubHeaderLink(props: {
return (
<DropdownMenuItem
href={target.href}
insights={{ target: link.to, position: SiteInsightsLinkPosition.Header }}
insights={{
type: 'link_click',
link: {
target: link.to,
position: SiteInsightsLinkPosition.Header,
},
}}
>
{link.title}
</DropdownMenuItem>
@@ -82,8 +82,11 @@ async function MoreMenuLink(props: {
insights={
link.to
? {
target: link.to,
position: SiteInsightsLinkPosition.Header,
type: 'link_click',
link: {
target: link.to,
position: SiteInsightsLinkPosition.Header,
},
}
: undefined
}
@@ -10,7 +10,7 @@ import * as cookies from '@/lib/cookies';
import { getSession } from './sessions';
import { getVisitorId } from './visitorId';
type SiteEventName = api.SiteInsightsEvent['type'];
export type InsightsEventName = api.SiteInsightsEvent['type'];
/**
* Global context for all events in the session.
@@ -46,15 +46,14 @@ interface InsightsEventOptions {
/**
* Input data for an event.
*/
type TrackEventInput<EventName extends SiteEventName> = { type: EventName } & Omit<
Extract<api.SiteInsightsEvent, { type: EventName }>,
'location' | 'session'
>;
export type TrackEventInput<EventName extends InsightsEventName = InsightsEventName> = {
type: EventName;
} & Omit<Extract<api.SiteInsightsEvent, { type: EventName }>, 'location' | 'session'>;
/**
* Callback to track an event.
*/
type TrackEventCallback = <EventName extends SiteEventName>(
type TrackEventCallback = <EventName extends InsightsEventName>(
event: TrackEventInput<EventName>,
ctx?: InsightsEventPageContext,
options?: InsightsEventOptions,
@@ -80,7 +79,7 @@ export function InsightsProvider(props: InsightsProviderProps) {
[pathname: string]:
| {
url: string;
events: TrackEventInput<SiteEventName>[];
events: TrackEventInput<InsightsEventName>[];
context: InsightsEventContext;
pageContext?: InsightsEventPageContext;
}
@@ -152,7 +151,7 @@ export function InsightsProvider(props: InsightsProviderProps) {
const trackEvent: TrackEventCallback = useEventCallback(
(
event: TrackEventInput<SiteEventName>,
event: TrackEventInput<InsightsEventName>,
ctx?: InsightsEventPageContext,
options?: InsightsEventOptions,
) => {
@@ -253,7 +252,7 @@ function sendEvents(args: {
*/
function transformEvents(input: {
url: string;
events: TrackEventInput<SiteEventName>[];
events: TrackEventInput<InsightsEventName>[];
context: InsightsEventContext;
pageContext: InsightsEventPageContext;
visitorId: string;
@@ -5,6 +5,7 @@ import {
Site,
SiteAdsStatus,
SiteCustomizationSettings,
SiteInsightsAdPlacement,
Space,
} from '@gitbook/api';
import { Icon } from '@gitbook/icons';
@@ -226,7 +227,7 @@ export async function PageAside(props: {
zoneId={
site?.ads && site.ads.status === SiteAdsStatus.Live ? site.ads.zoneId : null
}
placement="page.aside"
placement={SiteInsightsAdPlacement.Aside}
spaceId={space.id}
siteAdsStatus={site?.ads && site.ads.status ? site.ads.status : undefined}
ignore={process.env.NODE_ENV !== 'production'}
@@ -52,8 +52,11 @@ export async function PageBodyBlankslate(props: {
title={child.title}
href={resolved.href}
insights={{
target: child.target,
position: SiteInsightsLinkPosition.Content,
type: 'link_click',
link: {
target: child.target,
position: SiteInsightsLinkPosition.Content,
},
}}
/>
);
@@ -51,11 +51,14 @@ export async function PageFooterNavigation(props: {
title={previous.title}
href={previousHref}
insights={{
target: {
kind: 'page',
page: previous.id,
type: 'link_click',
link: {
target: {
kind: 'page',
page: previous.id,
},
position: SiteInsightsLinkPosition.Content,
},
position: SiteInsightsLinkPosition.Content,
}}
reversed
/>
@@ -67,11 +70,14 @@ export async function PageFooterNavigation(props: {
title={next.title}
href={nextHref}
insights={{
target: {
kind: 'page',
page: next.id,
type: 'link_click',
link: {
target: {
kind: 'page',
page: next.id,
},
position: SiteInsightsLinkPosition.Content,
},
position: SiteInsightsLinkPosition.Content,
}}
/>
) : null}
@@ -29,11 +29,14 @@ export async function PageDocumentItem(props: {
href={href}
pathname={getPagePath(rootPages, page)}
insights={{
target: {
kind: 'page',
page: page.id,
type: 'link_click',
link: {
target: {
kind: 'page',
page: page.id,
},
position: SiteInsightsLinkPosition.Sidebar,
},
position: SiteInsightsLinkPosition.Sidebar,
}}
descendants={
page.pages && page.pages.length ? (
@@ -36,8 +36,11 @@ export async function PageLinkItem(props: { page: RevisionPageLink; context: Con
'dark:hover:bg-light/2',
)}
insights={{
target: page.target,
position: SiteInsightsLinkPosition.Sidebar,
type: 'link_click',
link: {
target: page.target,
position: SiteInsightsLinkPosition.Sidebar,
},
}}
>
<TOCPageIcon page={page} />
@@ -4,6 +4,7 @@ import {
RevisionPageDocument,
RevisionPageGroup,
SiteCustomizationSettings,
SiteInsightsTrademarkPlacement,
Space,
} from '@gitbook/api';
import React from 'react';
@@ -135,7 +136,11 @@ export function TableOfContents(props: {
)}
/>
{customization.trademark.enabled ? (
<Trademark space={space} customization={customization} />
<Trademark
space={space}
customization={customization}
placement={SiteInsightsTrademarkPlacement.Sidebar}
/>
) : null}
</TOCScrollContainer>
</div>
@@ -1,15 +1,23 @@
import { CustomizationSettings, SiteCustomizationSettings, Space } from '@gitbook/api';
import {
CustomizationSettings,
SiteCustomizationSettings,
Space,
SiteInsightsTrademarkPlacement,
} from '@gitbook/api';
import { Icon } from '@gitbook/icons';
import { t, getSpaceLanguage } from '@/intl/server';
import { tcls } from '@/lib/tailwind';
import { Link } from '../primitives';
/**
* Trademark link to the GitBook.
*/
export function Trademark(props: {
space: Space;
customization: CustomizationSettings | SiteCustomizationSettings;
placement: SiteInsightsTrademarkPlacement;
}) {
return (
<div
@@ -63,8 +71,9 @@ export function Trademark(props: {
export function TrademarkLink(props: {
space: Space;
customization: CustomizationSettings | SiteCustomizationSettings;
placement: SiteInsightsTrademarkPlacement;
}) {
const { space, customization } = props;
const { space, customization, placement } = props;
const language = getSpaceLanguage(customization);
const url = new URL('https://www.gitbook.com');
@@ -73,7 +82,7 @@ export function TrademarkLink(props: {
url.searchParams.set('utm_campaign', space.id);
return (
<a
<Link
target="_blank"
href={url.toString()}
className={tcls(
@@ -104,9 +113,13 @@ export function TrademarkLink(props: {
'transition-colors',
'pointer-events-auto',
)}
insights={{
type: 'trademark_click',
placement,
}}
>
<Icon icon="gitbook" className={tcls('size-5', 'mr-3')} />
{t(language, 'powered_by_gitbook')}
</a>
</Link>
);
}
@@ -1,10 +1,9 @@
'use client';
import * as api from '@gitbook/api';
import NextLink, { LinkProps as NextLinkProps } from 'next/link';
import React from 'react';
import { useTrackEvent } from '../Insights';
import { useTrackEvent, TrackEventInput } from '../Insights';
// Props from Next, which includes NextLinkProps and all the things anchor elements support.
type BaseLinkProps = Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof NextLinkProps> &
@@ -13,8 +12,11 @@ type BaseLinkProps = Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof N
} & React.RefAttributes<HTMLAnchorElement>;
export type LinkInsightsProps = {
/** Target of the link, for insights. */
insights?: api.SiteInsightsEventLinkClick['link'];
/** Event to track when the link is clicked. */
insights?:
| TrackEventInput<'ad_click'>
| TrackEventInput<'link_click'>
| TrackEventInput<'trademark_click'>;
};
export type LinkProps = Omit<BaseLinkProps, 'href'> &
@@ -40,7 +42,7 @@ export const Link = React.forwardRef(function Link(
const onClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
if (insights) {
trackEvent({ type: 'link_click', link: insights }, undefined, {
trackEvent(insights, undefined, {
immediate: isExternal,
});
}
+1 -1
View File
@@ -10,7 +10,7 @@
},
"dependencies": {
"classnames": "^2.5.1",
"@gitbook/api": "^0.85.0",
"@gitbook/api": "^0.89.0",
"assert-never": "^1.2.1"
},
"peerDependencies": {