grafana / faro-web-sdk

The Grafana Faro Web SDK, part of the Grafana Faro project, is a highly configurable web SDK for real user monitoring (RUM) that instruments browser frontend applications to capture observability signals. Frontend telemetry can then be correlated with backend and infrastructure data for full-stack observability.
https://grafana.com/oss/faro/
Apache License 2.0
739 stars 64 forks source link

Cannot read properties of undefined (reading 'pushError') #573

Closed FutureKode closed 5 months ago

FutureKode commented 5 months ago

Description

Trying to replace my ErrorBoundary with FaroErrorBoundar, this error appears when the boundary is hit:

Cannot read properties of undefined (reading 'pushError')

Steps to reproduce

  1. Add Faro to the project
  2. Replace ErrorBoundary with FaroErrorBoundary

Expected behavior

It should work as intended.

Actual behavior

Fails with error message

Screenshot 2024-04-30 at 11 26 37

Environment

"@grafana/faro-react": "^1.7.0", "@grafana/faro-web-sdk": "^1.7.0", "@grafana/faro-web-tracing": "^1.7.0", MacBook Air macOS Sonoma Chrome 124

Demo

Context

codecapitano commented 5 months ago

Thanks for reporting @FutureKode It's the first time seeing this error/

Would you mind sharing your Faro init code?

FutureKode commented 5 months ago

Thanks for looking @codecapitano

faro init:

import {
  ExceptionEvent,
  getWebInstrumentations,
  initializeFaro,
  LogEvent,
  LogLevel,
  TraceEvent,
  TransportItemType,
} from "@grafana/faro-web-sdk";
import { TracingInstrumentation } from "@grafana/faro-web-tracing";
import type { IKeyValue } from "@opentelemetry/otlp-transformer";

import env from "@/env";
import { getLogRocketSessionUrl } from "@/utils/logrocketUtil";

import { storage } from "../../../utils/localStorageUtils";

export const initFaro = () => {
  const faroCollectorUrl = `https://faro-collector-prod-eu-west-0.grafana.net/collect/${env.FARO_KEY}`;

  const enrichResourceWithAttributes = new Map([
    ["deployment.environment", env.ROGER_ENV],
    ["service.namespace", "roger"],
  ]);

  console.log("env", env);

  const cluster = env.ROGER_ENV?.replace("production", "prod");
  const allowedHosts = [env.API_URL_PUBLIC, env.OIDC_ENDPOINT].map(
    (url) => new URL(url).host,
  );

  return initializeFaro({
    url: faroCollectorUrl,
    beforeSend: (data) => {
      if (window.location.pathname.includes("/auth")) {
        return null;
      }

      const logRocketUrl = getLogRocketSessionUrl() ?? "n/a";

      switch (data.type) {
        case TransportItemType.EXCEPTION:
          enrichExceptions();
          break;
        case TransportItemType.LOG:
          enrichLogs();
          break;
        case TransportItemType.TRACE:
          filterTraces();
          enrichTraces();
          break;
        default:
          break;
      }

      return data;

      function enrichExceptions() {
        const exceptionEvent = data.payload as ExceptionEvent;

        if (!exceptionEvent.context) {
          // eslint-disable-next-line functional/immutable-data
          exceptionEvent.context = {};
        }
        // eslint-disable-next-line functional/immutable-data
        exceptionEvent.context["logrocket_url"] = logRocketUrl;
      }

      function enrichLogs() {
        const logEvent = data.payload as LogEvent;

        if (logEvent.level === LogLevel.ERROR) {
          // eslint-disable-next-line functional/immutable-data
          logEvent.context["logrocket_url"] = logRocketUrl;
        }
      }

      function enrichTraces() {
        const traceEvent = data.payload as TraceEvent;

        const enrichSpanWithAttributes = new Map([
          ["cluster", cluster],
          ["team.id", storage.team.getActiveTeamId()],
          ["user.id", storage.user.getUserId()],
          ["expense.id", getExpenseId()],
        ]);

        traceEvent.resourceSpans?.forEach((resourceSpan) => {
          const attributes = resourceSpan?.resource?.attributes;
          // eslint-disable-next-line functional/prefer-readonly-type
          const resourceAttributes = attributes as IKeyValue[];

          enrichResourceAttributes();
          enrichSpanAttributes();

          function enrichSpanAttributes() {
            resourceSpan.scopeSpans?.forEach((scopeSpan) => {
              scopeSpan.spans?.forEach((span) => {
                // eslint-disable-next-line functional/prefer-readonly-type
                const spanAttributes = span.attributes as IKeyValue[];
                enrichSpanWithAttributes.forEach((value, key) => {
                  const attribute = spanAttributes.find((x) => x.key === key);

                  if (attribute || !value) {
                    return;
                  }
                  // eslint-disable-next-line functional/immutable-data
                  spanAttributes.push({
                    key,
                    value: {
                      stringValue: value,
                    },
                  });
                });
              });
            });
          }

          function enrichResourceAttributes() {
            enrichResourceWithAttributes.forEach((value, key) => {
              const attribute = resourceAttributes.find((x) => x.key === key);
              if (attribute || !value) {
                return;
              }
              // eslint-disable-next-line functional/immutable-data
              resourceAttributes.push({
                key,
                value: {
                  stringValue: value,
                },
              });
            });
          }
        });

        function getExpenseId(): string | undefined {
          const regex = /\/expenses\/([a-zA-Z0-9]+)/;
          const match = window.location.pathname.match(regex);

          if (match && match[1]) {
            const expenseId = match[1];
            return expenseId;
          }

          return storage.expense.getExpenseId();
        }
      }

      function filterTraces() {
        const traceEvent = data.payload as TraceEvent;

        traceEvent.resourceSpans?.forEach((resourceSpan) => {
          resourceSpan.scopeSpans?.forEach((scopeSpan) => {
            const filteredSpans = scopeSpan.spans?.filter((span) => {
              // eslint-disable-next-line functional/prefer-readonly-type
              const spanAttributes = span.attributes as IKeyValue[];

              const ignore = spanAttributes.find(
                (a) =>
                  a.key === "http.host" &&
                  a.value.stringValue &&
                  !allowedHosts.includes(a.value.stringValue),
              );
              return !ignore;
            });

            // eslint-disable-next-line functional/immutable-data
            scopeSpan.spans = filteredSpans;
          });
        });
      }
    },
    app: {
      name: "web",
      version: "1.0.1",
      environment: env.ROGER_ENV,
    },
    instrumentations: [
      // mandatory, overwriting the instrumentations array would cause the default instrumentations to be omitted
      ...getWebInstrumentations({
        captureConsole: true,
        captureConsoleDisabledLevels: [
          LogLevel.DEBUG,
          LogLevel.INFO,
          LogLevel.LOG,
        ],
        enablePerformanceInstrumentation: false,
      }),

      // initialization of the tracing package.
      // this packages is optional because it increases the bundle size noticeably. Only add it if you want tracing data.
      new TracingInstrumentation({
        instrumentationOptions: {
          // requests to these URLs will have tracing headers attached.
          propagateTraceHeaderCorsUrls: allowedHosts.map(
            (host) => new RegExp(`^.*${host}.*$`),
          ),
        },
      }),
    ],
  });
};

Error boundary:

    <FaroErrorBoundary onError={onError} fallback={<ErrorBoundaryUI type={type} />}>
      {children}
    </FaroErrorBoundary>
codecapitano commented 5 months ago

@FutureKode Config looks good so far. When do you initialize Faro? Is it initialized and available before the React App?

Here's an example form the Faro Demo app

https://github.com/grafana/faro-web-sdk/blob/b06dd9c3b974d3d9103f7a045d1e86207ba007d0/demo/src/client/index.tsx#L14

codecapitano commented 5 months ago

Maybe one other thing to try.

You mentioned the following dependencies: "@grafana/faro-react": "^1.7.0", "@grafana/faro-web-sdk": "^1.7.0", "@grafana/faro-web-tracing": "^1.7.0",

When you use faro-react you only need to install "@grafana/faro-react": "^1.7.0" and the "@grafana/faro-web-tracing": "^1.7.0", if you want otel tracing.

Can you test if the error goes away if you only install those two packages?

FutureKode commented 5 months ago

@codecapitano Yes faro is initialized like your example, from the app entry point, and I see a console.log from inside the initialisation code.

I tried removing "@grafana/faro-web-sdk" and just using faro-react and faro-web-tracing. But still the error persists 😿

codecapitano commented 5 months ago

Hi @FutureKode I found the problem aka I've overseen this part in the init code.

So the problem is that you still need to manually instantiate the ReactInstrumentation. It's missing in the docs, I'll update them.

Future wise faro-react should do this automatically if no extra config is needed. I'll open an issue.

You need to add this to the instrumentations array in your config: new ReactIntegration()

Cheers Marco

If you want to instrument the React router you do this via the new ReactIntegration() as well.

FutureKode commented 5 months ago

Thanks @codecapitano - that fixed it 👍🏻