Skip to content

Commit

Permalink
Track click on links for site insights (#2659)
Browse files Browse the repository at this point in the history
  • Loading branch information
SamyPesse authored Dec 21, 2024
1 parent e4e2f52 commit 1417279
Show file tree
Hide file tree
Showing 20 changed files with 322 additions and 111 deletions.
5 changes: 5 additions & 0 deletions .changeset/nine-gorillas-turn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'gitbook': minor
---

Track clicks on links (header, footer, content) for site insights.
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export async function BlockContentRef(props: BlockProps<DocumentBlockContentRef>
href={resolved.href}
title={resolved.text}
style={style}
insights={{
target: block.data.ref,
position: 'content',
}}
/>
);
}
Expand Down
9 changes: 7 additions & 2 deletions packages/gitbook/src/components/DocumentView/File.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { tcls } from '@/lib/tailwind';
import { BlockProps } from './Block';
import { Caption } from './Caption';
import { FileIcon } from './FileIcon';
import { Link } from '../primitives';

export async function File(props: BlockProps<DocumentBlockFile>) {
const { block, context } = props;
Expand All @@ -21,9 +22,13 @@ export async function File(props: BlockProps<DocumentBlockFile>) {

return (
<Caption {...props} wrapperStyle={[]}>
<a
<Link
href={file.downloadURL}
download={file.name}
insights={{
target: block.data.ref,
position: 'content',
}}
className={tcls(
'group/file',
'flex',
Expand Down Expand Up @@ -77,7 +82,7 @@ export async function File(props: BlockProps<DocumentBlockFile>) {
{contentType}
</div>
</div>
</a>
</Link>
</Caption>
);
}
Expand Down
6 changes: 5 additions & 1 deletion packages/gitbook/src/components/DocumentView/InlineLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
return (
<Link
href={resolved.href}
className="underline underline-offset-2 text-primary hover:text-primary-700 transition-colors "
className="underline underline-offset-2 text-primary hover:text-primary-700 transition-colors"
insights={{
target: inline.data.ref,
position: 'content',
}}
>
<Inlines
context={context}
Expand Down
12 changes: 11 additions & 1 deletion packages/gitbook/src/components/DocumentView/Mention.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,15 @@ export async function Mention(props: InlineProps<DocumentInlineMention>) {
return null;
}

return <StyledLink href={resolved.href}>{resolved.text}</StyledLink>;
return (
<StyledLink
href={resolved.href}
insights={{
target: inline.data.ref,
position: 'content',
}}
>
{resolved.text}
</StyledLink>
);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ContentRef, DocumentTableViewCards } from '@gitbook/api';
import React from 'react';

import { Link } from '@/components/primitives';
import { Image } from '@/components/utils';
import { ClassValue, tcls } from '@/lib/tailwind';

Expand Down Expand Up @@ -153,17 +154,21 @@ export async function RecordCard(
'before:dark:ring-light/2',
] as ClassValue;

if (target) {
if (target && targetRef) {
return (
<a
<Link
href={target.href}
className={tcls(style, [
'hover:before:ring-dark/4',
'dark:hover:before:ring-light/4',
])}
insights={{
target: targetRef,
position: 'content',
}}
>
{body}
</a>
</Link>
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ContentRef, DocumentBlockTable } from '@gitbook/api';
import { ContentRef, ContentRefUser, DocumentBlockTable } from '@gitbook/api';
import { Icon } from '@gitbook/icons';
import assertNever from 'assert-never';

Expand Down Expand Up @@ -148,6 +148,17 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
href={ref.href}
target="_blank"
style={['flex', 'flex-row', 'items-center', 'gap-2']}
insights={
ref.file
? {
target: {
kind: 'file',
file: ref.file.id,
},
position: 'content',
}
: undefined
}
>
{contentType === 'image' ? (
<Image
Expand Down Expand Up @@ -178,8 +189,9 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
</Tag>
);
case 'content-ref': {
const resolved = value
? await context.resolveContentRef(value as ContentRef, {
const contentRef = value ? (value as ContentRef) : null;
const resolved = contentRef
? await context.resolveContentRef(contentRef, {
resolveAnchorText: true,
iconStyle: ['mr-2', 'text-dark/6', 'dark:text-light/6'],
})
Expand All @@ -191,26 +203,51 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
>
{resolved?.icon ?? null}
{resolved ? (
<StyledLink href={resolved.href}>{resolved.text}</StyledLink>
<StyledLink
href={resolved.href}
insights={
contentRef
? {
target: contentRef,
position: 'content',
}
: undefined
}
>
{resolved.text}
</StyledLink>
) : null}
</Tag>
);
}
case 'users': {
const resolved = await Promise.all(
(value as string[]).map((userId) =>
context.resolveContentRef({
(value as string[]).map(async (userId) => {
const contentRef: ContentRefUser = {
kind: 'user',
user: userId,
}),
),
};
const resolved = await context.resolveContentRef(contentRef);
if (!resolved) {
return null;
}

return [contentRef, resolved] as const;
}),
);

return (
<Tag className={tcls('text-base')} aria-labelledby={ariaLabelledBy}>
{resolved.filter(filterOutNullable).map((file, index) => (
<StyledLink key={index} href={file.href}>
{file.text}
{resolved.filter(filterOutNullable).map(([contentRef, resolved], index) => (
<StyledLink
key={index}
href={resolved.href}
insights={{
target: contentRef,
position: 'content',
}}
>
{resolved.text}
</StyledLink>
))}
</Tag>
Expand Down
4 changes: 4 additions & 0 deletions packages/gitbook/src/components/Footer/FooterLinksGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ async function FooterLink(props: { link: CustomizationContentLink; context: Cont
'dark:text-light/8',
'dark:hover:text-light/9',
)}
insights={{
target: link.to,
position: 'footer',
}}
>
{link.title}
</Link>
Expand Down
19 changes: 11 additions & 8 deletions packages/gitbook/src/components/Header/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { DetailedHTMLProps, HTMLAttributes, useId } from 'react';

import { ClassValue, tcls } from '@/lib/tailwind';

import { Link } from '../primitives';
import { Link, LinkInsightsProps } from '../primitives';

export type DropdownButtonProps<E extends HTMLElement = HTMLElement> = Omit<
Partial<DetailedHTMLProps<HTMLAttributes<E>, E>>,
Expand Down Expand Up @@ -110,19 +110,22 @@ export function DropdownMenu(props: { children: React.ReactNode }) {
/**
* Menu item in a dropdown.
*/
export function DropdownMenuItem(props: {
href: string | null;
active?: boolean;
className?: ClassValue;
children: React.ReactNode;
}) {
const { children, active = false, href, className } = props;
export function DropdownMenuItem(
props: {
href: string | null;
active?: boolean;
className?: ClassValue;
children: React.ReactNode;
} & LinkInsightsProps,
) {
const { children, active = false, href, className, insights } = props;

if (href) {
return (
<Link
href={href}
prefetch={false}
insights={insights}
className={tcls(
'px-3 py-1 text-sm rounded straight-corners:rounded-sm',
active ? 'bg-primary/3 dark:bg-light/2 text-primary-600' : null,
Expand Down
43 changes: 32 additions & 11 deletions packages/gitbook/src/components/Header/HeaderLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
CustomizationHeaderPreset,
SiteCustomizationSettings,
CustomizationHeaderItem,
ContentRef,
} from '@gitbook/api';
import assertNever from 'assert-never';

Expand Down Expand Up @@ -35,7 +36,7 @@ export async function HeaderLink(props: {
<Dropdown
className="shrink"
button={(buttonProps) => {
if (!target) {
if (!target || !link.to) {
return (
<HeaderItemDropdown
{...buttonProps}
Expand All @@ -47,6 +48,7 @@ export async function HeaderLink(props: {
return (
<HeaderLinkNavItem
{...buttonProps}
linkTarget={link.to}
linkStyle={linkStyle}
headerPreset={headerPreset}
title={link.title}
Expand All @@ -65,12 +67,13 @@ export async function HeaderLink(props: {
);
}

if (!target) {
if (!target || !link.to) {
return null;
}

return (
<HeaderLinkNavItem
linkTarget={link.to}
linkStyle={linkStyle}
headerPreset={headerPreset}
title={link.title}
Expand All @@ -81,6 +84,7 @@ export async function HeaderLink(props: {
}

export type HeaderLinkNavItemProps = {
linkTarget: ContentRef;
linkStyle: NonNullable<CustomizationHeaderItem['style']>;
headerPreset: CustomizationHeaderPreset;
title: string;
Expand All @@ -89,14 +93,15 @@ export type HeaderLinkNavItemProps = {
} & DropdownButtonProps<HTMLElement>;

function HeaderLinkNavItem(props: HeaderLinkNavItemProps) {
switch (props.linkStyle) {
const { linkStyle, ...rest } = props;
switch (linkStyle) {
case 'button-secondary':
case 'button-primary':
return <HeaderItemButton {...props} linkStyle={props.linkStyle} />;
return <HeaderItemButton {...rest} linkStyle={linkStyle} />;
case 'link':
return <HeaderItemLink {...props} />;
return <HeaderItemLink {...rest} />;
default:
assertNever(props.linkStyle);
assertNever(linkStyle);
}
}

Expand All @@ -105,7 +110,7 @@ function HeaderItemButton(
linkStyle: 'button-secondary' | 'button-primary';
},
) {
const { linkStyle, headerPreset, title, href, isDropdown, ...rest } = props;
const { linkTarget, linkStyle, headerPreset, title, href, isDropdown, ...rest } = props;
const variant = (() => {
switch (linkStyle) {
case 'button-secondary':
Expand Down Expand Up @@ -139,6 +144,10 @@ function HeaderItemButton(
),
}[linkStyle],
)}
insights={{
target: linkTarget,
position: 'header',
}}
{...rest}
>
{title}
Expand All @@ -158,10 +167,18 @@ function getHeaderLinkClassName(props: { headerPreset: CustomizationHeaderPreset
);
}

function HeaderItemLink(props: HeaderLinkNavItemProps) {
const { headerPreset, title, isDropdown, href, ...rest } = props;
function HeaderItemLink(props: Omit<HeaderLinkNavItemProps, 'linkStyle'>) {
const { linkTarget, headerPreset, title, isDropdown, href, ...rest } = props;
return (
<Link href={href} className={getHeaderLinkClassName({ headerPreset })} {...rest}>
<Link
href={href}
className={getHeaderLinkClassName({ headerPreset })}
insights={{
target: linkTarget,
position: 'header',
}}
{...rest}
>
<span className="truncate min-w-0">{title}</span>
{isDropdown ? <DropdownChevron /> : null}
</Link>
Expand Down Expand Up @@ -198,5 +215,9 @@ async function SubHeaderLink(props: {
return null;
}

return <DropdownMenuItem href={target.href}>{link.title}</DropdownMenuItem>;
return (
<DropdownMenuItem href={target.href} insights={{ target: link.to, position: 'header' }}>
{link.title}
</DropdownMenuItem>
);
}
14 changes: 13 additions & 1 deletion packages/gitbook/src/components/Header/HeaderLinkMore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,19 @@ async function MoreMenuLink(props: {
{'links' in link && link.links.length > 0 && (
<hr className="first:hidden border-t border-light-3 dark:border-dark-3 my-1 -mx-2" />
)}
<DropdownMenuItem href={target?.href ?? null}>{link.title}</DropdownMenuItem>
<DropdownMenuItem
href={target?.href ?? null}
insights={
link.to
? {
target: link.to,
position: 'header',
}
: undefined
}
>
{link.title}
</DropdownMenuItem>
{'links' in link
? link.links.map((subLink, index) => (
<MoreMenuLink key={index} {...props} link={subLink} />
Expand Down
Loading

0 comments on commit 1417279

Please sign in to comment.