All files / src/lib/db issue-org-cache.ts

100% Statements 17/17
100% Branches 8/8
100% Functions 4/4
100% Lines 17/17

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110                                                                                      32x 1x 1x   31x 31x       31x 31x                           23x 2x   21x 21x                                         4x 1x   3x 3x                   271x 271x    
/**
 * Cache for numeric-issue-ID → organization-slug mappings.
 *
 * When a user runs `sentry issue view 123456789` without an org context,
 * the CLI must fall back to the legacy unscoped `GET /api/0/issues/{id}/`
 * endpoint (which does not support region routing) and then extract the
 * org from the response `permalink`. Follow-up fetches (events/latest,
 * trace/...) require the org slug, so without a cache every subsequent
 * command run repeats the same unscoped lookup.
 *
 * This module records the resolved numeric-id → org-slug mapping so
 * future runs can skip straight to the org-scoped endpoint. It addresses
 * the `sentry.issue.view` "Consecutive HTTP" pattern for Pattern D in
 * the issue triage (numeric-ID org discovery fan-out).
 *
 * Storage: dedicated `issue_org_cache` SQLite table (schema v15). Entries
 * are best-effort — a stale mapping (issue deleted, access revoked, or
 * moved) causes a single 404 on the cached org call which the caller
 * falls back from and evicts the entry. Cleared on logout since
 * mappings are scoped to the authenticated user's permissions.
 *
 * Values are not TTL'd because issues are owned by a single org for
 * their entire lifetime — the mapping cannot change except by issue
 * deletion, which we already handle via 404 eviction.
 */
 
import { recordCacheHit } from "../telemetry.js";
import { getDatabase } from "./index.js";
import { runUpsert } from "./utils.js";
 
type IssueOrgRow = {
  issue_id: string;
  org_slug: string;
  cached_at: number;
};
 
/**
 * Look up the cached organization slug for a numeric issue ID.
 *
 * @param numericId - Numeric issue group ID (e.g., "7413562541")
 * @returns Org slug if cached, undefined otherwise
 */
export function getCachedIssueOrg(numericId: string): string | undefined {
  if (!numericId) {
    recordCacheHit("issue_org", false);
    return;
  }
  const db = getDatabase();
  const row = db
    .query("SELECT org_slug FROM issue_org_cache WHERE issue_id = ?")
    .get(numericId) as Pick<IssueOrgRow, "org_slug"> | undefined;
 
  recordCacheHit("issue_org", !!row);
  return row?.org_slug;
}
 
/**
 * Remember the organization slug for a numeric issue ID.
 *
 * Silently no-ops when either argument is empty. Best-effort — callers
 * should not await this as a critical step; the DB layer already wraps
 * writes to be fault-tolerant.
 *
 * @param numericId - Numeric issue group ID (e.g., "7413562541")
 * @param orgSlug - Organization slug that owns the issue
 */
export function setCachedIssueOrg(numericId: string, orgSlug: string): void {
  if (!(numericId && orgSlug)) {
    return;
  }
  const db = getDatabase();
  runUpsert(
    db,
    "issue_org_cache",
    {
      issue_id: numericId,
      org_slug: orgSlug,
      cached_at: Date.now(),
    },
    ["issue_id"]
  );
}
 
/**
 * Drop the cached mapping for a numeric issue ID.
 *
 * Called when an org-scoped fetch 404s so subsequent runs re-resolve
 * the org via the legacy unscoped endpoint.
 *
 * @param numericId - Numeric issue group ID
 */
export function clearCachedIssueOrg(numericId: string): void {
  if (!numericId) {
    return;
  }
  const db = getDatabase();
  db.query("DELETE FROM issue_org_cache WHERE issue_id = ?").run(numericId);
}
 
/**
 * Drop ALL issue-id → org mappings.
 *
 * Called from auth logout handlers so signing out with one account does
 * not leak mappings into a different account's session.
 */
export function clearAllIssueOrgCache(): void {
  const db = getDatabase();
  db.query("DELETE FROM issue_org_cache").run();
}