diff --git a/frontend/src/lib/gql/gql-client.ts b/frontend/src/lib/gql/gql-client.ts index 402356732..1850b2ed9 100644 --- a/frontend/src/lib/gql/gql-client.ts +++ b/frontend/src/lib/gql/gql-client.ts @@ -1,5 +1,5 @@ import {browser} from '$app/environment'; -import {tracingExchange} from '$lib/otel'; +import {tracingExchange, tryCopyTraceContext} from '$lib/otel'; import type {LexAuthUser} from '$lib/user'; import {isRedirect} from '@sveltejs/kit'; import {devtoolsExchange} from '@urql/devtools'; @@ -279,7 +279,19 @@ class GqlClient { } private throwAnyUnexpectedErrors>(result: T, delayThrow: boolean = false): void { - if (!result.error) return; + const error = this.squashErrors(result); + if (!error) return; + + if (delayThrow && !isRedirect(error)) { // SvelteKit handles Redirects, so we don't want to delay them + // We can't throw errors here, because errors thrown in wonka/an exchange kill the frontend. + setTimeout(() => { throw error; }); + } else { + throw error; + } + } + + private squashErrors>(result: T): Error | undefined { + if (!result.error) return undefined; const error = // Various status codes are handled in the fetch hooks (see hooks.shared.ts). // throws there (e.g. SvelteKit redirects and 500's) turn into networkErrors that land here @@ -289,13 +301,8 @@ class GqlClient { // but it's far more interesting (particularly when debugging how errors affect our app) to know when and where errors are getting thrown, namely HERE. // So, we new up our own error to get the more useful stacktrace. new AggregateError(result.error.graphQLErrors, result.error.message ?? result.error.cause); - - if (delayThrow && !isRedirect(error)) { // SvelteKit handles Redirects, so we don't want to delay them - // We can't throw errors here, because errors thrown in wonka/an exchange kill the frontend. - setTimeout(() => { throw error; }); - } else { - throw error; - } + tryCopyTraceContext(result.error, error); + return error; } private findInputErrors({data}: OperationResult): LexGqlError> | undefined { diff --git a/frontend/src/lib/otel/types.ts b/frontend/src/lib/otel/types.ts index 93061f657..c4afcd171 100644 --- a/frontend/src/lib/otel/types.ts +++ b/frontend/src/lib/otel/types.ts @@ -1,7 +1,7 @@ -import { isObject, isObjectWhere } from '$lib/util/types'; +import {isObject, isObjectWhere} from '$lib/util/types'; -import type { ErrorSource } from './otel.shared'; -import type { SpanContext } from '@opentelemetry/api'; +import type {ErrorSource} from './otel.shared'; +import type {SpanContext} from '@opentelemetry/api'; export type TraceId = string; export type TracerId = ErrorSource; @@ -44,3 +44,9 @@ export function traceIt(traceable: Traceable, spanContext: SpanContext, tracer: throw new TraceItError(traceable, `spanContext not writeable.`); } } + +export function tryCopyTraceContext(from: unknown, to: unknown): void { + if (!isTraced(from)) return; + if (!isTraceable(to)) return; + traceIt(to, from.spanContext, from.tracer); +} diff --git a/frontend/src/routes/(authenticated)/admin/+page.ts b/frontend/src/routes/(authenticated)/admin/+page.ts index e688e5b0f..03a5e0888 100644 --- a/frontend/src/routes/(authenticated)/admin/+page.ts +++ b/frontend/src/routes/(authenticated)/admin/+page.ts @@ -1,28 +1,25 @@ -import { getClient, graphql } from '$lib/gql'; +import {getClient, graphql} from '$lib/gql'; -import type { PageLoadEvent } from './$types'; -import { type LexAuthUser } from '$lib/user'; -import { redirect } from '@sveltejs/kit'; +import type {PageLoadEvent} from './$types'; +import {type LexAuthUser} from '$lib/user'; +import {redirect} from '@sveltejs/kit'; import {getBoolSearchParam, getSearchParam} from '$lib/util/query-params'; -import { isGuid } from '$lib/util/guid'; +import {isGuid} from '$lib/util/guid'; import type { $OpResult, ChangeUserAccountByAdminInput, ChangeUserAccountByAdminMutation, CreateGuestUserByAdminInput, - CreateGuestUserByAdminMutation, - DraftProjectFilterInput, - ProjectFilterInput, - SetUserLockedInput, + CreateGuestUserByAdminMutation, SetUserLockedInput, SetUserLockedMutation, - UserFilterInput, + UserFilterInput } from '$lib/gql/types'; import type {LoadAdminDashboardProjectsQuery, LoadAdminDashboardUsersQuery} from '$lib/gql/types'; -import type { ProjectFilters } from '$lib/components/Projects'; -import { DEFAULT_PAGE_SIZE } from '$lib/components/Paging'; -import type { AdminTabId } from './AdminTabs.svelte'; -import { derived, readable } from 'svelte/store'; -import type { UserType } from '$lib/components/Users/UserFilter.svelte'; +import type {ProjectFilters} from '$lib/components/Projects'; +import {DEFAULT_PAGE_SIZE} from '$lib/components/Paging'; +import type {AdminTabId} from './AdminTabs.svelte'; +import {derived, readable} from 'svelte/store'; +import type {UserType} from '$lib/components/Users/UserFilter.svelte'; // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents -- false positive? export type AdminSearchParams = ProjectFilters & { @@ -46,12 +43,12 @@ export async function load(event: PageLoadEvent) { const client = getClient(); - const projectFilter: ProjectFilterInput = { + const projectFilter = { ...(memberSearch ? { users: { some: { user: { or: [ { email: { eq: memberSearch } }, { username: { eq: memberSearch } } ] } } } }: {}) - }; - const draftFilter: DraftProjectFilterInput = { + } as const; + const draftFilter = { ...(memberSearch ? { projectManager: { or: [ { email: { eq: memberSearch } }, { username: { eq: memberSearch } } ] } } : {}) - }; + } as const;