aws-observability / aws-rum-web

Amazon CloudWatch RUM Web Client
Apache License 2.0
117 stars 65 forks source link

[Bug]: Fetch plugin fetches multiple identities before starting #406

Closed hannesj closed 1 year ago

hannesj commented 1 year ago

Which web client version did you detect this bug with?

v1.13.6

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

Remix v1.14.3, Typescript v5.0.4, ECMAScript modules (ESM) and React v18.2.0, aws-rum-web installed as JavaScript Module

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

SPA

Please provide your web client configuration

{
  "allowCookies":true,
  "enableXRay":true,
  "endpoint":"https://dataplane.rum.eu-north-1.amazonaws.com",
  "guestRoleArn":"arn:aws:iam::account:role/stage-stack-App-AppMonitorUnauthentic1O1WJSN77WOS0",
  "identityPoolId":"eu-north-1:4447b5b0-ebdd-4a73-ace3-ede974be41c3",
  "sessionSampleRate":1,
  "telemetries":[
    "errors",
    "performance",
    ["http", { "addXRayTraceIdHeader": true }]
  ]
}

Please describe the bug/issue

There seems to be some kind of race condition, if the application is installed as a JS module, whereby multiple identities are loaded when starting up the application. I believe this should happen only once. If the script is injected via the script tag, it works as expected with the same config.

image

image

adebayor123 commented 1 year ago

Hi, can you please show how you initialize the web client in your application? I've seen this happen with users who initialize the web client based on certain conditions, such as clicking on a cart button.

hannesj commented 1 year ago

I initialize it in the root.tsx in a remix.run application as follows:

import { cssBundleHref } from "@remix-run/css-bundle";
import type { LinksFunction, LoaderArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import {
  Links,
  LiveReload,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
  useLoaderData,
  useLocation,
} from "@remix-run/react";
import { AwsRum } from "aws-rum-web";
import { useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import { useChangeLanguage } from "remix-i18next";

import i18next, { cookie } from "./i18next.server";
import styles from "./index.css";

export const links: LinksFunction = () => {
  return [
    ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []),
    {
      rel: "stylesheet",
      href: styles,
    },
  ];
};

export async function loader({ request }: LoaderArgs) {
  const locale = await i18next.getLocale(request);
  const awsRumConfig = {
    applicationId: process.env.RUM_ID,
    applicationVersion: "0.0.0",
    applicationRegion: process.env.RUM_REGION,
    config: {
      endpoint: process.env.RUM_ENDPOINT,
      guestRoleArn: process.env.RUM_GUEST_ROLE_ARN,
      identityPoolId: process.env.RUM_IDENTITY_POOL_ID,
    },
  };

  return json(
    { locale, awsRumConfig },
    { headers: { "Set-Cookie": await cookie.serialize(locale) } },
  );
}

export const handle = {
  i18n: "app",
};

export default function Root() {
  const { locale, awsRumConfig } = useLoaderData<typeof loader>();
  const { i18n } = useTranslation();

  useChangeLanguage(locale);

  const awsRum = useRef(
    typeof document === "undefined"
      ? undefined
      : new AwsRum(
          awsRumConfig.applicationId ?? "",
          awsRumConfig.applicationVersion,
          awsRumConfig.applicationRegion ?? "",
          {
            ...awsRumConfig.config,
            allowCookies: true,
            enableXRay: true,
            sessionSampleRate: 1,
            telemetries: ["errors", "performance", ["http", { addXRayTraceIdHeader: true }]],
          },
        ),
  );

  const location = useLocation();
  useEffect(() => {
    awsRum.current?.recordPageView(location.pathname);
  }, [location.pathname]);

  return (
    <html lang={locale} dir={i18n.dir()}>
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width,initial-scale=1" />
        <title>Returns by Renow</title>
        <link rel="preconnect" href="https://fonts.googleapis.com" />
        <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
        <link href="https://fonts.googleapis.com/css2?family=Lato:wght@400;700;900&display=swap" rel="stylesheet"/>
        <Meta />
        <Links />
      </head>
      <body>
        <Outlet />
        <ScrollRestoration />
        <Scripts />
        <LiveReload />
      </body>
    </html>
  );
}
adebayor123 commented 1 year ago

Is the root.tsx ever invoked more than once? Or to be exact, is the function Root() ever invoked more than once? Based on what you have, you initialize the web client and then call recordPageView.

But at the same time, I assume when you embed the web client, it's inside the html code in the return statement - is this correct?

qhanam commented 1 year ago

There could be a race condition. When the credentials provider is invoked, it does not check if there is an existing request in flight. However, I'm not sure why the credential provider would be invoked multiple times.

If I recall correctly, the web client does a pre-fetch of credentials at startup, and then if needed (i.e., when there is no credential in memory or localStorage) whenever the web client needs to dispatch events.

As @adebayor123 said, this could occur if multiple instances of the web client are being created. This would be the case if React calls Root() more than once (i.e., to re-render the page when the state changes).