1414 * @module
1515 */
1616
17- import { createHmac , randomUUID } from "node:crypto" ;
17+ import { randomUUID } from "node:crypto" ;
1818import {
1919 mkdir ,
2020 readdir ,
@@ -26,6 +26,11 @@ import {
2626 writeFile ,
2727} from "node:fs/promises" ;
2828import { join } from "node:path" ;
29+ import {
30+ buildCacheKey as buildIdentityCacheKey ,
31+ classifyUrl ,
32+ FALLBACK_TTL_MS ,
33+ } from "@sentry/toolkit-core/response-cache" ;
2934import CachePolicy from "http-cache-semantics" ;
3035import pLimit from "p-limit" ;
3136
@@ -39,127 +44,20 @@ import { recordCacheHit, withCacheSpan } from "./telemetry.js";
3944const log = logger . withTag ( "response-cache" ) ;
4045
4146// ---------------------------------------------------------------------------
42- // TTL tiers — used as fallback when the server sends no cache headers
43- // ---------------------------------------------------------------------------
44-
45- /**
46- * TTL tier classification for URLs.
47- *
48- * - `stable`: data that changes infrequently (orgs, projects, teams,
49- * organization event details, trace-item details)
50- * - `volatile`: data that changes often (issues and their events, project
51- * event details, full traces, log and span queries)
52- * - `no-cache`: never cache (polling endpoints like autofix state)
53- *
54- * No tier treats a response as immutable. Event payloads embed mutable
55- * fields (`nextEventID`, user reports, release data), `latest`/`oldest`/
56- * `recommended` resolve to different events over time, and traces gain
57- * spans as they arrive (getsentry/sentry#128818).
58- */
59- type TtlTier = "stable" | "volatile" | "no-cache" ;
60-
61- /** Fallback TTL durations by tier (milliseconds). `no-cache` uses 0 as a sentinel. */
62- const FALLBACK_TTL_MS : Record < TtlTier , number > = {
63- stable : 5 * 60 * 1000 , // 5 minutes
64- volatile : 60 * 1000 , // 60 seconds
65- "no-cache" : 0 ,
66- } ;
67-
68- /**
69- * URL patterns grouped by TTL tier.
70- *
71- * Checked in tier priority order (no-cache → volatile).
72- * "stable" has no patterns — it is the default fallback when nothing else matches.
73- *
74- * TODO: Classify by generated operation metadata from `@sentry/api` instead
75- * of URL regexes, once it ships per-operation metadata
76- * (getsentry/sentry-api-schema#81) and a route-keyed operation map
77- * (getsentry/sentry-api-schema#102).
78- */
79- const URL_TIER_REGEXPS : Readonly < Record < TtlTier , readonly RegExp [ ] > > = {
80- // Polling endpoints where state changes rapidly
81- "no-cache" : [ / \/ (?: a u t o f i x | r o o t - c a u s e ) \/ / ] ,
82- volatile : [
83- // Issue endpoints: lists, detail views, and issue events such as
84- // `/issues/{id}/events/latest/`
85- / \/ i s s u e s \/ / ,
86- / [ ? & ] d a t a s e t = (?: l o g s | s p a n s ) / ,
87- / \/ t r a c e - l o g s \/ / ,
88- // Full traces gain spans as they arrive
89- / \/ t r a c e \/ [ 0 - 9 a - f ] { 32 } \/ / ,
90- // Project event details embed `nextEventID`/`previousEventID` and user
91- // reports, and reprocessing can change them for the same ID
92- / \/ p r o j e c t s \/ [ ^ / ] + \/ [ ^ / ] + \/ e v e n t s \/ [ ^ / ? ] + \/ ? (?: \? | $ ) / ,
93- ] ,
94- // Default fallback — no patterns needed
95- stable : [ ] ,
96- } ;
97-
98- /** Tier check order — stable is the default and has no patterns to check. */
99- const TIER_CHECK_ORDER : readonly TtlTier [ ] = [ "no-cache" , "volatile" ] ;
100-
101- /**
102- * Classify a URL into a TTL tier for fallback caching.
103- *
104- * @param url - Full URL string (with query params)
105- * @returns The TTL tier
106- * @internal Exported for testing
107- */
108- export function classifyUrl ( url : string ) : TtlTier {
109- for ( const tier of TIER_CHECK_ORDER ) {
110- for ( const pattern of URL_TIER_REGEXPS [ tier ] ) {
111- if ( pattern . test ( url ) ) {
112- return tier ;
113- }
114- }
115- }
116- return "stable" ;
117- }
118-
119- // ---------------------------------------------------------------------------
120- // Cache key generation
47+ // Cache keys — tiers, keys, and invalidation prefixes live in toolkit-core
12148// ---------------------------------------------------------------------------
12249
12350/**
124- * Build a deterministic cache key from the active identity + method + URL.
125- *
126- * Query params are sorted alphabetically so `?a=1&b=2` and `?b=2&a=1`
127- * produce the same key. The identity fingerprint scopes entries per
128- * bearer token so switching accounts never serves another user's data.
51+ * Build the cache key for the active identity + method + URL.
12952 *
13053 * @internal Exported for testing
13154 */
132- export function buildCacheKey (
55+ export async function buildCacheKey (
13356 method : string ,
13457 url : string ,
13558 identity = getIdentityFingerprint ( ) ,
136- ) : string {
137- const normalized = normalizeUrl ( method , url ) ;
138- return createHmac ( "sha256" , identity ) . update ( normalized ) . digest ( "hex" ) ;
139- }
140-
141- /**
142- * Normalize method + URL into a stable string for cache key derivation.
143- * Sorts query params alphabetically for deterministic key generation.
144- *
145- * @throws {TypeError } If the URL cannot be parsed
146- * @internal Exported for testing
147- */
148- export function normalizeUrl ( method : string , url : string ) : string {
149- const parsed = new URL ( url ) ;
150- const sortedParams = new URLSearchParams (
151- [ ...parsed . searchParams . entries ( ) ] . sort ( ( [ a ] , [ b ] ) => {
152- if ( a < b ) {
153- return - 1 ;
154- }
155- if ( a > b ) {
156- return 1 ;
157- }
158- return 0 ;
159- } ) ,
160- ) ;
161- parsed . search = sortedParams . toString ( ) ? `?${ sortedParams . toString ( ) } ` : "" ;
162- return `${ method . toUpperCase ( ) } |${ parsed . toString ( ) } ` ;
59+ ) : Promise < string > {
60+ return await buildIdentityCacheKey ( method , url , identity ) ;
16361}
16462
16563// ---------------------------------------------------------------------------
@@ -451,10 +349,10 @@ export async function getCachedResponse(
451349 let key : string ;
452350 // oxlint-disable-next-line sentry-cli/no-silent-catch -- grandfathered silent catch — see #1531; drain by adding log.debug()/log.warn() or re-throwing.
453351 try {
454- key = buildCacheKey ( method , url , identity ) ;
352+ key = await buildCacheKey ( method , url , identity ) ;
455353 } catch {
456- // Malformed URL — skip cache lookup. The request itself will surface
457- // any real URL error.
354+ // Malformed URL or empty identity — skip cache lookup. The request
355+ // itself will surface any real URL error.
458356 return ;
459357 }
460358
@@ -570,9 +468,9 @@ export async function storeCachedResponse(
570468 let key : string ;
571469 // oxlint-disable-next-line sentry-cli/no-silent-catch -- grandfathered silent catch — see #1531; drain by adding log.debug()/log.warn() or re-throwing.
572470 try {
573- key = buildCacheKey ( method , url , identity ) ;
471+ key = await buildCacheKey ( method , url , identity ) ;
574472 } catch {
575- // Malformed URL — skip caching this response
473+ // Malformed URL or empty identity — skip caching this response
576474 return ;
577475 }
578476
0 commit comments