|
| 1 | +import { |
| 2 | + SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD, |
| 3 | + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, |
| 4 | + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, |
| 5 | + SEMANTIC_ATTRIBUTE_URL_FULL, |
| 6 | +} from '../semanticAttributes'; |
| 7 | +import type { SpanAttributes } from '../types-hoist/span'; |
| 8 | + |
1 | 9 | type PartialURL = {
|
2 | 10 | host?: string;
|
3 | 11 | path?: string;
|
@@ -53,7 +61,7 @@ export function isURLObjectRelative(url: URLObject): url is RelativeURL {
|
53 | 61 | * @returns The parsed URL object or undefined if the URL is invalid
|
54 | 62 | */
|
55 | 63 | export function parseStringToURLObject(url: string, urlBase?: string | URL | undefined): URLObject | undefined {
|
56 |
| - const isRelative = url.startsWith('/'); |
| 64 | + const isRelative = url.indexOf('://') <= 0 && url.indexOf('//') !== 0; |
57 | 65 | const base = urlBase ?? (isRelative ? DEFAULT_BASE_URL : undefined);
|
58 | 66 | try {
|
59 | 67 | // Use `canParse` to short-circuit the URL constructor if it's not a valid URL
|
@@ -107,6 +115,95 @@ export function getSanitizedUrlStringFromUrlObject(url: URLObject): string {
|
107 | 115 | return newUrl.toString();
|
108 | 116 | }
|
109 | 117 |
|
| 118 | +type PartialRequest = { |
| 119 | + method?: string; |
| 120 | +}; |
| 121 | + |
| 122 | +function getHttpSpanNameFromUrlObject( |
| 123 | + urlObject: URLObject | undefined, |
| 124 | + kind: 'server' | 'client', |
| 125 | + request?: PartialRequest, |
| 126 | + routeName?: string, |
| 127 | +): string { |
| 128 | + const method = request?.method?.toUpperCase() ?? 'GET'; |
| 129 | + const route = routeName |
| 130 | + ? routeName |
| 131 | + : urlObject |
| 132 | + ? kind === 'client' |
| 133 | + ? getSanitizedUrlStringFromUrlObject(urlObject) |
| 134 | + : urlObject.pathname |
| 135 | + : '/'; |
| 136 | + |
| 137 | + return `${method} ${route}`; |
| 138 | +} |
| 139 | + |
| 140 | +/** |
| 141 | + * Takes a parsed URL object and returns a set of attributes for the span |
| 142 | + * that represents the HTTP request for that url. This is used for both server |
| 143 | + * and client http spans. |
| 144 | + * |
| 145 | + * Follows https://opentelemetry.io/docs/specs/semconv/http/. |
| 146 | + * |
| 147 | + * @param urlObject - see {@link parseStringToURLObject} |
| 148 | + * @param kind - The type of HTTP operation (server or client) |
| 149 | + * @param spanOrigin - The origin of the span |
| 150 | + * @param request - The request object, see {@link PartialRequest} |
| 151 | + * @param routeName - The name of the route, must be low cardinality |
| 152 | + * @returns The span name and attributes for the HTTP operation |
| 153 | + */ |
| 154 | +export function getHttpSpanDetailsFromUrlObject( |
| 155 | + urlObject: URLObject | undefined, |
| 156 | + kind: 'server' | 'client', |
| 157 | + spanOrigin: string, |
| 158 | + request?: PartialRequest, |
| 159 | + routeName?: string, |
| 160 | +): [name: string, attributes: SpanAttributes] { |
| 161 | + const attributes: SpanAttributes = { |
| 162 | + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: spanOrigin, |
| 163 | + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', |
| 164 | + }; |
| 165 | + |
| 166 | + if (routeName) { |
| 167 | + // This is based on https://opentelemetry.io/docs/specs/semconv/http/http-spans/#name |
| 168 | + attributes[kind === 'server' ? 'http.route' : 'url.template'] = routeName; |
| 169 | + attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] = 'route'; |
| 170 | + } |
| 171 | + |
| 172 | + if (request?.method) { |
| 173 | + attributes[SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD] = request.method.toUpperCase(); |
| 174 | + } |
| 175 | + |
| 176 | + if (urlObject) { |
| 177 | + if (urlObject.search) { |
| 178 | + attributes['url.query'] = urlObject.search; |
| 179 | + } |
| 180 | + if (urlObject.hash) { |
| 181 | + attributes['url.fragment'] = urlObject.hash; |
| 182 | + } |
| 183 | + if (urlObject.pathname) { |
| 184 | + attributes['url.path'] = urlObject.pathname; |
| 185 | + if (urlObject.pathname === '/') { |
| 186 | + attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] = 'route'; |
| 187 | + } |
| 188 | + } |
| 189 | + |
| 190 | + if (!isURLObjectRelative(urlObject)) { |
| 191 | + attributes[SEMANTIC_ATTRIBUTE_URL_FULL] = urlObject.href; |
| 192 | + if (urlObject.port) { |
| 193 | + attributes['url.port'] = urlObject.port; |
| 194 | + } |
| 195 | + if (urlObject.protocol) { |
| 196 | + attributes['url.scheme'] = urlObject.protocol; |
| 197 | + } |
| 198 | + if (urlObject.hostname) { |
| 199 | + attributes[kind === 'server' ? 'server.address' : 'url.domain'] = urlObject.hostname; |
| 200 | + } |
| 201 | + } |
| 202 | + } |
| 203 | + |
| 204 | + return [getHttpSpanNameFromUrlObject(urlObject, kind, request, routeName), attributes]; |
| 205 | +} |
| 206 | + |
110 | 207 | /**
|
111 | 208 | * Parses string form of URL into an object
|
112 | 209 | * // borrowed from https://tools.ietf.org/html/rfc3986#appendix-B
|
|
0 commit comments