Skip to content

Commit 7eabcd6

Browse files
committed
fix(telemetry): centralize sentry.org/project tags in resolution functions
Move setOrgProjectContext() from individual command files into the shared resolution functions in resolve-target.ts, trace-target.ts, and dashboard/resolve.ts. This ensures every command that resolves an org or project automatically gets sentry.org and sentry.project telemetry tags, eliminating the class of bugs where a command forgets to call setContext(). Previously ~15 commands manually called this.setContext() after resolution, while ~15 others (all dashboard/*, event/view, project/*, trial/*, org/view) forgot — causing missing telemetry tags on error events. Changes: - Add setOrgProjectContext calls to 6 functions in resolve-target.ts - Add setOrgProjectContext calls to 2 functions in trace-target.ts - Add setOrgProjectContext call to resolveOrgFromTarget in dashboard/resolve.ts - Remove setContext from SentryContext interface and buildContext - Remove all manual setContext calls from 12 command files - Update 35 test files to remove setContext from mock contexts - Delete 7 test blocks that asserted setContext was called
1 parent 48a9a8f commit 7eabcd6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+128
-357
lines changed

AGENTS.md

Lines changed: 10 additions & 72 deletions
Large diffs are not rendered by default.

src/commands/dashboard/resolve.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { listDashboards } from "../../lib/api-client.js";
99
import type { parseOrgProjectArg } from "../../lib/arg-parsing.js";
1010
import { ContextError, ValidationError } from "../../lib/errors.js";
1111
import { resolveOrg } from "../../lib/resolve-target.js";
12+
import { setOrgProjectContext } from "../../lib/telemetry.js";
1213
import { isAllDigits } from "../../lib/utils.js";
1314
import {
1415
type DashboardWidget,
@@ -52,9 +53,11 @@ export async function resolveOrgFromTarget(
5253
switch (parsed.type) {
5354
case "explicit":
5455
case "org-all":
56+
setOrgProjectContext([parsed.org], []);
5557
return parsed.org;
5658
case "project-search":
5759
case "auto-detect": {
60+
// resolveOrg already sets telemetry context
5861
const resolved = await resolveOrg({ cwd });
5962
if (!resolved) {
6063
throw new ContextError("Organization", usageHint);

src/commands/issue/explain.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export const explainCommand = buildCommand({
7474
},
7575
async *func(this: SentryContext, flags: ExplainFlags, issueArg: string) {
7676
applyFreshFlag(flags);
77-
const { cwd, setContext } = this;
77+
const { cwd } = this;
7878

7979
// Declare org outside try block so it's accessible in catch for error messages
8080
let resolvedOrg: string | undefined;
@@ -88,9 +88,6 @@ export const explainCommand = buildCommand({
8888
});
8989
resolvedOrg = org;
9090

91-
// Set telemetry context so SeerError events carry the org tag
92-
setContext([org], []);
93-
9491
// Ensure root cause analysis exists (triggers if needed)
9592
const state = await ensureRootCauseAnalysis({
9693
org,

src/commands/issue/list.ts

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ import {
7474
toNumericId,
7575
} from "../../lib/resolve-target.js";
7676
import { getApiBaseUrl } from "../../lib/sentry-client.js";
77+
import { setOrgProjectContext } from "../../lib/telemetry.js";
7778
import type {
7879
ProjectAliasEntry,
7980
SentryIssue,
@@ -349,6 +350,7 @@ async function resolveTargetsFromParsedArg(
349350

350351
case "explicit": {
351352
// Single explicit target — fetch project ID for API query param
353+
setOrgProjectContext([parsed.org], [parsed.project]);
352354
const projectId = await fetchProjectId(parsed.org, parsed.project);
353355
return {
354356
targets: [
@@ -365,6 +367,7 @@ async function resolveTargetsFromParsedArg(
365367

366368
case "org-all": {
367369
// List all projects in the specified org
370+
setOrgProjectContext([parsed.org], []);
368371
const projects = await listProjects(parsed.org);
369372
const targets: ResolvedTarget[] = projects.map((p) => ({
370373
org: parsed.org,
@@ -460,6 +463,10 @@ async function resolveTargetsFromParsedArg(
460463
projectDisplay: m.name,
461464
}));
462465

466+
const uniqueOrgs = [...new Set(targets.map((t) => t.org))];
467+
const uniqueProjects = [...new Set(targets.map((t) => t.project))];
468+
setOrgProjectContext(uniqueOrgs, uniqueProjects);
469+
463470
return {
464471
targets,
465472
footer:
@@ -819,7 +826,6 @@ async function fetchOrgAllIssues(
819826
type OrgAllIssuesOptions = {
820827
org: string;
821828
flags: ListFlags;
822-
setContext: (orgs: string[], projects: string[]) => void;
823829
};
824830

825831
/**
@@ -832,7 +838,7 @@ type OrgAllIssuesOptions = {
832838
async function handleOrgAllIssues(
833839
options: OrgAllIssuesOptions
834840
): Promise<IssueListResult> {
835-
const { org, flags, setContext } = options;
841+
const { org, flags } = options;
836842
// Encode sort + query in context key so cursors from different searches don't collide.
837843
const contextKey = buildPaginationContextKey("org", org, {
838844
sort: flags.sort,
@@ -841,8 +847,6 @@ async function handleOrgAllIssues(
841847
});
842848
const cursor = resolveOrgCursor(flags.cursor, PAGINATION_KEY, contextKey);
843849

844-
setContext([org], []);
845-
846850
let issuesResult: IssuesPage;
847851
try {
848852
issuesResult = await withProgress(
@@ -915,7 +919,6 @@ type ResolvedTargetsOptions = {
915919
parsed: ReturnType<typeof parseOrgProjectArg>;
916920
flags: ListFlags;
917921
cwd: string;
918-
setContext: (orgs: string[], projects: string[]) => void;
919922
};
920923

921924
/** Default --period value (used to detect user-implicit vs explicit). */
@@ -1049,15 +1052,11 @@ function build403Detail(originalDetail: string | undefined): string {
10491052
async function handleResolvedTargets(
10501053
options: ResolvedTargetsOptions
10511054
): Promise<IssueListResult> {
1052-
const { parsed, flags, cwd, setContext } = options;
1055+
const { parsed, flags, cwd } = options;
10531056

10541057
const { targets, footer, skippedSelfHosted, detectedDsns } =
10551058
await resolveTargetsFromParsedArg(parsed, cwd);
10561059

1057-
const orgs = [...new Set(targets.map((t) => t.org))];
1058-
const projects = [...new Set(targets.map((t) => t.project))];
1059-
setContext(orgs, projects);
1060-
10611060
if (targets.length === 0) {
10621061
if (skippedSelfHosted) {
10631062
throw new ContextError("Organization and project", USAGE_HINT, [
@@ -1479,7 +1478,7 @@ export const listCommand = buildListCommand("issue", {
14791478
},
14801479
},
14811480
async *func(this: SentryContext, flags: ListFlags, target?: string) {
1482-
const { cwd, setContext } = this;
1481+
const { cwd } = this;
14831482

14841483
const parsed = parseOrgProjectArg(target);
14851484

@@ -1502,7 +1501,6 @@ export const listCommand = buildListCommand("issue", {
15021501
handleResolvedTargets({
15031502
...ctx,
15041503
flags,
1505-
setContext,
15061504
});
15071505

15081506
const result = (await dispatchOrgScopedList({
@@ -1521,7 +1519,6 @@ export const listCommand = buildListCommand("issue", {
15211519
handleOrgAllIssues({
15221520
org: ctx.parsed.org,
15231521
flags,
1524-
setContext,
15251522
}),
15261523
},
15271524
})) as IssueListResult;

src/commands/issue/plan.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ export const planCommand = buildCommand({
193193
},
194194
async *func(this: SentryContext, flags: PlanFlags, issueArg: string) {
195195
applyFreshFlag(flags);
196-
const { cwd, setContext } = this;
196+
const { cwd } = this;
197197

198198
// Declare org outside try block so it's accessible in catch for error messages
199199
let resolvedOrg: string | undefined;
@@ -207,9 +207,6 @@ export const planCommand = buildCommand({
207207
});
208208
resolvedOrg = org;
209209

210-
// Set telemetry context so SeerError events carry the org tag
211-
setContext([org], []);
212-
213210
// Ensure root cause analysis exists (runs explain if needed)
214211
const state = await ensureRootCauseAnalysis({
215212
org,

src/commands/issue/utils.ts

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
} from "../../lib/resolve-target.js";
4141
import { parseSentryUrl } from "../../lib/sentry-url-parser.js";
4242
import { buildIssueUrl } from "../../lib/sentry-urls.js";
43+
import { setOrgProjectContext } from "../../lib/telemetry.js";
4344
import { isAllDigits } from "../../lib/utils.js";
4445
import type { SentryIssue } from "../../types/index.js";
4546
import { type AutofixState, isTerminalStatus } from "../../types/seer.js";
@@ -530,24 +531,28 @@ export async function resolveIssue(
530531
const parsed = parseIssueArg(issueArg);
531532
const commandHint = buildCommandHint(command, issueArg);
532533

534+
let result: ResolvedIssueResult;
535+
533536
switch (parsed.type) {
534537
case "numeric":
535-
return resolveNumericIssue(parsed.id, cwd, command);
538+
result = await resolveNumericIssue(parsed.id, cwd, command);
539+
break;
536540

537541
case "explicit": {
538542
// Full context: org + project + suffix
539543
const org = await resolveEffectiveOrg(parsed.org);
540544
const fullShortId = expandToFullShortId(parsed.suffix, parsed.project);
541545
const issue = await getIssueByShortId(org, fullShortId);
542-
return { org, issue };
546+
result = { org, issue };
547+
break;
543548
}
544549

545550
case "explicit-org-numeric": {
546551
// Org + numeric ID — use org-scoped endpoint for proper region routing.
547552
const org = await resolveEffectiveOrg(parsed.org);
548553
try {
549554
const issue = await getIssueInOrg(org, parsed.numericId);
550-
return { org, issue };
555+
result = { org, issue };
551556
} catch (err) {
552557
if (err instanceof ApiError && err.status === 404) {
553558
throw new ResolutionError(
@@ -562,30 +567,40 @@ export async function resolveIssue(
562567
}
563568
throw err;
564569
}
570+
break;
565571
}
566572

567573
case "explicit-org-suffix": {
568574
// Org + suffix only - ambiguous without project, always errors
569575
const org = await resolveEffectiveOrg(parsed.org);
570-
return resolveExplicitOrgSuffix(org, parsed.suffix, commandHint);
576+
result = await resolveExplicitOrgSuffix(org, parsed.suffix, commandHint);
577+
break;
571578
}
572579

573580
case "project-search":
574581
// Project slug + suffix - search across orgs
575-
return resolveProjectSearch(
582+
result = await resolveProjectSearch(
576583
parsed.projectSlug,
577584
parsed.suffix,
578585
cwd,
579586
commandHint
580587
);
588+
break;
581589

582590
case "suffix-only":
583591
// Just suffix - need DSN for org and project
584-
return resolveSuffixOnly(parsed.suffix, cwd, commandHint);
592+
result = await resolveSuffixOnly(parsed.suffix, cwd, commandHint);
593+
break;
585594

586595
case "selector":
587596
// Magic @ selector - fetch top issue by sort criteria
588-
return resolveSelector(parsed.selector, parsed.org, cwd, commandHint);
597+
result = await resolveSelector(
598+
parsed.selector,
599+
parsed.org,
600+
cwd,
601+
commandHint
602+
);
603+
break;
589604

590605
default: {
591606
// Exhaustive check - this should never be reached
@@ -595,6 +610,16 @@ export async function resolveIssue(
595610
);
596611
}
597612
}
613+
614+
// Set telemetry context from the resolved result
615+
if (result.org) {
616+
setOrgProjectContext(
617+
[result.org],
618+
result.issue.project?.slug ? [result.issue.project.slug] : []
619+
);
620+
}
621+
622+
return result;
598623
}
599624

600625
/**

src/commands/issue/view.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ export const viewCommand = buildCommand({
146146
},
147147
async *func(this: SentryContext, flags: ViewFlags, issueArg: string) {
148148
applyFreshFlag(flags);
149-
const { cwd, setContext } = this;
149+
const { cwd } = this;
150150

151151
// Resolve issue using shared resolution logic
152152
const { org: orgSlug, issue } = await resolveIssue({
@@ -155,12 +155,6 @@ export const viewCommand = buildCommand({
155155
command: "view",
156156
});
157157

158-
// Set telemetry context
159-
setContext(
160-
orgSlug ? [orgSlug] : [],
161-
issue.project?.slug ? [issue.project.slug] : []
162-
);
163-
164158
if (flags.web) {
165159
await openInBrowser(issue.permalink, "issue");
166160
return;

src/commands/log/list.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -645,7 +645,7 @@ export const listCommand = buildListCommand(
645645
},
646646
},
647647
async *func(this: SentryContext, flags: ListFlags, ...args: string[]) {
648-
const { cwd, setContext } = this;
648+
const { cwd } = this;
649649

650650
const parsed = parseLogListArgs(args);
651651

@@ -657,8 +657,6 @@ export const listCommand = buildListCommand(
657657
cwd,
658658
TRACE_USAGE_HINT
659659
);
660-
setContext([org], []);
661-
662660
if (flags.follow) {
663661
// Banner (suppressed in JSON mode)
664662
writeFollowBanner(
@@ -725,8 +723,6 @@ export const listCommand = buildListCommand(
725723
cwd,
726724
COMMAND_NAME
727725
);
728-
setContext([org], [project]);
729-
730726
if (flags.follow) {
731727
writeFollowBanner(
732728
flags.follow ?? DEFAULT_POLL_INTERVAL,

src/commands/log/view.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ export const viewCommand = buildCommand({
349349
},
350350
async *func(this: SentryContext, flags: ViewFlags, ...args: string[]) {
351351
applyFreshFlag(flags);
352-
const { cwd, setContext } = this;
352+
const { cwd } = this;
353353
const cmdLog = logger.withTag("log.view");
354354

355355
// Parse positional args
@@ -365,9 +365,6 @@ export const viewCommand = buildCommand({
365365
throw new ContextError("Organization and project", USAGE_HINT);
366366
}
367367

368-
// Set telemetry context
369-
setContext([target.org], [target.project]);
370-
371368
if (flags.web) {
372369
await handleWebOpen(target.org, logIds);
373370
return;

src/commands/span/list.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,6 @@ function jsonTransformSpanList(data: SpanListData, fields?: string[]): unknown {
239239
type ModeContext = {
240240
cwd: string;
241241
flags: ListFlags;
242-
setContext: (orgs: string[], projects: string[]) => void;
243242
};
244243

245244
/**
@@ -259,8 +258,6 @@ async function handleTraceMode(
259258
cwd,
260259
TRACE_USAGE_HINT
261260
);
262-
ctx.setContext([org], [project]);
263-
264261
const queryParts = [`trace:${traceId}`];
265262
if (flags.query) {
266263
queryParts.push(translateSpanQuery(flags.query));
@@ -324,8 +321,6 @@ async function handleProjectMode(
324321
cwd,
325322
COMMAND_NAME
326323
);
327-
ctx.setContext([org], [project]);
328-
329324
const apiQuery = flags.query ? translateSpanQuery(flags.query) : undefined;
330325

331326
const contextKey = buildPaginationContextKey(
@@ -447,9 +442,9 @@ export const listCommand = buildListCommand("span", {
447442
},
448443
},
449444
async *func(this: SentryContext, flags: ListFlags, ...args: string[]) {
450-
const { cwd, setContext } = this;
445+
const { cwd } = this;
451446
const parsed = parseSpanListArgs(args);
452-
const modeCtx: ModeContext = { cwd, flags, setContext };
447+
const modeCtx: ModeContext = { cwd, flags };
453448

454449
const { output, hint } =
455450
parsed.mode === "trace"

0 commit comments

Comments
 (0)