From ed1cb825d235632cbaf83b0fcd14f2074d196fcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Daxb=C3=B6ck?= Date: Mon, 1 Dec 2025 16:04:20 +0100 Subject: [PATCH 01/17] add last_updated information to develop docs --- app/[[...path]]/page.tsx | 16 ++++- src/components/docPage/index.tsx | 3 + src/components/lastUpdated/index.tsx | 102 +++++++++++++++++++++++++++ src/mdx.ts | 14 +++- src/types/frontmatter.ts | 9 +++ src/utils/getGitMetadata.ts | 58 +++++++++++++++ 6 files changed, 200 insertions(+), 2 deletions(-) create mode 100644 src/components/lastUpdated/index.tsx create mode 100644 src/utils/getGitMetadata.ts diff --git a/app/[[...path]]/page.tsx b/app/[[...path]]/page.tsx index 70872af034763..a3ea25b73cb4e 100644 --- a/app/[[...path]]/page.tsx +++ b/app/[[...path]]/page.tsx @@ -131,6 +131,20 @@ export default async function Page(props: {params: Promise<{path?: string[]}>}) } const {mdxSource, frontMatter} = doc; + // Fetch git metadata on-demand for this page only (faster in dev mode) + let gitMetadata = pageNode.frontmatter.gitMetadata; + if (!gitMetadata && pageNode.frontmatter.sourcePath) { + // In dev mode or if not cached, fetch git metadata for current page only + const {getGitMetadata} = await import('sentry-docs/utils/getGitMetadata'); + gitMetadata = getGitMetadata(pageNode.frontmatter.sourcePath); + } + + // Merge gitMetadata into frontMatter + const frontMatterWithGit = { + ...frontMatter, + gitMetadata, + }; + // pass frontmatter tree into sidebar, rendered page + fm into middle, headers into toc const pageType = (params.path?.[0] as PageType) || 'unknown'; return ( @@ -138,7 +152,7 @@ export default async function Page(props: {params: Promise<{path?: string[]}>}) diff --git a/src/components/docPage/index.tsx b/src/components/docPage/index.tsx index 9390875a78d01..eb0eb7a8816c6 100644 --- a/src/components/docPage/index.tsx +++ b/src/components/docPage/index.tsx @@ -16,6 +16,7 @@ import {CopyMarkdownButton} from '../copyMarkdownButton'; import {DocFeedback} from '../docFeedback'; import {GitHubCTA} from '../githubCTA'; import {Header} from '../header'; +import {LastUpdated} from '../lastUpdated'; import Mermaid from '../mermaid'; import {PaginationNav} from '../paginationNav'; import {PlatformSdkDetail} from '../platformSdkDetail'; @@ -94,6 +95,8 @@ export function DocPage({

{frontMatter.title}

+ {/* Show last updated info for develop-docs pages */} + {frontMatter.gitMetadata && }

{frontMatter.description}

{/* This exact id is important for Algolia indexing */} diff --git a/src/components/lastUpdated/index.tsx b/src/components/lastUpdated/index.tsx new file mode 100644 index 0000000000000..d5926b8de8da8 --- /dev/null +++ b/src/components/lastUpdated/index.tsx @@ -0,0 +1,102 @@ +'use client'; + +import Link from 'next/link'; + +interface GitMetadata { + commitHash: string; + author: string; + timestamp: number; +} + +interface LastUpdatedProps { + gitMetadata: GitMetadata; +} + +/** + * Format a timestamp as a relative time string (e.g., "2 days ago") + */ +function formatRelativeTime(timestamp: number): string { + const now = Date.now(); + const diff = now - timestamp * 1000; // timestamp is in seconds + const seconds = Math.floor(diff / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + const months = Math.floor(days / 30); + const years = Math.floor(days / 365); + + if (years > 0) { + return years === 1 ? '1 year ago' : `${years} years ago`; + } + if (months > 0) { + return months === 1 ? '1 month ago' : `${months} months ago`; + } + if (days > 0) { + return days === 1 ? '1 day ago' : `${days} days ago`; + } + if (hours > 0) { + return hours === 1 ? '1 hour ago' : `${hours} hours ago`; + } + if (minutes > 0) { + return minutes === 1 ? '1 minute ago' : `${minutes} minutes ago`; + } + return 'just now'; +} + +/** + * Format a timestamp as a full date string for tooltip + */ +function formatFullDate(timestamp: number): string { + const date = new Date(timestamp * 1000); + return date.toLocaleString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + hour12: true, + }); +} + +/** + * Abbreviate a commit hash to first 7 characters + */ +function abbreviateHash(hash: string): string { + return hash.substring(0, 7); +} + +export function LastUpdated({gitMetadata}: LastUpdatedProps) { + const {commitHash, author, timestamp} = gitMetadata; + const relativeTime = formatRelativeTime(timestamp); + const fullDate = formatFullDate(timestamp); + const abbreviatedHash = abbreviateHash(commitHash); + const commitUrl = `https://github.com/getsentry/sentry-docs/commit/${commitHash}`; + + return ( +
+ {/* Text content */} + + updated by + {author} + {/* Relative time with tooltip */} + + {relativeTime} + + + + {/* Commit link */} + + + + #{abbreviatedHash} + + +
+ ); +} + diff --git a/src/mdx.ts b/src/mdx.ts index 611e2f212ead1..0e621bc695994 100644 --- a/src/mdx.ts +++ b/src/mdx.ts @@ -252,10 +252,22 @@ export async function getDevDocsFrontMatterUncached(): Promise { const source = await readFile(file, 'utf8'); const {data: frontmatter} = matter(source); + const sourcePath = path.join(folder, fileName); + + // In production builds, fetch git metadata for all pages upfront + // In development, skip this and fetch on-demand per page (faster dev server startup) + let gitMetadata: typeof frontmatter.gitMetadata = undefined; + if (process.env.NODE_ENV !== 'development') { + const {getGitMetadata} = await import('./utils/getGitMetadata'); + const metadata = getGitMetadata(sourcePath); + gitMetadata = metadata ?? undefined; + } + return { ...(frontmatter as FrontMatter), slug: fileName.replace(/\/index.mdx?$/, '').replace(/\.mdx?$/, ''), - sourcePath: path.join(folder, fileName), + sourcePath, + gitMetadata, }; }, {concurrency: FILE_CONCURRENCY_LIMIT} diff --git a/src/types/frontmatter.ts b/src/types/frontmatter.ts index a336bcefefe48..6477c336fc5a5 100644 --- a/src/types/frontmatter.ts +++ b/src/types/frontmatter.ts @@ -114,6 +114,15 @@ export interface FrontMatter { */ sourcePath?: string; + /** + * Git metadata for the last commit that modified this file + */ + gitMetadata?: { + commitHash: string; + author: string; + timestamp: number; + }; + /** * Specific guides that this page is relevant to. */ diff --git a/src/utils/getGitMetadata.ts b/src/utils/getGitMetadata.ts new file mode 100644 index 0000000000000..4ef67dcfe3c6d --- /dev/null +++ b/src/utils/getGitMetadata.ts @@ -0,0 +1,58 @@ +import {execSync} from 'child_process'; +import path from 'path'; + +export interface GitMetadata { + commitHash: string; + author: string; + timestamp: number; +} + +// Cache to avoid repeated git calls during build +const gitMetadataCache = new Map(); + +/** + * Get git metadata for a file + * @param filePath - Path to the file relative to the repository root + * @returns Git metadata or null if unavailable + */ +export function getGitMetadata(filePath: string): GitMetadata | null { + // Check cache first + if (gitMetadataCache.has(filePath)) { + return gitMetadataCache.get(filePath) ?? null; + } + + try { + // Get commit hash, author name, and timestamp + const logOutput = execSync( + `git log -1 --format="%H|%an|%at" -- "${filePath}"`, + { + encoding: 'utf8', + cwd: path.resolve(process.cwd()), + stdio: ['pipe', 'pipe', 'ignore'], // Suppress stderr + } + ).trim(); + + if (!logOutput) { + // No commits found for this file + gitMetadataCache.set(filePath, null); + return null; + } + + const [commitHash, author, timestampStr] = logOutput.split('|'); + const timestamp = parseInt(timestampStr, 10); + + const metadata: GitMetadata = { + commitHash, + author, + timestamp, + }; + + gitMetadataCache.set(filePath, metadata); + return metadata; + } catch (error) { + // Git command failed or file doesn't exist in git + gitMetadataCache.set(filePath, null); + return null; + } +} + From 16ad85f26460aa9c6315786f2b6fc721bf2308b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Daxb=C3=B6ck?= Date: Mon, 1 Dec 2025 16:15:27 +0100 Subject: [PATCH 02/17] fix TS null vs undefined --- app/[[...path]]/page.tsx | 3 ++- src/types/frontmatter.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/[[...path]]/page.tsx b/app/[[...path]]/page.tsx index a3ea25b73cb4e..bee2312ba6775 100644 --- a/app/[[...path]]/page.tsx +++ b/app/[[...path]]/page.tsx @@ -136,7 +136,8 @@ export default async function Page(props: {params: Promise<{path?: string[]}>}) if (!gitMetadata && pageNode.frontmatter.sourcePath) { // In dev mode or if not cached, fetch git metadata for current page only const {getGitMetadata} = await import('sentry-docs/utils/getGitMetadata'); - gitMetadata = getGitMetadata(pageNode.frontmatter.sourcePath); + const metadata = getGitMetadata(pageNode.frontmatter.sourcePath); + gitMetadata = metadata ?? undefined; } // Merge gitMetadata into frontMatter diff --git a/src/types/frontmatter.ts b/src/types/frontmatter.ts index 6477c336fc5a5..a7974ae261ebe 100644 --- a/src/types/frontmatter.ts +++ b/src/types/frontmatter.ts @@ -115,7 +115,7 @@ export interface FrontMatter { sourcePath?: string; /** - * Git metadata for the last commit that modified this file + * Git metadata for the last commit & author that modified this file */ gitMetadata?: { commitHash: string; From 4e916b66ff994a35d0021a61fda992d322c9fa5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Daxb=C3=B6ck?= Date: Mon, 1 Dec 2025 16:20:32 +0100 Subject: [PATCH 03/17] Lint --- src/components/lastUpdated/index.tsx | 2 +- src/types/frontmatter.ts | 20 ++++++++++---------- src/utils/getGitMetadata.ts | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/components/lastUpdated/index.tsx b/src/components/lastUpdated/index.tsx index d5926b8de8da8..934115f6a5abf 100644 --- a/src/components/lastUpdated/index.tsx +++ b/src/components/lastUpdated/index.tsx @@ -3,8 +3,8 @@ import Link from 'next/link'; interface GitMetadata { - commitHash: string; author: string; + commitHash: string; timestamp: number; } diff --git a/src/types/frontmatter.ts b/src/types/frontmatter.ts index a7974ae261ebe..0791839148e89 100644 --- a/src/types/frontmatter.ts +++ b/src/types/frontmatter.ts @@ -35,15 +35,23 @@ export interface FrontMatter { */ fullWidth?: boolean; + /** + * Git metadata for the last commit & author that modified this file + */ + gitMetadata?: { + author: string; + commitHash: string; + timestamp: number; + }; /** * A list of keywords for indexing with search. */ keywords?: string[]; + /** * Set this to true to show a "new" badge next to the title in the sidebar */ new?: boolean; - /** * The next page in the bottom pagination navigation. */ @@ -53,6 +61,7 @@ export interface FrontMatter { * takes precedence over children when present */ next_steps?: string[]; + /** * Set this to true to disable indexing (robots, algolia) of this content. */ @@ -114,15 +123,6 @@ export interface FrontMatter { */ sourcePath?: string; - /** - * Git metadata for the last commit & author that modified this file - */ - gitMetadata?: { - commitHash: string; - author: string; - timestamp: number; - }; - /** * Specific guides that this page is relevant to. */ diff --git a/src/utils/getGitMetadata.ts b/src/utils/getGitMetadata.ts index 4ef67dcfe3c6d..8edac72ed82b9 100644 --- a/src/utils/getGitMetadata.ts +++ b/src/utils/getGitMetadata.ts @@ -2,8 +2,8 @@ import {execSync} from 'child_process'; import path from 'path'; export interface GitMetadata { - commitHash: string; author: string; + commitHash: string; timestamp: number; } From 928a87834f69d874a94a006af59451850ba6cce2 Mon Sep 17 00:00:00 2001 From: "getsantry[bot]" <66042841+getsantry[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 22:14:47 +0000 Subject: [PATCH 04/17] [getsentry/action-github-commit] Auto commit --- src/components/docPage/index.tsx | 4 +++- src/components/lastUpdated/index.tsx | 1 - src/mdx.ts | 4 ++-- src/utils/getGitMetadata.ts | 14 +++++--------- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/components/docPage/index.tsx b/src/components/docPage/index.tsx index eb0eb7a8816c6..3e9171a864f26 100644 --- a/src/components/docPage/index.tsx +++ b/src/components/docPage/index.tsx @@ -96,7 +96,9 @@ export function DocPage({

{frontMatter.title}

{/* Show last updated info for develop-docs pages */} - {frontMatter.gitMetadata && } + {frontMatter.gitMetadata && ( + + )}

{frontMatter.description}

{/* This exact id is important for Algolia indexing */} diff --git a/src/components/lastUpdated/index.tsx b/src/components/lastUpdated/index.tsx index 934115f6a5abf..e076aa679377b 100644 --- a/src/components/lastUpdated/index.tsx +++ b/src/components/lastUpdated/index.tsx @@ -99,4 +99,3 @@ export function LastUpdated({gitMetadata}: LastUpdatedProps) {
); } - diff --git a/src/mdx.ts b/src/mdx.ts index 0e621bc695994..4e2ba9a51f048 100644 --- a/src/mdx.ts +++ b/src/mdx.ts @@ -253,7 +253,7 @@ export async function getDevDocsFrontMatterUncached(): Promise { const source = await readFile(file, 'utf8'); const {data: frontmatter} = matter(source); const sourcePath = path.join(folder, fileName); - + // In production builds, fetch git metadata for all pages upfront // In development, skip this and fetch on-demand per page (faster dev server startup) let gitMetadata: typeof frontmatter.gitMetadata = undefined; @@ -262,7 +262,7 @@ export async function getDevDocsFrontMatterUncached(): Promise { const metadata = getGitMetadata(sourcePath); gitMetadata = metadata ?? undefined; } - + return { ...(frontmatter as FrontMatter), slug: fileName.replace(/\/index.mdx?$/, '').replace(/\.mdx?$/, ''), diff --git a/src/utils/getGitMetadata.ts b/src/utils/getGitMetadata.ts index 8edac72ed82b9..3d494ebbcef6c 100644 --- a/src/utils/getGitMetadata.ts +++ b/src/utils/getGitMetadata.ts @@ -23,14 +23,11 @@ export function getGitMetadata(filePath: string): GitMetadata | null { try { // Get commit hash, author name, and timestamp - const logOutput = execSync( - `git log -1 --format="%H|%an|%at" -- "${filePath}"`, - { - encoding: 'utf8', - cwd: path.resolve(process.cwd()), - stdio: ['pipe', 'pipe', 'ignore'], // Suppress stderr - } - ).trim(); + const logOutput = execSync(`git log -1 --format="%H|%an|%at" -- "${filePath}"`, { + encoding: 'utf8', + cwd: path.resolve(process.cwd()), + stdio: ['pipe', 'pipe', 'ignore'], // Suppress stderr + }).trim(); if (!logOutput) { // No commits found for this file @@ -55,4 +52,3 @@ export function getGitMetadata(filePath: string): GitMetadata | null { return null; } } - From 60325f6e3f76f043eb061d6449362da86ff3ca11 Mon Sep 17 00:00:00 2001 From: "getsantry[bot]" <66042841+getsantry[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 22:15:05 +0000 Subject: [PATCH 05/17] [getsentry/action-github-commit] Auto commit From 22617744d14ba8ab5cc318058031927a3377fb0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Daxb=C3=B6ck?= Date: Mon, 1 Dec 2025 23:56:09 +0100 Subject: [PATCH 06/17] Fix for only showing on develop docs --- app/[[...path]]/page.tsx | 2 +- src/mdx.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/[[...path]]/page.tsx b/app/[[...path]]/page.tsx index bee2312ba6775..d6a191513040a 100644 --- a/app/[[...path]]/page.tsx +++ b/app/[[...path]]/page.tsx @@ -133,7 +133,7 @@ export default async function Page(props: {params: Promise<{path?: string[]}>}) // Fetch git metadata on-demand for this page only (faster in dev mode) let gitMetadata = pageNode.frontmatter.gitMetadata; - if (!gitMetadata && pageNode.frontmatter.sourcePath) { + if (!gitMetadata && pageNode.frontmatter.sourcePath?.startsWith('develop-docs/')) { // In dev mode or if not cached, fetch git metadata for current page only const {getGitMetadata} = await import('sentry-docs/utils/getGitMetadata'); const metadata = getGitMetadata(pageNode.frontmatter.sourcePath); diff --git a/src/mdx.ts b/src/mdx.ts index 4e2ba9a51f048..077ba5931c01b 100644 --- a/src/mdx.ts +++ b/src/mdx.ts @@ -253,11 +253,11 @@ export async function getDevDocsFrontMatterUncached(): Promise { const source = await readFile(file, 'utf8'); const {data: frontmatter} = matter(source); const sourcePath = path.join(folder, fileName); - - // In production builds, fetch git metadata for all pages upfront + + // In production builds, fetch git metadata for develop-docs pages only // In development, skip this and fetch on-demand per page (faster dev server startup) let gitMetadata: typeof frontmatter.gitMetadata = undefined; - if (process.env.NODE_ENV !== 'development') { + if (process.env.NODE_ENV !== 'development' && sourcePath.startsWith('develop-docs/')) { const {getGitMetadata} = await import('./utils/getGitMetadata'); const metadata = getGitMetadata(sourcePath); gitMetadata = metadata ?? undefined; From 353c89564549c56caaed5e7fdb44658c7e4b37cf Mon Sep 17 00:00:00 2001 From: "getsantry[bot]" <66042841+getsantry[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 23:02:46 +0000 Subject: [PATCH 07/17] [getsentry/action-github-commit] Auto commit --- src/mdx.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/mdx.ts b/src/mdx.ts index 077ba5931c01b..7e4462e1035bc 100644 --- a/src/mdx.ts +++ b/src/mdx.ts @@ -253,11 +253,14 @@ export async function getDevDocsFrontMatterUncached(): Promise { const source = await readFile(file, 'utf8'); const {data: frontmatter} = matter(source); const sourcePath = path.join(folder, fileName); - + // In production builds, fetch git metadata for develop-docs pages only // In development, skip this and fetch on-demand per page (faster dev server startup) let gitMetadata: typeof frontmatter.gitMetadata = undefined; - if (process.env.NODE_ENV !== 'development' && sourcePath.startsWith('develop-docs/')) { + if ( + process.env.NODE_ENV !== 'development' && + sourcePath.startsWith('develop-docs/') + ) { const {getGitMetadata} = await import('./utils/getGitMetadata'); const metadata = getGitMetadata(sourcePath); gitMetadata = metadata ?? undefined; From f387289be43c8902c884849cf28926a48b2d336d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Daxb=C3=B6ck?= Date: Tue, 2 Dec 2025 00:22:55 +0100 Subject: [PATCH 08/17] return new object copies, preventing reference sharing in cached metadata --- src/utils/getGitMetadata.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/utils/getGitMetadata.ts b/src/utils/getGitMetadata.ts index 3d494ebbcef6c..b49fed7b5234b 100644 --- a/src/utils/getGitMetadata.ts +++ b/src/utils/getGitMetadata.ts @@ -18,7 +18,9 @@ const gitMetadataCache = new Map(); export function getGitMetadata(filePath: string): GitMetadata | null { // Check cache first if (gitMetadataCache.has(filePath)) { - return gitMetadataCache.get(filePath) ?? null; + const cached = gitMetadataCache.get(filePath); + // Return a NEW copy to avoid reference sharing + return cached ? { ...cached } : null; } try { @@ -38,14 +40,23 @@ export function getGitMetadata(filePath: string): GitMetadata | null { const [commitHash, author, timestampStr] = logOutput.split('|'); const timestamp = parseInt(timestampStr, 10); + // Create a fresh object for each call to avoid reference sharing const metadata: GitMetadata = { commitHash, author, timestamp, }; + // Cache the metadata gitMetadataCache.set(filePath, metadata); - return metadata; + + // IMPORTANT: Return a NEW object, not the cached one + // This prevents all pages from sharing the same object reference + return { + commitHash: metadata.commitHash, + author: metadata.author, + timestamp: metadata.timestamp, + }; } catch (error) { // Git command failed or file doesn't exist in git gitMetadataCache.set(filePath, null); From df13418a2d20654ed6fd7f3c13b00e2ae498502d Mon Sep 17 00:00:00 2001 From: "getsantry[bot]" <66042841+getsantry[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 23:24:03 +0000 Subject: [PATCH 09/17] [getsentry/action-github-commit] Auto commit --- src/utils/getGitMetadata.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/getGitMetadata.ts b/src/utils/getGitMetadata.ts index b49fed7b5234b..549627aa5e5e1 100644 --- a/src/utils/getGitMetadata.ts +++ b/src/utils/getGitMetadata.ts @@ -20,7 +20,7 @@ export function getGitMetadata(filePath: string): GitMetadata | null { if (gitMetadataCache.has(filePath)) { const cached = gitMetadataCache.get(filePath); // Return a NEW copy to avoid reference sharing - return cached ? { ...cached } : null; + return cached ? {...cached} : null; } try { @@ -49,7 +49,7 @@ export function getGitMetadata(filePath: string): GitMetadata | null { // Cache the metadata gitMetadataCache.set(filePath, metadata); - + // IMPORTANT: Return a NEW object, not the cached one // This prevents all pages from sharing the same object reference return { From af1d879d2f0596387d3e70e3bf2145eb747dbf42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Daxb=C3=B6ck?= Date: Tue, 2 Dec 2025 14:03:58 +0100 Subject: [PATCH 10/17] Add some debug code --- src/mdx.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/mdx.ts b/src/mdx.ts index 7e4462e1035bc..5e5a6ab0bc92f 100644 --- a/src/mdx.ts +++ b/src/mdx.ts @@ -263,7 +263,13 @@ export async function getDevDocsFrontMatterUncached(): Promise { ) { const {getGitMetadata} = await import('./utils/getGitMetadata'); const metadata = getGitMetadata(sourcePath); - gitMetadata = metadata ?? undefined; + // Ensure we create a completely new object to avoid any reference sharing + gitMetadata = metadata ? {...metadata} : undefined; + + // Log during build to debug Vercel issues + if (process.env.CI || process.env.VERCEL) { + console.log(`[BUILD] Git metadata for ${sourcePath}:`, gitMetadata); + } } return { From 6733525161fa7b9bbf02e71c6f086fd159c36c76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Daxb=C3=B6ck?= Date: Tue, 2 Dec 2025 14:04:20 +0100 Subject: [PATCH 11/17] Update getGitMetadata.ts --- src/utils/getGitMetadata.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/utils/getGitMetadata.ts b/src/utils/getGitMetadata.ts index 549627aa5e5e1..07b3688227be4 100644 --- a/src/utils/getGitMetadata.ts +++ b/src/utils/getGitMetadata.ts @@ -31,6 +31,11 @@ export function getGitMetadata(filePath: string): GitMetadata | null { stdio: ['pipe', 'pipe', 'ignore'], // Suppress stderr }).trim(); + // Log for debugging on Vercel + if (process.env.CI || process.env.VERCEL) { + console.log(`[getGitMetadata] File: ${filePath} -> Output: ${logOutput}`); + } + if (!logOutput) { // No commits found for this file gitMetadataCache.set(filePath, null); From dbee391430ddf912d027983aac1750f598d1e492 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Daxb=C3=B6ck?= Date: Thu, 15 Jan 2026 13:48:59 +0100 Subject: [PATCH 12/17] Capture queue time docs --- .../ruby/common/configuration/options.mdx | 26 ++++++++++ .../automatic-instrumentation.mdx | 2 + .../instrumentation/performance-metrics.mdx | 2 + .../performance/queue-time-capture/ruby.mdx | 50 +++++++++++++++++++ 4 files changed, 80 insertions(+) create mode 100644 platform-includes/performance/queue-time-capture/ruby.mdx diff --git a/docs/platforms/ruby/common/configuration/options.mdx b/docs/platforms/ruby/common/configuration/options.mdx index 49955f5533f8f..bb646f46fee0e 100644 --- a/docs/platforms/ruby/common/configuration/options.mdx +++ b/docs/platforms/ruby/common/configuration/options.mdx @@ -326,6 +326,32 @@ config.trace_ignore_status_codes = [404, (502..511)] + + +Automatically capture how long requests wait in the web server queue before processing begins. The SDK reads the `X-Request-Start` header set by reverse proxies (Nginx, HAProxy, Heroku) and attaches queue time to transactions as `http.queue_time_ms`. + +This helps identify when requests are delayed due to insufficient worker threads or server capacity, which is especially useful under load. + +To disable queue time capture: + +```ruby +config.capture_queue_time = false +``` + +**Nginx:** + +```nginx +proxy_set_header X-Request-Start "t=${msec}"; +``` + +**HAProxy:** + +```haproxy +http-request set-header X-Request-Start t=%Ts%ms +``` + + + The instrumenter to use, `:sentry` or `:otel` for [use with OpenTelemetry](../../tracing/instrumentation/opentelemetry). diff --git a/docs/platforms/ruby/common/tracing/instrumentation/automatic-instrumentation.mdx b/docs/platforms/ruby/common/tracing/instrumentation/automatic-instrumentation.mdx index 06e5c62996a28..969849a219443 100644 --- a/docs/platforms/ruby/common/tracing/instrumentation/automatic-instrumentation.mdx +++ b/docs/platforms/ruby/common/tracing/instrumentation/automatic-instrumentation.mdx @@ -20,5 +20,7 @@ Spans are instrumented for the following operations within a transaction: - includes common database systems such as Postgres and MySQL - Outgoing HTTP requests made with `Net::HTTP` - Redis operations +- Queue time for requests behind reverse proxies (Nginx, HAProxy, Heroku) + - Requires `X-Request-Start` header from reverse proxy Spans are only created within an existing transaction. If you're not using any of the supported frameworks, you'll need to create transactions manually. diff --git a/docs/platforms/ruby/common/tracing/instrumentation/performance-metrics.mdx b/docs/platforms/ruby/common/tracing/instrumentation/performance-metrics.mdx index 8955adc964609..ab8c95a9b51ee 100644 --- a/docs/platforms/ruby/common/tracing/instrumentation/performance-metrics.mdx +++ b/docs/platforms/ruby/common/tracing/instrumentation/performance-metrics.mdx @@ -22,6 +22,8 @@ Sentry supports adding arbitrary custom units, but we recommend using one of the + + ## Supported Measurement Units Units augment measurement values by giving meaning to what otherwise might be abstract numbers. Adding units also allows Sentry to offer controls - unit conversions, filters, and so on - based on those units. For values that are unitless, you can supply an empty string or `none`. diff --git a/platform-includes/performance/queue-time-capture/ruby.mdx b/platform-includes/performance/queue-time-capture/ruby.mdx new file mode 100644 index 0000000000000..8385fa823c739 --- /dev/null +++ b/platform-includes/performance/queue-time-capture/ruby.mdx @@ -0,0 +1,50 @@ +## Automatic Queue Time Capture + +The Ruby SDK automatically captures queue time for Rack-based applications when the `X-Request-Start` header is present. This measures how long requests wait in the web server queue (e.g., waiting for a Puma thread) before your application begins processing them. + +Queue time is attached to transactions as `http.queue_time_ms` and helps identify server capacity issues. + +### Setup + +Configure your reverse proxy to add the `X-Request-Start` header: + +**Nginx:** + +```nginx +location / { + proxy_pass http://your-app; + proxy_set_header X-Request-Start "t=${msec}"; +} +``` + +**HAProxy:** + +```haproxy +frontend http-in + http-request set-header X-Request-Start t=%Ts%ms +``` + +**Heroku:** The header is automatically set by Heroku's router. + +### How It Works + +The SDK: + +1. Reads the `X-Request-Start` header timestamp from your reverse proxy +2. Calculates the time difference between the header timestamp and when the request reaches your application +3. Subtracts `puma.request_body_wait` (if present) to exclude time spent waiting for slow client uploads +4. Attaches the result as `http.queue_time_ms` to the transaction + +### Disable Queue Time Capture + +If you don't want queue time captured, disable it in your configuration: + +```ruby +Sentry.init do |config| + config.capture_queue_time = false +end +``` + +### Viewing Queue Time + +Queue time appears in the Sentry transaction details under the "Data" section as `http.queue_time_ms` (measured in milliseconds). From 340aa909f29e8727aa5cc7d067da9cd8f572d792 Mon Sep 17 00:00:00 2001 From: "getsantry[bot]" <66042841+getsantry[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 12:51:19 +0000 Subject: [PATCH 13/17] [getsentry/action-github-commit] Auto commit --- src/mdx.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mdx.ts b/src/mdx.ts index cba6fdf0dff32..d94cb2a664a61 100644 --- a/src/mdx.ts +++ b/src/mdx.ts @@ -281,7 +281,7 @@ export async function getDevDocsFrontMatterUncached(): Promise { const metadata = getGitMetadata(sourcePath); // Ensure we create a completely new object to avoid any reference sharing gitMetadata = metadata ? {...metadata} : undefined; - + // Log during build to debug Vercel issues if (process.env.CI || process.env.VERCEL) { console.log(`[BUILD] Git metadata for ${sourcePath}:`, gitMetadata); From d421c309ade72c52aa953414ab697ce88913ef13 Mon Sep 17 00:00:00 2001 From: "getsantry[bot]" <66042841+getsantry[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 12:51:49 +0000 Subject: [PATCH 14/17] [getsentry/action-github-commit] Auto commit From 72c0502169e91ad067b58274e97cdacc15ea2eb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Daxb=C3=B6ck?= Date: Fri, 16 Jan 2026 13:55:44 +0100 Subject: [PATCH 15/17] remove unrelated code --- app/[[...path]]/page.tsx | 17 +---- src/components/docPage/index.tsx | 5 -- src/components/lastUpdated/index.tsx | 101 --------------------------- src/mdx.ts | 22 +----- src/types/frontmatter.ts | 8 --- src/utils/getGitMetadata.ts | 70 ------------------- 6 files changed, 2 insertions(+), 221 deletions(-) delete mode 100644 src/components/lastUpdated/index.tsx delete mode 100644 src/utils/getGitMetadata.ts diff --git a/app/[[...path]]/page.tsx b/app/[[...path]]/page.tsx index 0e47344541bc3..9a17e9433d095 100644 --- a/app/[[...path]]/page.tsx +++ b/app/[[...path]]/page.tsx @@ -131,21 +131,6 @@ export default async function Page(props: {params: Promise<{path?: string[]}>}) } const {mdxSource, frontMatter} = doc; - // Fetch git metadata on-demand for this page only (faster in dev mode) - let gitMetadata = pageNode.frontmatter.gitMetadata; - if (!gitMetadata && pageNode.frontmatter.sourcePath?.startsWith('develop-docs/')) { - // In dev mode or if not cached, fetch git metadata for current page only - const {getGitMetadata} = await import('sentry-docs/utils/getGitMetadata'); - const metadata = getGitMetadata(pageNode.frontmatter.sourcePath); - gitMetadata = metadata ?? undefined; - } - - // Merge gitMetadata into frontMatter - const frontMatterWithGit = { - ...frontMatter, - gitMetadata, - }; - // pass frontmatter tree into sidebar, rendered page + fm into middle, headers into toc const pageType = (params.path?.[0] as PageType) || 'unknown'; return ( @@ -153,7 +138,7 @@ export default async function Page(props: {params: Promise<{path?: string[]}>}) diff --git a/src/components/docPage/index.tsx b/src/components/docPage/index.tsx index 3e9171a864f26..9390875a78d01 100644 --- a/src/components/docPage/index.tsx +++ b/src/components/docPage/index.tsx @@ -16,7 +16,6 @@ import {CopyMarkdownButton} from '../copyMarkdownButton'; import {DocFeedback} from '../docFeedback'; import {GitHubCTA} from '../githubCTA'; import {Header} from '../header'; -import {LastUpdated} from '../lastUpdated'; import Mermaid from '../mermaid'; import {PaginationNav} from '../paginationNav'; import {PlatformSdkDetail} from '../platformSdkDetail'; @@ -95,10 +94,6 @@ export function DocPage({

{frontMatter.title}

- {/* Show last updated info for develop-docs pages */} - {frontMatter.gitMetadata && ( - - )}

{frontMatter.description}

{/* This exact id is important for Algolia indexing */} diff --git a/src/components/lastUpdated/index.tsx b/src/components/lastUpdated/index.tsx deleted file mode 100644 index e076aa679377b..0000000000000 --- a/src/components/lastUpdated/index.tsx +++ /dev/null @@ -1,101 +0,0 @@ -'use client'; - -import Link from 'next/link'; - -interface GitMetadata { - author: string; - commitHash: string; - timestamp: number; -} - -interface LastUpdatedProps { - gitMetadata: GitMetadata; -} - -/** - * Format a timestamp as a relative time string (e.g., "2 days ago") - */ -function formatRelativeTime(timestamp: number): string { - const now = Date.now(); - const diff = now - timestamp * 1000; // timestamp is in seconds - const seconds = Math.floor(diff / 1000); - const minutes = Math.floor(seconds / 60); - const hours = Math.floor(minutes / 60); - const days = Math.floor(hours / 24); - const months = Math.floor(days / 30); - const years = Math.floor(days / 365); - - if (years > 0) { - return years === 1 ? '1 year ago' : `${years} years ago`; - } - if (months > 0) { - return months === 1 ? '1 month ago' : `${months} months ago`; - } - if (days > 0) { - return days === 1 ? '1 day ago' : `${days} days ago`; - } - if (hours > 0) { - return hours === 1 ? '1 hour ago' : `${hours} hours ago`; - } - if (minutes > 0) { - return minutes === 1 ? '1 minute ago' : `${minutes} minutes ago`; - } - return 'just now'; -} - -/** - * Format a timestamp as a full date string for tooltip - */ -function formatFullDate(timestamp: number): string { - const date = new Date(timestamp * 1000); - return date.toLocaleString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: 'numeric', - minute: '2-digit', - hour12: true, - }); -} - -/** - * Abbreviate a commit hash to first 7 characters - */ -function abbreviateHash(hash: string): string { - return hash.substring(0, 7); -} - -export function LastUpdated({gitMetadata}: LastUpdatedProps) { - const {commitHash, author, timestamp} = gitMetadata; - const relativeTime = formatRelativeTime(timestamp); - const fullDate = formatFullDate(timestamp); - const abbreviatedHash = abbreviateHash(commitHash); - const commitUrl = `https://github.com/getsentry/sentry-docs/commit/${commitHash}`; - - return ( -
- {/* Text content */} - - updated by - {author} - {/* Relative time with tooltip */} - - {relativeTime} - - - - {/* Commit link */} - - - - #{abbreviatedHash} - - -
- ); -} diff --git a/src/mdx.ts b/src/mdx.ts index d94cb2a664a61..af0374a87ebf7 100644 --- a/src/mdx.ts +++ b/src/mdx.ts @@ -268,31 +268,11 @@ export async function getDevDocsFrontMatterUncached(): Promise { const source = await readFile(file, 'utf8'); const {data: frontmatter} = matter(source); - const sourcePath = path.join(folder, fileName); - - // In production builds, fetch git metadata for develop-docs pages only - // In development, skip this and fetch on-demand per page (faster dev server startup) - let gitMetadata: typeof frontmatter.gitMetadata = undefined; - if ( - process.env.NODE_ENV !== 'development' && - sourcePath.startsWith('develop-docs/') - ) { - const {getGitMetadata} = await import('./utils/getGitMetadata'); - const metadata = getGitMetadata(sourcePath); - // Ensure we create a completely new object to avoid any reference sharing - gitMetadata = metadata ? {...metadata} : undefined; - - // Log during build to debug Vercel issues - if (process.env.CI || process.env.VERCEL) { - console.log(`[BUILD] Git metadata for ${sourcePath}:`, gitMetadata); - } - } return { ...(frontmatter as FrontMatter), slug: fileName.replace(/\/index.mdx?$/, '').replace(/\.mdx?$/, ''), - sourcePath, - gitMetadata, + sourcePath: path.join(folder, fileName), }; }, {concurrency: FILE_CONCURRENCY_LIMIT} diff --git a/src/types/frontmatter.ts b/src/types/frontmatter.ts index 0791839148e89..aed7608e1ec61 100644 --- a/src/types/frontmatter.ts +++ b/src/types/frontmatter.ts @@ -35,14 +35,6 @@ export interface FrontMatter { */ fullWidth?: boolean; - /** - * Git metadata for the last commit & author that modified this file - */ - gitMetadata?: { - author: string; - commitHash: string; - timestamp: number; - }; /** * A list of keywords for indexing with search. */ diff --git a/src/utils/getGitMetadata.ts b/src/utils/getGitMetadata.ts deleted file mode 100644 index 07b3688227be4..0000000000000 --- a/src/utils/getGitMetadata.ts +++ /dev/null @@ -1,70 +0,0 @@ -import {execSync} from 'child_process'; -import path from 'path'; - -export interface GitMetadata { - author: string; - commitHash: string; - timestamp: number; -} - -// Cache to avoid repeated git calls during build -const gitMetadataCache = new Map(); - -/** - * Get git metadata for a file - * @param filePath - Path to the file relative to the repository root - * @returns Git metadata or null if unavailable - */ -export function getGitMetadata(filePath: string): GitMetadata | null { - // Check cache first - if (gitMetadataCache.has(filePath)) { - const cached = gitMetadataCache.get(filePath); - // Return a NEW copy to avoid reference sharing - return cached ? {...cached} : null; - } - - try { - // Get commit hash, author name, and timestamp - const logOutput = execSync(`git log -1 --format="%H|%an|%at" -- "${filePath}"`, { - encoding: 'utf8', - cwd: path.resolve(process.cwd()), - stdio: ['pipe', 'pipe', 'ignore'], // Suppress stderr - }).trim(); - - // Log for debugging on Vercel - if (process.env.CI || process.env.VERCEL) { - console.log(`[getGitMetadata] File: ${filePath} -> Output: ${logOutput}`); - } - - if (!logOutput) { - // No commits found for this file - gitMetadataCache.set(filePath, null); - return null; - } - - const [commitHash, author, timestampStr] = logOutput.split('|'); - const timestamp = parseInt(timestampStr, 10); - - // Create a fresh object for each call to avoid reference sharing - const metadata: GitMetadata = { - commitHash, - author, - timestamp, - }; - - // Cache the metadata - gitMetadataCache.set(filePath, metadata); - - // IMPORTANT: Return a NEW object, not the cached one - // This prevents all pages from sharing the same object reference - return { - commitHash: metadata.commitHash, - author: metadata.author, - timestamp: metadata.timestamp, - }; - } catch (error) { - // Git command failed or file doesn't exist in git - gitMetadataCache.set(filePath, null); - return null; - } -} From 9dec483e7d26dd769dab8c4aac868b2341027915 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Daxb=C3=B6ck?= <114897+dingsdax@users.noreply.github.com> Date: Fri, 23 Jan 2026 13:44:22 +0100 Subject: [PATCH 16/17] Apply suggestions from code review Co-authored-by: Alex Krawiec --- docs/platforms/ruby/common/configuration/options.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/platforms/ruby/common/configuration/options.mdx b/docs/platforms/ruby/common/configuration/options.mdx index 9767dbd1316ef..23cd01e5c980b 100644 --- a/docs/platforms/ruby/common/configuration/options.mdx +++ b/docs/platforms/ruby/common/configuration/options.mdx @@ -330,7 +330,7 @@ config.trace_ignore_status_codes = [404, (502..511)] Automatically capture how long requests wait in the web server queue before processing begins. The SDK reads the `X-Request-Start` header set by reverse proxies (Nginx, HAProxy, Heroku) and attaches queue time to transactions as `http.queue_time_ms`. -This helps identify when requests are delayed due to insufficient worker threads or server capacity, which is especially useful under load. +This helps identify when requests are delayed due to insufficient worker threads or server capacity, which is especially useful under high load. To disable queue time capture: From f7ddb6f67087eea46ad6d6e2435a6292cde6ba6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Daxb=C3=B6ck?= Date: Fri, 23 Jan 2026 15:20:29 +0100 Subject: [PATCH 17/17] Fix attribute name, simplify options --- .../ruby/common/configuration/options.mdx | 16 ++-------------- .../performance/queue-time-capture/ruby.mdx | 6 +++--- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/docs/platforms/ruby/common/configuration/options.mdx b/docs/platforms/ruby/common/configuration/options.mdx index 23cd01e5c980b..fe7819fa04dcf 100644 --- a/docs/platforms/ruby/common/configuration/options.mdx +++ b/docs/platforms/ruby/common/configuration/options.mdx @@ -328,9 +328,9 @@ config.trace_ignore_status_codes = [404, (502..511)] -Automatically capture how long requests wait in the web server queue before processing begins. The SDK reads the `X-Request-Start` header set by reverse proxies (Nginx, HAProxy, Heroku) and attaches queue time to transactions as `http.queue_time_ms`. +Automatically capture how long requests wait in the web server queue before processing begins. The SDK reads the `X-Request-Start` header set by reverse proxies (Nginx, HAProxy, Heroku) and attaches queue time to transactions as `http.server.request.time_in_queue`. -This helps identify when requests are delayed due to insufficient worker threads or server capacity, which is especially useful under high load. +This helps identify when requests are delayed due to insufficient worker threads or server capacity, which is especially useful under load. Learn more about [automatic queue time capture](/tracing/instrumentation/performance-metrics/#automatic-queue-time-capture). To disable queue time capture: @@ -338,18 +338,6 @@ To disable queue time capture: config.capture_queue_time = false ``` -**Nginx:** - -```nginx -proxy_set_header X-Request-Start "t=${msec}"; -``` - -**HAProxy:** - -```haproxy -http-request set-header X-Request-Start t=%Ts%ms -``` - diff --git a/platform-includes/performance/queue-time-capture/ruby.mdx b/platform-includes/performance/queue-time-capture/ruby.mdx index 8385fa823c739..3548f29b21516 100644 --- a/platform-includes/performance/queue-time-capture/ruby.mdx +++ b/platform-includes/performance/queue-time-capture/ruby.mdx @@ -2,7 +2,7 @@ The Ruby SDK automatically captures queue time for Rack-based applications when the `X-Request-Start` header is present. This measures how long requests wait in the web server queue (e.g., waiting for a Puma thread) before your application begins processing them. -Queue time is attached to transactions as `http.queue_time_ms` and helps identify server capacity issues. +Queue time is attached to transactions as `http.server.request.time_in_queue` and helps identify server capacity issues. ### Setup @@ -33,7 +33,7 @@ The SDK: 1. Reads the `X-Request-Start` header timestamp from your reverse proxy 2. Calculates the time difference between the header timestamp and when the request reaches your application 3. Subtracts `puma.request_body_wait` (if present) to exclude time spent waiting for slow client uploads -4. Attaches the result as `http.queue_time_ms` to the transaction +4. Attaches the result as `http.server.request.time_in_queue` to the transaction ### Disable Queue Time Capture @@ -47,4 +47,4 @@ end ### Viewing Queue Time -Queue time appears in the Sentry transaction details under the "Data" section as `http.queue_time_ms` (measured in milliseconds). +Queue time appears in the Sentry transaction details under the "Data" section as `http.server.request.time_in_queue` (measured in milliseconds).