All files / src/lib/dsn resolver.ts

97.14% Statements 34/35
85.71% Branches 12/14
100% Functions 5/5
96.96% Lines 32/33

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 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186                                                                          8x 1x               7x 7x                   7x 3x 3x 2x           1x                   1x   1x             4x     2x   2x                                   4x     5x   4x 1x             3x     4x   3x 1x           2x                                                 8x   8x 8x   6x 8x 8x   6x 6x                             8x    
/**
 * Project Resolver
 *
 * Resolves DSN to org/project information with caching.
 * Uses cached resolution when available to avoid API calls.
 */
 
import {
  findProjectByDsnKey,
  listOrganizations,
  listProjects,
  resolveOrgDisplayName,
} from "../api-client.js";
import { getCachedDsn, updateCachedResolution } from "../db/dsn-cache.js";
import { getDsnSourceDescription } from "./detector.js";
import type {
  DetectedDsn,
  ResolvedProject,
  ResolvedProjectInfo,
} from "./types.js";
 
/**
 * Resolve a detected DSN to full project information
 *
 * Uses cached resolution if available, otherwise fetches from API.
 * Updates cache with resolution for future use.
 *
 * @param cwd - Directory where DSN was detected
 * @param dsn - Detected DSN to resolve
 * @returns Resolved project with org/project slugs and names
 * @throws Error if DSN cannot be resolved (no org ID, API error, etc.)
 */
export async function resolveProject(
  cwd: string,
  dsn: DetectedDsn
): Promise<ResolvedProject> {
  // Check if we have cached resolution
  if (dsn.resolved) {
    return {
      ...dsn.resolved,
      dsn,
      sourceDescription: getDsnSourceDescription(dsn),
    };
  }
 
  // Check cache for resolution
  const cached = getCachedDsn(cwd);
  Iif (cached?.resolved && cached.dsn === dsn.raw) {
    return {
      ...cached.resolved,
      dsn,
      sourceDescription: getDsnSourceDescription(dsn),
    };
  }
 
  // Need to fetch from API
  // For DSNs without orgId, try to resolve by searching with the public key
  if (!dsn.orgId) {
    const project = await findProjectByDsnKey(dsn.publicKey);
    if (!project?.organization) {
      throw new Error(
        "Cannot resolve project: DSN could not be matched to any accessible project. " +
          "You may not have access, or specify the target explicitly: sentry <command> <org>/<project>"
      );
    }
 
    const resolved: ResolvedProjectInfo = {
      orgSlug: project.organization.slug,
      orgName: resolveOrgDisplayName(
        project.organization.slug,
        project.organization.name
      ),
      projectSlug: project.slug,
      projectName: project.name,
    };
 
    updateCachedResolution(cwd, resolved);
 
    return {
      ...resolved,
      dsn,
      sourceDescription: getDsnSourceDescription(dsn),
    };
  }
 
  const resolved = await fetchProjectInfo(dsn.orgId, dsn.projectId);
 
  // Update cache with resolution
  updateCachedResolution(cwd, resolved);
 
  return {
    ...resolved,
    dsn,
    sourceDescription: getDsnSourceDescription(dsn),
  };
}
 
/**
 * Fetch project info from Sentry API
 *
 * Since we only have orgId (numeric) and projectId (numeric) from the DSN,
 * we need to fetch the org and project to get slugs and names.
 */
async function fetchProjectInfo(
  orgId: string,
  projectId: string
): Promise<ResolvedProjectInfo> {
  // Fetch all orgs to find the one matching our orgId
  const orgs = await listOrganizations();
 
  // Find org by ID - org.id might be string or number depending on API
  const org = orgs.find((o) => String(o.id) === orgId);
 
  if (!org) {
    throw new Error(
      `Could not find organization with ID ${orgId}. ` +
        "You may not have access to this organization."
    );
  }
 
  // Fetch projects for this org to find the one matching our projectId
  const projects = await listProjects(org.slug);
 
  // Find project by ID
  const project = projects.find((p) => String(p.id) === projectId);
 
  if (!project) {
    throw new Error(
      `Could not find project with ID ${projectId} in organization ${org.slug}. ` +
        "You may not have access to this project."
    );
  }
 
  return {
    orgSlug: org.slug,
    orgName: org.name,
    projectSlug: project.slug,
    projectName: project.name,
  };
}
 
/** Project reference with org context */
type AccessibleProject = {
  org: string;
  project: string;
  orgName: string;
  projectName: string;
};
 
/**
 * Get list of accessible projects for the current user.
 * Fetches all projects from all accessible organizations.
 *
 * Used for "no DSN found" error messages to help user specify project.
 *
 * @returns Array of org/project pairs
 */
export async function getAccessibleProjects(): Promise<AccessibleProject[]> {
  const results: AccessibleProject[] = [];
 
  try {
    const orgs = await listOrganizations();
 
    for (const org of orgs) {
      try {
        const projects = await listProjects(org.slug);
 
        for (const project of projects) {
          results.push({
            org: org.slug,
            project: project.slug,
            orgName: org.name,
            projectName: project.name,
          });
        }
      } catch {
        // Skip orgs we can't access
      }
    }
  } catch {
    // Not authenticated or API error
  }
 
  return results;
}