Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/transport-credentials.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"evlog": minor
---

Add configurable `credentials` (`RequestCredentials`, default `same-origin`) for the client log transport and browser drain `fetch` calls. The Nuxt module forwards `transport.credentials` into `runtimeConfig.public.evlog` so client `initLog()` receives it.
2 changes: 1 addition & 1 deletion apps/docs/content/3.core-concepts/6.client-logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export default defineNuxtPlugin(() => {
service: 'web',
transport: {
enabled: true,
endpoint: '/api/_evlog/ingest', // default
endpoint: '/api/_evlog/ingest',
},
})
})
Expand Down
1 change: 1 addition & 0 deletions apps/docs/content/3.core-concepts/7.configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ The Nuxt module accepts all global options and middleware options in `nuxt.confi
| `console` | `boolean` | `true` | Enable/disable browser console output (client-side only) |
| `transport.enabled` | `boolean` | `false` | Send client logs to the server via API endpoint |
| `transport.endpoint` | `string` | `'/api/_evlog/ingest'` | Custom transport endpoint |
| `transport.credentials` | `RequestCredentials` | `'same-origin'` | Fetch credentials mode (`'include'` for cross-origin endpoints) |

See the full [Nuxt configuration](/frameworks/nuxt#configuration).

Expand Down
3 changes: 2 additions & 1 deletion apps/docs/content/4.adapters/11.browser.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ const drain = pipeline(transport)
| `headers` | - | Custom headers sent with each `fetch` request (e.g. `Authorization`, `X-API-Key`) |
| `timeout` | `5000` | Request timeout in milliseconds |
| `useBeacon` | `true` | Use `sendBeacon` when the page is hidden |
| `credentials` | `'same-origin'` | Fetch credentials mode (`'omit'`, `'same-origin'`, `'include'`). Set to `'include'` for cross-origin endpoints |

### `BrowserLogDrainOptions`

Expand Down Expand Up @@ -123,7 +124,7 @@ const drain = createBrowserLogDrain({
```

::callout{icon="i-lucide-shield-alert" color="warning"}
`headers` are applied to `fetch` requests only. The `sendBeacon` API does not support custom headers, so when the page is hidden and `sendBeacon` is used, headers are not sent. If your endpoint requires authentication, consider validating via a session cookie (`credentials: 'same-origin'` is set by default) or disable `sendBeacon` with `useBeacon: false`.
`headers` are applied to `fetch` requests only. The `sendBeacon` API does not support custom headers, so when the page is hidden and `sendBeacon` is used, headers are not sent. If your endpoint requires authentication, consider validating via a session cookie (set `credentials: 'include'` for cross-origin endpoints, defaults to `'same-origin'`) or disable `sendBeacon` with `useBeacon: false`.
::

## Server Endpoint
Expand Down
6 changes: 4 additions & 2 deletions packages/evlog/src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export interface BrowserDrainConfig {
timeout?: number
/** Use sendBeacon when the page is hidden. @default true */
useBeacon?: boolean
/** Fetch credentials mode. @default 'same-origin' */
credentials?: RequestCredentials
}

export interface BrowserLogDrainOptions {
Expand Down Expand Up @@ -39,7 +41,7 @@ export interface BrowserLogDrainOptions {
* ```
*/
export function createBrowserDrain(config: BrowserDrainConfig): (batch: DrainContext[]) => Promise<void> {
const { endpoint, headers: customHeaders, timeout = 5000, useBeacon = true } = config
const { endpoint, headers: customHeaders, timeout = 5000, useBeacon = true, credentials = 'same-origin' } = config

return async (batch: DrainContext[]): Promise<void> => {
if (batch.length === 0) return
Expand Down Expand Up @@ -70,7 +72,7 @@ export function createBrowserDrain(config: BrowserDrainConfig): (batch: DrainCon
body,
signal: controller.signal,
keepalive: true,
credentials: 'same-origin',
credentials,
})

if (!response.ok) {
Expand Down
7 changes: 5 additions & 2 deletions packages/evlog/src/nuxt/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,9 @@ export interface ModuleOptions {
* @example
* ```ts
* transport: {
* enabled: true, // Send logs to server API
* endpoint: '/api/_evlog/ingest' // Custom endpoint
* enabled: true, // send client logs to server via API endpoint
* endpoint: '/api/_evlog/ingest', // default endpoint (or custom endpoint)
* credentials: 'include', // optional: cross-origin ingest
* }
* ```
*/
Expand Down Expand Up @@ -264,6 +265,7 @@ export default defineNuxtModule<ModuleOptions>({

const transportEnabled = options.transport?.enabled ?? false
const transportEndpoint = options.transport?.endpoint ?? '/api/_evlog/ingest'
const transportCredentials = options.transport?.credentials ?? 'same-origin'

// Register custom error handler for proper EvlogError serialization
// Only set if not already configured to avoid overwriting user's custom handler
Expand All @@ -280,6 +282,7 @@ export default defineNuxtModule<ModuleOptions>({
transport: {
enabled: transportEnabled,
endpoint: transportEndpoint,
credentials: transportCredentials,
},
}

Expand Down
4 changes: 3 additions & 1 deletion packages/evlog/src/runtime/client/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ let clientPretty = true
let clientService = 'client'
let transportEnabled = false
let transportEndpoint = '/api/_evlog/ingest'
let transportCredentials: RequestCredentials = 'same-origin'
let identityContext: Record<string, unknown> = {}

export function setIdentity(identity: Record<string, unknown>): void {
Expand All @@ -27,6 +28,7 @@ export function initLog(options: { enabled?: boolean, console?: boolean, pretty?
clientService = options.service ?? 'client'
transportEnabled = options.transport?.enabled ?? false
transportEndpoint = options.transport?.endpoint ?? '/api/_evlog/ingest'
transportCredentials = options.transport?.credentials ?? 'same-origin'
}

async function sendToServer(event: Record<string, unknown>): Promise<void> {
Expand All @@ -38,7 +40,7 @@ async function sendToServer(event: Record<string, unknown>): Promise<void> {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event),
keepalive: true,
credentials: 'same-origin',
credentials: transportCredentials,
})
} catch {
// Silently fail - don't break the app
Expand Down
6 changes: 6 additions & 0 deletions packages/evlog/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ export interface TransportConfig {
* @default '/api/_evlog/ingest'
*/
endpoint?: string

/**
* Fetch credentials mode
* @default 'same-origin'
*/
credentials?: RequestCredentials
}

/**
Expand Down
9 changes: 9 additions & 0 deletions packages/evlog/test/browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ describe('createBrowserDrain', () => {
})
})

it('uses custom credentials mode', async () => {
const drain = createBrowserDrain({ endpoint: '/api/logs', credentials: 'include' })

await drain([createTestContext(1)])

const [, options] = vi.mocked(fetch).mock.calls[0]!
expect(options?.credentials).toBe('include')
})

it('skips empty batches', async () => {
const drain = createBrowserDrain({ endpoint: '/api/logs' })

Expand Down
14 changes: 14 additions & 0 deletions packages/evlog/test/client-console.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,20 @@ describe('client console option', () => {
expect(fetchSpy).toHaveBeenCalledTimes(1)
})

it('uses custom credentials mode for transport', () => {
initLog({
enabled: true,
console: false,
pretty: false,
transport: { enabled: true, endpoint: '/api/_evlog/ingest', credentials: 'include' },
})

log.info({ action: 'test' })

const [, options] = fetchSpy.mock.calls[0]!
expect(options?.credentials).toBe('include')
})

it('suppresses pretty console output when console is false', () => {
initLog({ enabled: true, console: false, pretty: true })

Expand Down
Loading