aws-rum-web icon indicating copy to clipboard operation
aws-rum-web copied to clipboard

[Bug]: `FetchPlugin` does not support server-side rendering

Open quisido opened this issue 1 year ago • 12 comments

Which web client version did you detect this bug with?

v1.12.0

What environment (build systems, module system, and framework) did you detect this bug with?

  • NextJS 12.3.4
  • React 18.2.0
  • TypeScript 4.9.5
  • Webpack (unknown version, NextJS internal)

Is your web application a single page application (SPA) or multi page application (MPA)?

SPA

Please provide your web client configuration

N/A

Please describe the bug/issue

When instantiating new AwsRum(), server-side rendering throws an error:

TypeError: Failed to fetch
     at VirtualPageLoadTimer._this.fetch (webpack-internal:///./node_modules/aws-rum-web/dist/es/sessions/VirtualPageLoadTimer.js:57:18)
     at eval (webpack-internal:///./node_modules/aws-rum-web/dist/es/sessions/VirtualPageLoadTimer.js:71:33)
     at FetchPlugin._this.fetch (webpack-internal:///./node_modules/aws-rum-web/dist/es/plugins/event-plugins/FetchPlugin.js:163:18)
     at eval (webpack-internal:///./node_modules/aws-rum-web/dist/es/plugins/event-plugins/FetchPlugin.js:179:33)

It appears to be trying to call window.fetch once instantiated, which of course does not exist.

I would like to instantiate new AwsRum on the server without it actually connecting to emit any metrics, since there is no user in this context -- there should be nothing to emit, as no events have been triggered.

  • I understand that a server-side rendered page cannot (should not) be connecting to RUM to emit metrics.
  • My concern is that I do not believe that instantiating AwsRum in and of itself should not be connecting to RUM to emit metrics.

Please advise. 😊

quisido avatar Jun 08 '23 15:06 quisido

Hi Charles, web clients should not be initialized on a server - is there a specific reason why you wish to do so?

adebayor123 avatar Jun 08 '23 18:06 adebayor123

I'm server rendering a React application, so the client side runtime code is also executing on the server.

quisido avatar Jun 08 '23 23:06 quisido

Hi, does this issue mean that RUM is not supported when server-side rendering is enabled on frameworks such as NextJS? This should be clearly noted, as NextJS is currently the recommended React framework, and is a blocker to teams using server-side rendering. @CharlesStover , did you find a way around this for your use case?

hjniermann avatar Jun 30 '23 14:06 hjniermann

You can use useEffect to instantiate RUM only after your application has loaded on the client, even though this is an anti-pattern per NextJS.

You can use patch-package to patch the RUM package to not call window.fetch on the server.

I forget which path I ended up going.

quisido avatar Jul 02 '23 17:07 quisido

When initialized, the web client (1) performs steps to get credentials, and (2) emits a session start event. I believe both of these events can be deferred.

Regarding (2), initialize the web client in the disabled state by setting enabled: false in the config. The web client will not dispatch events when disabled.

This setting is currently missing from the docs, which we should fix. The use of this setting is demonstrated in this blog.

Regarding (1), omit the guestRoleArn and identityPoolId from the config. When the application is ready, use the setAwsCredentials command to pass the credential provider.

We may want to do something so that credentials are not fetched when the web client is disabled, in which case the application need only flip the web client from disabled to enabled when it is ready.

qhanam avatar Jul 14 '23 22:07 qhanam

Please re-open if this doesn't work or if you have thoughts on a better solution.

Meanwhile, we'll add these to the backlog.

qhanam avatar Jul 14 '23 22:07 qhanam

I'll give those a try. As a placeholder, I've seen as a common pattern for SSR+CSR packages to use if (typeof window === 'undefined') to determine if they are rendering on a client or server. This pattern may work for you if you want RUM to no-op on a server render: only call window.fetch if window is defined.

quisido avatar Jul 14 '23 22:07 quisido

So to clarify, when rendering on a server (i.e., no window object), you expect all calls to the web client to be no-ops?

qhanam avatar Jul 14 '23 22:07 qhanam

So to clarify, when rendering on a server (i.e., no window object), you expect all calls to the web client to be no-ops?

Yes, server-rendering serves primarily to generate the initial DOM. Once passed to the client, React will "hydrate," re-executing the render to (1) build the virtual DOM into memory [since the actual DOM is already present] and (2) execute any non-DOM side effects. For example, instantiating RUM and monitoring web vitals does not impact the initial DOM or view, so this behavior does not need to occur during the initial server render. There's no value in having a RUM object in server memory, but once the application re-hydrates on the client, RUM can instantiate in memory and perform browser-based actions like fetching and web vitals.

For a server-side rendered application, the application code is executed once on the server to build the initial DOM and again on the client to hydrate the application into browser memory. By saying "only perform this actions when the window object exists," it will no-op on the server, but it will still execute in the browser during the hydration cycle of the application, as that same JavaScript will re-execute, however now in an environment where window exists.

On the server (or any environment where window does not exist), I don't think it makes sense to call a metric "real user monitoring," because it would not be emit by a user -- it could be a server or a test runner in case of unit testing. No-op makes sense to me to prevent machine-generated metrics from being included as "real users," as there is value in having machines execute the RUM runtime code while traversing code paths of the parent application.

For power user inversion of control, you could perhaps support fetch as a parameter, but I'm really scope-creeping with this.

class RUM {
  private _fetch;
  constructor({ fetch = window.fetch }) {
    this._fetch = fetch;
  }
}

// or

function defaultGetIdentityPool() {
  if (typeof window === 'undefined') {
    return;
  }
  return window.fetch('...', ...);
}

function defaultPostEvent() {
  if (typeof window === 'undefined') {
    return;
  }
  return window.fetch('...', ...);
}

class RUM {
  private _getIdentityPool;
  private _postEvent;
  constructor({
    getIdentityPool = defaultGetIdentityPool,
    postEvent = defaultPostEvent,
  }) {
    this._getIdentityPool = getIdentityPool
    this._postEvent = postEvent;
  }
}

// User story:
// As a test runner, I want to mock and monitor RUM.
const TEST_POST_EVENT = jest.fn();
new RUM({
  postEvent: TEST_POST_EVENT,
});

describe('my application', () => {
  it('should emit web vitals', () => {
    myApplication();
    expect(TEST_POST_EVENT).toHaveBeenCalledWith(...); // <-- web vitals
  });

  it('should emit my.custom.event', () => {
    myApplication();
    someUserAction();
    expect(TEST_POST_EVENT).toHaveBeenCalledWith(...); // <-- my.custom.event
  });
});

Again, that's creep-scope, but I think it would help testability. No-op is a great starting point that's compatible with the above stretch goal.

Thanks for the attention to the issue. 🙂

quisido avatar Jul 17 '23 03:07 quisido

Agree with @CharlesStover here. Would be great to have a mock/stub implementation of the RUM library.

Not only for SSR purposes but also for testability.

A great example of this mocking ability is Undici:

https://undici.nodejs.org/#/docs/api/MockClient

This greatly simplifies testing, and don't need to monkey patch or use jest.mock.

moltar avatar Sep 26 '23 11:09 moltar

@moltar If you are using React, my package aws-rum-react comes with a MockAwsRumProvider that you can wrap your test in to mock implementation of the RUM library. It still doesn't support SSR per se, but it can help with unit tests. :)

quisido avatar Oct 05 '23 17:10 quisido

Core functionality of RUM for SSR website is not working to capture http error.

  1. Upgraded the RUM version from 1.18.0 to 1.19.0
  2. I would like to clarify that the Performance API focuses on performance-related metrics, which don't meet my request. I only want to track HTTP and error metrics. But still, I tried implementing the script you suggested for the Performance API, but unable to catch the 403 error.
  3. I've increased the priority of the cwr.js file, so now it's loaded before the generated JS file. However, we are still experiencing issues with session and not catching HTTP errors.
  4. performance telemetry also not able to capture 4xx and 5XX error
Screenshot 2024-08-22 at 2 38 52 PM Screenshot 2024-08-22 at 2 38 52 PM

sonu-gameskraft avatar Aug 28 '24 05:08 sonu-gameskraft