Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Analytics scroll depth #12079

Merged
merged 11 commits into from
Jan 9, 2025
36 changes: 17 additions & 19 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {Theme} from '@radix-ui/themes';
import type {Metadata} from 'next';
import {Rubik} from 'next/font/google';
import Script from 'next/script';
import PlausibleProvider from 'next-plausible';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the advantage of having this instead of the script?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

send events dynamically through a hook, nice TS and local DX experience


import {ThemeProvider} from 'sentry-docs/components/theme-provider';

Expand Down Expand Up @@ -31,6 +32,9 @@ export const metadata: Metadata = {
export default function RootLayout({children}: {children: React.ReactNode}) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<PlausibleProvider domain="docs.sentry.io,rollup.sentry.io" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need dev docs as well?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably, we should ask @lizokm

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't look like we have Plausible installed on dev docs (if it is, it's not hooked up to the company account).

</head>
<body className={rubik.variable} suppressHydrationWarning>
<ThemeProvider
attribute="class"
Expand All @@ -42,26 +46,20 @@ export default function RootLayout({children}: {children: React.ReactNode}) {
{children}
</Theme>
</ThemeProvider>
<Script
async
src="https://widget.kapa.ai/kapa-widget.bundle.js"
data-website-id="cac7cc70-969e-4bc1-a968-55534a839be4"
data-button-hide // do not render kapa ai button
data-modal-override-open-class="kapa-ai-class" // all elements with this class will open the kapa ai modal
data-project-name="Sentry"
data-project-color="#6A5FC1"
data-project-logo="https://avatars.githubusercontent.com/u/1396951?s=280&v=4"
data-font-family="var(--font-rubik)"
data-modal-disclaimer="Disclaimer: Welcome to our knowledge search bot! While we'd love to be able to answer all your questions, please remember this is a tool for searching our publicly available sources and not a support forum. Don't include any sensitive or personal information in your queries. For more on how Sentry handles your data, see our [Privacy Policy](https://sentry.io/privacy/). This form is protected by reCAPTCHA. Google's Privacy Policy and Google's Terms of Service apply."
data-modal-example-questions="How to set up Sentry for Next.js?,What are tracePropagationTargets?"
/>
</body>
<Script
defer
data-domain="docs.sentry.io,rollup.sentry.io"
data-api="https://plausible.io/api/event"
src="https://plausible.io/js/script.tagged-events.js"
/>
<Script
async
src="https://widget.kapa.ai/kapa-widget.bundle.js"
data-website-id="cac7cc70-969e-4bc1-a968-55534a839be4"
data-button-hide // do not render kapa ai button
data-modal-override-open-class="kapa-ai-class" // all elements with this class will open the kapa ai modal
data-project-name="Sentry"
data-project-color="#6A5FC1"
data-project-logo="https://avatars.githubusercontent.com/u/1396951?s=280&v=4"
data-font-family="var(--font-rubik)"
data-modal-disclaimer="Disclaimer: Welcome to our knowledge search bot! While we'd love to be able to answer all your questions, please remember this is a tool for searching our publicly available sources and not a support forum. Don't include any sensitive or personal information in your queries. For more on how Sentry handles your data, see our [Privacy Policy](https://sentry.io/privacy/). This form is protected by reCAPTCHA. Google's Privacy Policy and Google's Terms of Service apply."
data-modal-example-questions="How to set up Sentry for Next.js?,What are tracePropagationTargets?"
/>
</html>
);
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"micromark": "^4.0.0",
"next": "15.0.3",
"next-mdx-remote": "^4.4.1",
"next-plausible": "^3.12.4",
"next-themes": "^0.3.0",
"nextjs-toploader": "^1.6.6",
"parse-numeric-range": "^1.3.0",
Expand Down
2 changes: 2 additions & 0 deletions src/components/docPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {PaginationNav} from '../paginationNav';
import {PlatformSdkDetail} from '../platformSdkDetail';
import {Sidebar} from '../sidebar';
import {TableOfContents} from '../tableOfContents';
import {ReaderDepthTracker} from '../track-reader-depth';

type Props = {
children: ReactNode;
Expand Down Expand Up @@ -114,6 +115,7 @@ export function DocPage({
</main>
</section>
<Mermaid />
<ReaderDepthTracker />
</div>
);
}
59 changes: 59 additions & 0 deletions src/components/track-reader-depth.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
'use client';
import {useEffect} from 'react';
import {usePlausible} from 'next-plausible';

import {debounce} from 'sentry-docs/utils';

const EVENT = 'Read Progress';
const milestones = [25, 50, 75, 100] as const;
type Milestone = (typeof milestones)[number];
type EVENT_PROPS = {page: string; readProgress: Milestone};

export function ReaderDepthTracker() {
const plausible = usePlausible<{[EVENT]: EVENT_PROPS}>();

const sendProgressToPlausible = (progress: Milestone) => {
plausible(EVENT, {props: {readProgress: progress, page: document.title}});
};

useEffect(() => {
const reachedMilestones = new Set<Milestone>();

const trackProgress = () => {
// calculate the progress based on the scroll position
const scrollPosition = window.scrollY;
const totalHeight = document.documentElement.scrollHeight - window.innerHeight;
let progress = Math.floor((scrollPosition / totalHeight) * 100);
// it's hard to trigger the 100% milestone, so we'll just assume beyond 95%
if (progress > 95) {
progress = 100;
}

// find the biggest milestone that has not been reached yet
const milestone = milestones.findLast(
m =>
progress >= m &&
!reachedMilestones.has(m) &&
// we shouldn't report smaller milestones once a bigger one has been reached
Array.from(reachedMilestones).every(r => m > r)
);
if (milestone) {
reachedMilestones.add(milestone);
sendProgressToPlausible(milestone);
}
};

// if the page is not scrollable, we don't need to track anything
if (document.documentElement.scrollHeight - window.innerHeight === 0) {
return () => {};
}
const debouncedTrackProgress = debounce(trackProgress, 50);

window.addEventListener('scroll', debouncedTrackProgress);
return () => {
window.removeEventListener('scroll', debouncedTrackProgress);
};
});
// do not render anything
return null;
}
14 changes: 14 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,17 @@ export const isLocalStorageAvailable = () => typeof localStorage !== 'undefined'
export const stripTrailingSlash = (url: string) => {
return url.replace(/\/$/, '');
};

/**
* Debounce function to limit the number of times a function is called.
* @param func The function to be debounced.
* @param wait The time to wait before calling the function.
* @returns A debounced function that only calls the original function after the wait time has passed.
*/
export function debounce<T extends unknown[]>(func: (...args: T) => void, delay: number) {
let timer: ReturnType<typeof setTimeout>;
return function (...args: T) {
clearTimeout(timer);
timer = setTimeout(() => func.apply(this, args), delay);
};
}
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9885,6 +9885,11 @@ next-mdx-remote@^4.4.1:
vfile "^5.3.0"
vfile-matter "^3.0.1"

next-plausible@^3.12.4:
version "3.12.4"
resolved "https://registry.yarnpkg.com/next-plausible/-/next-plausible-3.12.4.tgz#d0ac1d7dcbe9836b6c93e37d42b80e3661fdaa34"
integrity sha512-cD3+ixJxf8yBYvsideTxqli3fvrB7R4BXcvsNJz8Sm2X1QN039WfiXjCyNWkub4h5++rRs6fHhchUMnOuJokcg==

next-themes@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.3.0.tgz#b4d2a866137a67d42564b07f3a3e720e2ff3871a"
Expand Down
Loading