Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 69 additions & 13 deletions src/Exceptionless.Web/ClientApp/src/hooks.client.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,85 @@
import type { ServerInit } from '@sveltejs/kit';
import type { HandleClientError, ServerInit } from '@sveltejs/kit';

import { dev } from '$app/environment';
import { page } from '$app/state';
import { env } from '$env/dynamic/public';
import { Exceptionless, toError } from '@exceptionless/browser';
import { normalizePath, normalizeRouteId } from '$lib/telemetry';
import { Exceptionless, guid, toError } from '@exceptionless/browser';
import { useMiddleware } from '@exceptionless/fetchclient';

// If the PUBLIC_BASE_URL is set in local storage, we will use that instead of the one from the environment variables.
// This allows you to target other environments from your browser.
const PUBLIC_BASE_URL = localStorage?.getItem('PUBLIC_BASE_URL');
if (PUBLIC_BASE_URL) {
env.PUBLIC_BASE_URL = PUBLIC_BASE_URL;
}

export const init: ServerInit = async () => {
if (!env.PUBLIC_EXCEPTIONLESS_API_KEY) {
return;
}

await Exceptionless.startup((c) => {
c.apiKey = env.PUBLIC_EXCEPTIONLESS_API_KEY;
c.serverUrl = env.PUBLIC_EXCEPTIONLESS_SERVER_URL || window.location.origin;

c.defaultTags.push('UI', 'Svelte');
c.settings['@@log:*'] = 'debug';

if (env.PUBLIC_APP_VERSION) {
c.version = env.PUBLIC_APP_VERSION;
}

if (dev) {
c.settings['@@log:*'] = 'debug';
}

c.useSessions();

c.addPlugin('route-context', 10, async (ctx) => {
if (ctx.event.type !== 'usage') {
ctx.event.data = ctx.event.data ?? {};
ctx.event.data['@route'] = normalizeRouteId(page.route.id);
}
});
});

useMiddleware(async (ctx, next) => {
await next();

const status = ctx.response?.status;
if (!status || (status < 500 && status !== 429)) {
return;
}

const rawUrl = ctx.request?.url ?? '';
if (rawUrl.includes('/api/v2/events') || rawUrl.includes('/api/v2/configuration')) {
return;
}

const method = ctx.request?.method ?? 'UNKNOWN';
const pathname = (() => {
try {
return new URL(rawUrl).pathname;
} catch {
return rawUrl;
}
})();
const path = normalizePath(pathname, '');
void Exceptionless.createLog(`${method} ${path}`, `HTTP ${status}`, 'warn').addTags('api-failure').submit();
});
};

/** @type {import('@sveltejs/kit').HandleClientError} */
export async function handleError({ error, event, message, status }) {
console.warn({ error, event, message, source: 'client error handler', status });
await Exceptionless.createException(toError(error ?? message))
.setProperty('status', status)
.submit();
}
export const handleError: HandleClientError = async ({ error, event, message, status }) => {
if (dev) {
console.warn({ error, event, message, status });
}

let errorId: null | string = null;
try {
await Exceptionless.createException(toError(error ?? message))
.setProperty('status', String(status))
.submit();
errorId = Exceptionless.getLastReferenceId();
} catch {
// never throw
}

return { errorId: errorId ?? guid(), message };
};
Comment on lines +75 to +85
21 changes: 21 additions & 0 deletions src/Exceptionless.Web/ClientApp/src/lib/telemetry/Telemetry.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<script lang="ts">
import { afterNavigate } from '$app/navigation';
import { Exceptionless } from '@exceptionless/browser';

import { normalizeRouteId } from './index';

interface Props {
userId?: string;
userName?: string;
}

let { userId, userName }: Props = $props();

afterNavigate(({ to }) => {
void Exceptionless.submitFeatureUsage(normalizeRouteId(to?.route.id ?? null));
});

$effect(() => {
Exceptionless.config.setUserIdentity(userId ?? '', userName ?? '');
});
</script>
Comment on lines +1 to +21
2 changes: 2 additions & 0 deletions src/Exceptionless.Web/ClientApp/src/lib/telemetry/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { normalizePath, normalizeRouteId } from './route';
export { default as Telemetry } from './Telemetry.svelte';
Comment on lines +1 to +2
45 changes: 45 additions & 0 deletions src/Exceptionless.Web/ClientApp/src/lib/telemetry/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
const OBJECTID_SEGMENT_REGEX = /^[0-9a-f]{24}$/i;
const UUID_SEGMENT_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const NUMERIC_SEGMENT_REGEX = /^\d+$/;

export function normalizePath(path: string, basePath = '/next'): string {
let normalized = path;

if (basePath && normalized.startsWith(basePath)) {
normalized = normalized.slice(basePath.length);
}

if (!normalized.startsWith('/')) {
normalized = '/' + normalized;
}

normalized = normalized
.split('/')
.map((segment) => (segment && isIdSegment(segment) ? ':id' : segment))
.join('/');

if (normalized.length > 1 && normalized.endsWith('/')) {
normalized = normalized.slice(0, -1);
}

return normalized;
}

export function normalizeRouteId(routeId: null | string): string {
if (!routeId) {
return 'root';
}

return (
routeId
.replace(/\/\([^)]+\)/g, '')
.replace(/\[\[([^=\]]+)(?:=[^\]]+)?\]\]/g, ':$1')
.replace(/\[\.\.\.([^=\]]+)(?:=[^\]]+)?\]/g, ':$1')
.replace(/\[([^=\]]+)(?:=[^\]]+)?\]/g, ':$1')
.replace(/^\//, '') || 'root'
);
}
Comment on lines +28 to +41

function isIdSegment(segment: string): boolean {
return OBJECTID_SEGMENT_REGEX.test(segment) || UUID_SEGMENT_REGEX.test(segment) || NUMERIC_SEGMENT_REGEX.test(segment);
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import { invalidateWebhookQueries } from '$features/webhooks/api.svelte';
import { isEntityChangedType, type WebSocketMessageType } from '$features/websockets/models';
import { WebSocketClient } from '$features/websockets/web-socket-client.svelte';
import { Telemetry } from '$lib/telemetry';
import { useMiddleware } from '@exceptionless/fetchclient';
import { useQueryClient } from '@tanstack/svelte-query';
import { tick } from 'svelte';
Expand Down Expand Up @@ -58,7 +59,6 @@
let isOrganizationSwitcherOpen = $state(false);
let isUserMenuOpen = $state(false);

// Auto-reset premium page state on navigation so pages don't need cleanup
beforeNavigate(() => {
premiumPage.current = undefined;
});
Expand Down Expand Up @@ -533,3 +533,5 @@
<UpgradeRequiredDialog />
{/if}
{/if}

<Telemetry userId={isAuthenticated ? meQuery.data?.id : undefined} userName={isAuthenticated ? meQuery.data?.full_name : undefined} />