kit icon indicating copy to clipboard operation
kit copied to clipboard

There is no way of transfering functions and/or objects with associated methods to client code during SSR in SvelteKit, but it is present in Svelte with Server-side component API

Open secondfry opened this issue 1 year ago • 2 comments

Describe the problem

i.e. I want to log errors during SSR to Sentry while having access to RequestEvent parts.

Describe the proposed solution

Use Svelte Server-side component API in SvelteKit via passing context to client code during SSR.

Alternatives considered

  1. Ignoring errors during SSR as same errors will be logged via client side logging. Sounds... sad.
  2. Passing all required options down to client-side code to create server-side logger only during SSR. Can be quite cumbersome.

Importance

would make my life easier

Additional Information

Proposed solution has open PR – https://github.com/sveltejs/kit/pull/10064.

Additional info from there:

@secondfry: Sometimes you need a function defined only in server context – i.e. to log errors inside of SSR. This PR allows one to define context in RequestEvent as follows:

hooks.server.ts

const handle: Handle = async ({ event, resolve }) => {
  const logger = new Logger();
  event.context = new Map();
  event.context.set('logger', logger);
  return await resolve(event);
});

some +page.svelte

import { browser } from '$app/environment';
const logger = browser ? new ClientLogger() : getContext('logger');
logger.info('yes');

@benmccann:

const logger = browser ? new ClientLogger() : getContext('logger');

I feel like there's not enough context here to understand this PR and there should probably be an associated issue. Why can you do new ClientLogger() but can't do new ServerLogger()?

@secondfry: @benmccann in case of very simple logging requirements, yes, maybe one can do that. But the moment you will need to transfer such logs to i.e. Sentry, with request context, timings, etc., you will question yourself: why can't you just pass already initialized logger from server side?

Expanding on the example you would have something like this: hooks.server.ts

const initLogger = async ({ event, resolve }) => {
  event.locals.logger = new QuiteComplicatedServerLogger({ endpoint, format, ... });
  return await resolve(event);
};

const quiteComplicatedThingWhichRequiresLoggerA = async ({ event, resolve }) => {
  try { await explode(); } catch (cause) { event.locals.logger(new Error('Unexpected explosion', { cause })); }
  return await resolve(event);
};
const quiteComplicatedThingWhichRequiresLoggerB = ...;
const quiteComplicatedThingWhichRequiresLoggerC = ...;

const handle: Handle = sequence(
  initLogger,
  quiteComplicatedThingWhichRequiresLoggerA,
  quiteComplicatedThingWhichRequiresLoggerB,
  quiteComplicatedThingWhichRequiresLoggerC,
  async ({ event, resolve }) => {
    event.context = new Map();
    event.context.set('logger', event.locals.logger);
    return await resolve(event);
  }
);

Otherwise one would have to pass all options of QuiteComplicatedServerLogger to svelte code.

So as Svelte has Server-side component API which allows passing context it seemed to us as the most logical choice. If there are other ways to pass functions and/or objects with associated methods into client code during SSR, I'm would gladly learn that.

secondfry avatar May 31 '23 13:05 secondfry

I think this is reasonably simple already:

  • Create a module that returns the logger
  • Attach the logger to a context in +layout.svelte

For example:

// $lib/logger

const loggerContextKey = new Symbol('logger');

export function createLogger(config) {
  return browser ? new ClientLogger(config) : new ServerLogger(config);
}

export function createLoggerContext(logger) {
  setContext(loggerContextKey, logger);
}

export function getLogger() {
  return getContext(loggerContextKey);
}
<script>
  // routes/+layout.svelte
  import { createLogger, createLoggerContext } from `$lib/logger`;

  createLoggerContext(createLogger);
</script>

<slot />

You'll have to adjust the module depending on whether you need your logger scoped to the request (you can simplify if you can share an instance between all requests on the server), but that should get you pretty much all the way there.

(disclaimer: typed on phone)

Agree with @tcc-sejohnson that should be possible to do with existing methods. Additionally, if you need some initial value passed to the logger context you could always return that from your load function.

/// file: +layout.js
import { browser } from '$app/environment';

export function load() {
  return {
    stuff: 'foo',
    loggerStuff: browser ? undefined : something
  }
}
<!-- file: +layout.svelte -->
<script>
  export let data;

  createLoggerContext(logger, data.loggerStuff);
</script>

Is there a use case where this wouldn't work which I'm missing?

dummdidumm avatar Jun 23 '23 08:06 dummdidumm

Setting/getting logger via context is too late, such logger won't be available in hooks since contexts are only creatable from Svelte components.

secondfry avatar Feb 28 '24 07:02 secondfry