Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ed1cb82
add last_updated information to develop docs
dingsdax Dec 1, 2025
16ad85f
fix TS null vs undefined
dingsdax Dec 1, 2025
4e916b6
Lint
dingsdax Dec 1, 2025
2fa50bb
Merge branch 'master' into develop_doc_last_updated
dingsdax Dec 1, 2025
928a878
[getsentry/action-github-commit] Auto commit
getsantry[bot] Dec 1, 2025
60325f6
[getsentry/action-github-commit] Auto commit
getsantry[bot] Dec 1, 2025
2261774
Fix for only showing on develop docs
dingsdax Dec 1, 2025
353c895
[getsentry/action-github-commit] Auto commit
getsantry[bot] Dec 1, 2025
f387289
return new object copies, preventing reference sharing in cached meta…
dingsdax Dec 1, 2025
df13418
[getsentry/action-github-commit] Auto commit
getsantry[bot] Dec 1, 2025
af1d879
Add some debug code
dingsdax Dec 2, 2025
6733525
Update getGitMetadata.ts
dingsdax Dec 2, 2025
dbee391
Capture queue time docs
dingsdax Jan 15, 2026
8a1891a
Merge branch 'master' into queue-time-metric
dingsdax Jan 15, 2026
340aa90
[getsentry/action-github-commit] Auto commit
getsantry[bot] Jan 15, 2026
d421c30
[getsentry/action-github-commit] Auto commit
getsantry[bot] Jan 15, 2026
72c0502
remove unrelated code
dingsdax Jan 16, 2026
de260a4
Merge branch 'master' into queue-time-metric
dingsdax Jan 16, 2026
9dec483
Apply suggestions from code review
dingsdax Jan 23, 2026
f7ddb6f
Fix attribute name, simplify options
dingsdax Jan 23, 2026
7381a33
Merge branch 'master' into queue-time-metric
dingsdax Feb 24, 2026
1d55734
Merge branch 'master' into queue-time-metric
dingsdax Feb 24, 2026
100bfc5
docs(ruby): Review and fix queue time capture docs
dingsdax Feb 24, 2026
5894dc4
fix(ruby): Use PlatformLink for queue time capture reference in options
dingsdax Feb 24, 2026
faf703b
Merge branch 'master' into queue-time-metric
dingsdax Apr 2, 2026
d26bcc0
Merge branch 'master' into queue-time-metric
dingsdax Apr 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion app/[[...path]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,14 +131,29 @@ 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 (
<Fragment>
<PageLoadMetrics pageType={pageType} attributes={{is_developer_docs: true}} />
<MDXLayoutRenderer
mdxSource={mdxSource}
frontMatter={frontMatter}
frontMatter={frontMatterWithGit}
nextPage={nextPage}
previousPage={previousPage}
/>
Expand Down
26 changes: 26 additions & 0 deletions docs/platforms/ruby/common/configuration/options.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,32 @@ config.trace_ignore_status_codes = [404, (502..511)]

</SdkOption>

<SdkOption name="capture_queue_time" type="Boolean" defaultValue="true">

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.
Comment thread
dingsdax marked this conversation as resolved.
Outdated

To disable queue time capture:

```ruby
config.capture_queue_time = false
```

**Nginx:**
Comment thread
dingsdax marked this conversation as resolved.
Outdated

```nginx
proxy_set_header X-Request-Start "t=${msec}";
```

**HAProxy:**

```haproxy
http-request set-header X-Request-Start t=%Ts%ms
```

</SdkOption>

<SdkOption name="instrumenter" type="Symbol" defaultValue=":sentry">

The instrumenter to use, `:sentry` or `:otel` for [use with OpenTelemetry](../../tracing/instrumentation/opentelemetry).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <PlatformLink to="/tracing/instrumentation/custom-instrumentation/">create transactions manually</PlatformLink>.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Sentry supports adding arbitrary custom units, but we recommend using one of the

<Include name="custom-measurements-units-disclaimer.mdx" />

<PlatformContent includePath="performance/queue-time-capture" />

## 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`.
Expand Down
50 changes: 50 additions & 0 deletions platform-includes/performance/queue-time-capture/ruby.mdx
Original file line number Diff line number Diff line change
@@ -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).
5 changes: 5 additions & 0 deletions src/components/docPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -94,6 +95,10 @@ export function DocPage({
<div>
<hgroup>
<h1>{frontMatter.title}</h1>
{/* Show last updated info for develop-docs pages */}
{frontMatter.gitMetadata && (
<LastUpdated gitMetadata={frontMatter.gitMetadata} />
)}
<h2>{frontMatter.description}</h2>
</hgroup>
{/* This exact id is important for Algolia indexing */}
Expand Down
101 changes: 101 additions & 0 deletions src/components/lastUpdated/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
'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 (
<div className="flex flex-wrap items-center gap-1 text-xs text-[var(--foreground-secondary)] mt-1 mb-4">
{/* Text content */}
<span className="flex flex-wrap items-center gap-1">
<span>updated by</span>
<span className="font-medium">{author}</span>
{/* Relative time with tooltip */}
<span title={fullDate} className="cursor-help">
{relativeTime}
</span>
</span>

{/* Commit link */}
<span className="flex items-center gap-1">
<span className="text-[var(--foreground-secondary)]">•</span>
<Link
href={commitUrl}
target="_blank"
rel="noopener noreferrer"
className="hover:text-[var(--accent-purple)] underline"
>
#{abbreviatedHash}
</Link>
</span>
</div>
);
}
23 changes: 22 additions & 1 deletion src/mdx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,10 +268,31 @@

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/')
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
) {
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);

Check warning on line 287 in src/mdx.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected console statement
}
}

return {
...(frontmatter as FrontMatter),
slug: fileName.replace(/\/index.mdx?$/, '').replace(/\.mdx?$/, ''),
sourcePath: path.join(folder, fileName),
sourcePath,
gitMetadata,
};
},
{concurrency: FILE_CONCURRENCY_LIMIT}
Expand Down
11 changes: 10 additions & 1 deletion src/types/frontmatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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.
*/
Expand Down
70 changes: 70 additions & 0 deletions src/utils/getGitMetadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
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<string, GitMetadata | null>();

/**
* 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}`);

Check warning on line 36 in src/utils/getGitMetadata.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected console statement
}

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);
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated

// 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;
}
}
Loading