|
| 1 | +/** Supported Sentry transport protocols in a Dsn. */ |
| 2 | +export type DsnProtocol = 'http' | 'https'; |
| 3 | + |
| 4 | +/** Primitive components of a Dsn. */ |
| 5 | +interface DsnComponents { |
| 6 | + /** Protocol used to connect to Sentry. */ |
| 7 | + protocol: DsnProtocol; |
| 8 | + /** Public authorization key. */ |
| 9 | + publicKey?: string; |
| 10 | + /** Private authorization key (deprecated, optional). */ |
| 11 | + pass?: string; |
| 12 | + /** Hostname of the Sentry instance. */ |
| 13 | + host: string; |
| 14 | + /** Port of the Sentry instance. */ |
| 15 | + port?: string; |
| 16 | + /** Sub path/ */ |
| 17 | + path?: string; |
| 18 | + /** Project ID */ |
| 19 | + projectId: string; |
| 20 | +} |
| 21 | + |
| 22 | +interface Package { |
| 23 | + name: string; |
| 24 | + version: string; |
| 25 | + dependencies?: Record<string, string>; |
| 26 | + devDependencies?: Record<string, string>; |
| 27 | +} |
| 28 | + |
| 29 | +interface SdkInfo { |
| 30 | + name?: string; |
| 31 | + version?: string; |
| 32 | + integrations?: string[]; |
| 33 | + packages?: Package[]; |
| 34 | + settings?: { |
| 35 | + infer_ip?: 'auto' | 'never'; |
| 36 | + }; |
| 37 | +} |
| 38 | + |
| 39 | +const SENTRY_API_VERSION = '7'; |
| 40 | + |
| 41 | +/** |
| 42 | + * Converts a Dsn string to the ingest endpoint URL. |
| 43 | + * |
| 44 | + * @param dsnString A Dsn as string |
| 45 | + * @returns The ingest endpoint URL |
| 46 | + */ |
| 47 | +export function dsnStringToIngestEndpoint(dsnString: string): string { |
| 48 | + const dsn = dsnFromString(dsnString); |
| 49 | + if (!dsn) { |
| 50 | + throw new Error(`Invalid Sentry Dsn: ${dsnString}`); |
| 51 | + } |
| 52 | + return getEnvelopeEndpointWithUrlEncodedAuth(dsn); |
| 53 | +} |
| 54 | + |
| 55 | +/** Returns the prefix to construct Sentry ingestion API endpoints. */ |
| 56 | +function getBaseApiEndpoint(dsn: DsnComponents): string { |
| 57 | + const protocol = dsn.protocol ? `${dsn.protocol}:` : ''; |
| 58 | + const port = dsn.port ? `:${dsn.port}` : ''; |
| 59 | + return `${protocol}//${dsn.host}${port}${dsn.path ? `/${dsn.path}` : ''}/api/`; |
| 60 | +} |
| 61 | + |
| 62 | +/** Returns the ingest API endpoint for target. */ |
| 63 | +function _getIngestEndpoint(dsn: DsnComponents): string { |
| 64 | + return `${getBaseApiEndpoint(dsn)}${dsn.projectId}/envelope/`; |
| 65 | +} |
| 66 | + |
| 67 | +/** Returns a URL-encoded string with auth config suitable for a query string. */ |
| 68 | +function _encodedAuth(dsn: DsnComponents, sdkInfo: SdkInfo | undefined): string { |
| 69 | + const params: Record<string, string> = { |
| 70 | + sentry_version: SENTRY_API_VERSION, |
| 71 | + }; |
| 72 | + |
| 73 | + if (dsn.publicKey) { |
| 74 | + // We send only the minimum set of required information. See |
| 75 | + // https://github.com/getsentry/sentry-javascript/issues/2572. |
| 76 | + params.sentry_key = dsn.publicKey; |
| 77 | + } |
| 78 | + |
| 79 | + if (sdkInfo) { |
| 80 | + params.sentry_client = `${sdkInfo.name}/${sdkInfo.version}`; |
| 81 | + } |
| 82 | + |
| 83 | + return new URLSearchParams(params).toString(); |
| 84 | +} |
| 85 | + |
| 86 | +/** |
| 87 | + * Returns the envelope endpoint URL with auth in the query string. |
| 88 | + * |
| 89 | + * Sending auth as part of the query string and not as custom HTTP headers avoids CORS preflight requests. |
| 90 | + */ |
| 91 | +function getEnvelopeEndpointWithUrlEncodedAuth(dsn: DsnComponents, tunnel?: string, sdkInfo?: SdkInfo): string { |
| 92 | + return tunnel ? tunnel : `${_getIngestEndpoint(dsn)}?${_encodedAuth(dsn, sdkInfo)}`; |
| 93 | +} |
| 94 | + |
| 95 | +const DSN_REGEX = /^(?:(\w+):)\/\/(?:(\w+)(?::(\w+)?)?@)([\w.-]+)(?::(\d+))?\/(.+)/; |
| 96 | + |
| 97 | +/** |
| 98 | + * Parses a Dsn from a given string. |
| 99 | + * |
| 100 | + * @param str A Dsn as string |
| 101 | + * @returns Dsn as DsnComponents or undefined if @param str is not a valid DSN string |
| 102 | + */ |
| 103 | +function dsnFromString(str: string): DsnComponents | undefined { |
| 104 | + const match = DSN_REGEX.exec(str); |
| 105 | + |
| 106 | + if (!match) { |
| 107 | + // eslint-disable-next-line no-console |
| 108 | + console.error(`Invalid Sentry Dsn: ${str}`); |
| 109 | + |
| 110 | + return undefined; |
| 111 | + } |
| 112 | + |
| 113 | + const [protocol, publicKey, pass = '', host = '', port = '', lastPath = ''] = match.slice(1); |
| 114 | + let path = ''; |
| 115 | + let projectId = lastPath; |
| 116 | + |
| 117 | + const split = projectId.split('/'); |
| 118 | + if (split.length > 1) { |
| 119 | + path = split.slice(0, -1).join('/'); |
| 120 | + projectId = split.pop() as string; |
| 121 | + } |
| 122 | + |
| 123 | + if (projectId) { |
| 124 | + const projectMatch = projectId.match(/^\d+/); |
| 125 | + if (projectMatch) { |
| 126 | + projectId = projectMatch[0]; |
| 127 | + } |
| 128 | + } |
| 129 | + |
| 130 | + return dsnFromComponents({ host, pass, path, projectId, port, protocol: protocol as DsnProtocol, publicKey }); |
| 131 | +} |
| 132 | + |
| 133 | +function dsnFromComponents(components: DsnComponents): DsnComponents { |
| 134 | + return { |
| 135 | + protocol: components.protocol, |
| 136 | + publicKey: components.publicKey || '', |
| 137 | + pass: components.pass || '', |
| 138 | + host: components.host, |
| 139 | + port: components.port || '', |
| 140 | + path: components.path || '', |
| 141 | + projectId: components.projectId, |
| 142 | + }; |
| 143 | +} |
0 commit comments