Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
2c0d424
Basic tracing hooks
Fryuni May 12, 2025
ce2de17
Merge branch 'main' into feat/tracing-hooks
Fryuni May 12, 2025
15fe631
Add extra trace points, data and make tracing platform independent
Fryuni May 12, 2025
f7ed4da
Run formatter
Fryuni May 12, 2025
741be1e
Rename trace event
Fryuni May 12, 2025
b1833ed
Merge branch 'main' into fryuni/tracing-hooks
Fryuni May 23, 2025
cb8fdcb
Merge branch 'main' into fryuni/tracing-hooks
Fryuni May 31, 2025
f116c3f
Merge remote-tracking branch 'origin/main' into fryuni/tracing-hooks
Fryuni Jul 21, 2025
ebac4ec
format
Fryuni Jul 21, 2025
e32b685
Merge remote-tracking branch 'origin/main' into fryuni/tracing-hooks
Fryuni Jul 21, 2025
628fcf1
Merge remote-tracking branch 'origin/main' into fryuni/tracing-hooks
Fryuni Aug 1, 2025
c46fa13
Make type future-proof
Fryuni Aug 2, 2025
2e1caaa
Add hook for async operation completion
Fryuni Aug 2, 2025
17d26db
Add function for integrations to add early initialization logic
Fryuni Aug 3, 2025
d4e29bc
Working exemple of tracing using OpenTelemetry
Fryuni Aug 3, 2025
cfe14a0
wip changeset for CI
Fryuni Aug 9, 2025
0eae15d
Merge branch 'main' into fryuni/tracing-hooks
Fryuni Aug 9, 2025
0cbc2d9
Preview unreleased package
Fryuni Aug 9, 2025
dc3a5f8
Refactor OpenTelemetry integration to be configurable and adaptable
Fryuni Aug 9, 2025
cfbe4bc
Add tracing to middlewares
Fryuni Aug 10, 2025
65601f7
Make logger export to console by default
Fryuni Aug 10, 2025
6979108
Allow integration to be wrapped
Fryuni Aug 10, 2025
c8f7291
Implement helper imports for each telemetry resource
Fryuni Aug 10, 2025
d804ded
Fix package.json
Fryuni Aug 11, 2025
aca5126
Merge remote-tracking branch 'origin/main' into fryuni/tracing-hooks
Fryuni Aug 11, 2025
06b4a5c
Format
Fryuni Aug 11, 2025
ddb6232
WIP
Fryuni Aug 13, 2025
2c970bc
Merge branch 'main' into fryuni/tracing-hooks
Fryuni Aug 15, 2025
fc5a126
Setup telemetry and initialization for dev server
Fryuni Aug 15, 2025
04d8081
API bash instructions
Fryuni Aug 15, 2025
202fc6f
WIP
Fryuni Aug 15, 2025
effa5e3
WIP
Fryuni Aug 15, 2025
3c624ac
Improve example
Fryuni Aug 24, 2025
b43d090
Merge remote-tracking branch 'origin/main' into fryuni/tracing-hooks
Fryuni Aug 29, 2025
b74a37a
Update packages/integrations/opentelemetry/src/initialization/dev.ts
Fryuni Sep 4, 2025
9410c07
Simplify tracing hooks and prevent consumers from breaking internal code
Fryuni Aug 29, 2025
4189a6d
WIP tests
Fryuni Aug 29, 2025
45da97e
Merge branch 'main' into fryuni/tracing-hooks
Fryuni Sep 10, 2025
0b62164
Put tracing behind an experimental flag
Fryuni Sep 10, 2025
268c593
Dedupe deps
Fryuni Sep 10, 2025
684dc12
Fix default env values for Node telemetry
Fryuni Sep 10, 2025
7042a1e
Docs and unit tests
Fryuni Sep 11, 2025
069ec6b
Adjust integration tests
Fryuni Sep 11, 2025
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
1 change: 0 additions & 1 deletion examples/with-telemetry/astro.config.mts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { fileURLToPath } from 'node:url';
import node from '@astrojs/node';
import opentelemetry from '@astrojs/opentelemetry';
import svelte from '@astrojs/svelte';
Expand Down
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,13 @@
]
},
"onlyBuiltDependencies": [
"esbuild",
"workerd",
"@biomejs/biome",
"sharp"
"@tailwindcss/oxide",
"esbuild",
"oxc-resolver",
"protobufjs",
"sharp",
"workerd"
]
}
}
2 changes: 2 additions & 0 deletions packages/astro/src/container/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ function createManifest(
middleware: manifest?.middleware ?? middlewareInstance,
key: createKey(),
csp: manifest?.csp,
enableTracing: manifest?.enableTracing ?? false,
};
}

Expand Down Expand Up @@ -248,6 +249,7 @@ type AstroContainerManifest = Pick<
| 'outDir'
| 'cacheDir'
| 'csp'
| 'enableTracing'
>;

type AstroContainerConstructor = {
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/core/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export type SSRManifest = {
buildClientDir: string | URL;
buildServerDir: string | URL;
csp: SSRManifestCSP | undefined;
enableTracing: boolean;
};

export type SSRActions = {
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -742,5 +742,6 @@ async function createBuildManifest(
(settings.config.security?.checkOrigin && settings.buildOutput === 'server') ?? false,
key,
csp,
enableTracing: settings.config.experimental.enableTracing,
};
}
1 change: 1 addition & 0 deletions packages/astro/src/core/build/plugins/plugin-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,5 +371,6 @@ async function buildManifest(
key: encodedKey,
sessionConfig: settings.config.session,
csp,
enableTracing: settings.config.experimental.enableTracing,
};
}
5 changes: 5 additions & 0 deletions packages/astro/src/core/config/schemas/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export const ASTRO_CONFIG_DEFAULTS = {
csp: false,
staticImportMetaEnv: false,
chromeDevtoolsWorkspace: false,
enableTracing: false,
},
} satisfies AstroUserConfig & { server: { open: boolean } };

Expand Down Expand Up @@ -510,6 +511,10 @@ export const AstroConfigSchema = z.object({
.boolean()
.optional()
.default(ASTRO_CONFIG_DEFAULTS.experimental.chromeDevtoolsWorkspace),
enableTracing: z
.boolean()
.optional()
.default(ASTRO_CONFIG_DEFAULTS.experimental.enableTracing),
})
.strict(
`Invalid or outdated experimental feature.\nCheck for incorrect spelling or outdated Astro version.\nSee https://docs.astro.build/en/reference/experimental-flags/ for a list of all current experiments.`,
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/manifest/virtual-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ function serializeServerConfig(manifest: SSRManifest): string {
trailingSlash: manifest.trailingSlash,
site: manifest.site,
compressHTML: manifest.compressHTML,
enableTracing: manifest.enableTracing,
};
const output = [];
for (const [key, value] of Object.entries(serverConfig)) {
Expand Down
173 changes: 67 additions & 106 deletions packages/astro/src/runtime/server/tracing.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,11 @@
import type {
TraceEvent,
TraceEventsPayloads,
TraceListener,
TraceWrapListener,
} from '../../types/public/tracing.js';
import type { ServerDeserializedManifest } from '../../types/public/index.js';
import type { TraceEvent, TraceEventsPayloads, TraceListener } from '../../types/public/tracing.js';

export type { TraceEvent, TraceEventsPayloads, TraceListener, TraceWrapListener };
export type { TraceEvent, TraceEventsPayloads, TraceListener };

type OperationLifecycle = 'before' | 'onComplete' | 'after';

const eventLifecycle: Record<OperationLifecycle, TraceListener[]> = {
before: [],
onComplete: [],
after: [],
};

function onTrace(lifecycle: OperationLifecycle, listener: TraceListener, signal?: AbortSignal) {
const wrapped: TraceListener = (...args) => {
try {
const res: unknown = listener(...args);
// Attach an error handler to avoid unhandled promise rejections
if (res instanceof Promise) res.catch(console.error);
} catch (error) {
console.error(error);
}
};

const listeners = eventLifecycle[lifecycle];
const listeners: TraceListener[] = [];

export function onTraceEvent(listener: TraceListener, signal?: AbortSignal) {
if (signal) {
if (signal.aborted) {
// The signal is already aborted, the listener should never be called.
Expand All @@ -37,61 +15,76 @@ function onTrace(lifecycle: OperationLifecycle, listener: TraceListener, signal?
return;
}
signal.addEventListener('abort', () => {
listeners.splice(listeners.indexOf(wrapped), 1);
listeners.splice(listeners.indexOf(listener), 1);
});
}

listeners.push(wrapped);
}

/**
* @experimental
*/
export function onBeforeTrace(listener: TraceListener, signal?: AbortSignal) {
onTrace('before', listener, signal);
}

/**
* @experimental
*/
export function onCompleteTrace(listener: TraceListener, signal?: AbortSignal) {
onTrace('onComplete', listener, signal);
listeners.push(listener);
}

/**
* @experimental
* A wrapper to call listeners in sequence, ensuring that each listener is
* called once and only once, even if some of them don't call the `next` callback
* or call it multiple times.
*
* This ensures that the presence of tracing listeners cannot interfere with
* other tracing listeners or the function being traced.
*/
export function onAfterTrace(listener: TraceListener, signal?: AbortSignal) {
onTrace('after', listener, signal);
}

const wrapListeners: TraceWrapListener[] = [];
function sequenceListeners<T>(event: TraceEvent, fn: () => T, index = 0): T {
if (index >= listeners.length) {
return fn();
}

export function onTraceEvent(listener: TraceWrapListener, signal?: AbortSignal) {
if (signal) {
if (signal.aborted) {
// The signal is already aborted, the listener should never be called.
// Returning early avoids both possible scenarios:
// - The `abort` event is being processed and the listener would be removed depending on a race condition.
// - The `abort` signal was already processed and the listener will never be removed, triggering after the signal is aborted.
return;
const listener = listeners[index];

let state: 'pending' | 'called' | 'failed' = 'pending';
let resultValue: T;
let errorValue: unknown;
// Wrapper to ensure the callback is only called once
// but that always yields the same effect.
const next = () => {
switch (state) {
case 'pending':
try {
resultValue = sequenceListeners(event, fn, index + 1);
} catch (e) {
state = 'failed';
errorValue = e;
throw e;
}
state = 'called';
case 'called':
return resultValue!;
case 'failed':
throw errorValue;
}
signal.addEventListener('abort', () => {
wrapListeners.splice(wrapListeners.indexOf(listener), 1);
};

try {
listener(event, () => {
const result = next();
return result instanceof Promise
? // Return a promise that always resolve to void, but only once resultValue resolves.
// This allow tracing listeners to await the completion of the inner function without
// without having access to any internal values.
result.then<void>(() => {})
: undefined;
});
} catch {
// Ignore errors in listeners to avoid breaking the main flow.
}

wrapListeners.push(listener);
return next();
}

function wrapCall<T>(event: TraceEvent, fn: () => T, index = 0): T {
if (index >= wrapListeners.length) {
return fn();
}

const listener = wrapListeners[index];
return listener(event, () => wrapCall(event, fn, index + 1));
}
// TODO: Figure out why this module is being reported as unknown
const tracingEnabled = await import('astro:config/server' as any)
.then((m: ServerDeserializedManifest) => m.enableTracing)
// Tracing enabled in case of import errors to allow testing and
// dev environments outside of Vite.
// Once the feature is stabilized this flag wouldn't be needed since tracing
// always be enabled (disabling by not having listeners instead of a config flag).
.catch(() => true);

export function wrapWithTracing<
This,
Expand All @@ -103,13 +96,12 @@ export function wrapWithTracing<
fn: (this: This, ...args: Args) => Return,
payload: TraceEventsPayloads[Event] | ((this: This, ...args: Args) => TraceEventsPayloads[Event]),
): (this: This, ...args: Args) => Return {
if (!tracingEnabled) {
return fn;
}

return function (this: This, ...args: Args): Return {
if (
eventLifecycle.before.length === 0 &&
eventLifecycle.onComplete.length === 0 &&
eventLifecycle.after.length === 0 &&
wrapListeners.length === 0
) {
if (listeners.length === 0) {
// Avoid constructing payloads and emitting events if no listeners are attached
return fn.apply(this, args);
}
Expand All @@ -119,37 +111,6 @@ export function wrapWithTracing<
payload: typeof payload === 'function' ? payload.apply(this, args) : payload,
} as TraceEvent;

for (const listener of eventLifecycle.before) {
listener(eventArgs);
}

let result =
wrapListeners.length === 0
? fn.apply(this, args)
: wrapCall(eventArgs, () => fn.apply(this, args));

if (result instanceof Promise) {
if (eventLifecycle.onComplete.length > 0) {
// Only attach a `finally` handler if there are onComplete listeners.
// This avoids unnecessary entries on the event loop when tracing implementations don't use the `onComplete` hook.
result = result.finally(() => {
// Call hook after the async operation completes
for (const listener of eventLifecycle.onComplete) {
listener(eventArgs);
}
}) as /* Safe to cast because Promise.finally doesn't change the resolved or thrown value. */ Return;
}
} else {
// Operation was synchronous, call onComplete listeners immediately
for (const listener of eventLifecycle.onComplete) {
listener(eventArgs);
}
}

for (const listener of eventLifecycle.after) {
listener(eventArgs);
}

return result;
return sequenceListeners(eventArgs, () => fn.apply(this, args));
};
}
16 changes: 16 additions & 0 deletions packages/astro/src/types/public/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2483,6 +2483,22 @@ export interface ViteUserConfig extends OriginalViteUserConfig {
* See the [experimental Chrome DevTools workspace feature documentation](https://docs.astro.build/en/reference/experimental-flags/chrome-devtools-workspace/) for more information.
*/
chromeDevtoolsWorkspace?: boolean;

/**
* @name experimental.enableTracing
* @type {boolean}
* @default `false`
* @version 5.14
* @description
*
* Enables tracing functionality for Astro internals.
*
* This allows users and integrations to subscribe to events describing internal
* Astro processes during rendering or build.
*
* Without this flag, no tracing events will be emitted even if subscribers are registered.
*/
enableTracing?: boolean;
};
}

Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/types/public/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type DeserializedDirs = Extend<Dirs, URL>;

export type ServerDeserializedManifest = Pick<
SSRManifest,
'base' | 'trailingSlash' | 'compressHTML' | 'site'
'base' | 'trailingSlash' | 'compressHTML' | 'site' | 'enableTracing'
> &
DeserializedDirs & {
i18n: AstroConfig['i18n'];
Expand Down
3 changes: 1 addition & 2 deletions packages/astro/src/types/public/tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,4 @@ export type TraceEvent = {
};
}[keyof TraceEventsPayloads];

export type TraceListener = (event: TraceEvent) => void;
export type TraceWrapListener = <T>(event: TraceEvent, callback: () => T) => T;
export type TraceListener = (event: TraceEvent, callback: () => void | Promise<void>) => void;
1 change: 1 addition & 0 deletions packages/astro/src/vite-plugin-astro-server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,5 +285,6 @@ export function createDevelopmentManifest(settings: AstroSettings): SSRManifest
},
sessionConfig: settings.config.session,
csp,
enableTracing: settings.config.experimental.enableTracing ?? false,
};
}
12 changes: 12 additions & 0 deletions packages/astro/test/fixtures/tracing-integration/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineConfig } from 'astro/config';

export default defineConfig({
// Enable tracing and middleware for testing
experimental: {
// Any experimental features needed for tracing
},
// Configure for testing environment
server: {
port: 4321,
},
});
8 changes: 8 additions & 0 deletions packages/astro/test/fixtures/tracing-integration/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@test/tracing-integration",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*"
}
}
Loading
Loading