# Setup Honeycomb

To monitor application performance, Workleap has adopted Honeycomb, a tool that helps teams manage and analyze telemetry data from distributed systems. Built on OpenTelemetry, Honeycomb provides a robust API for tracking frontend telemetry.

Honeycomb's in-house HoneycombWebSDK includes great default instrumentation. However, this instrumentation has to be extended to capture traces specific to Squide features. To facilitate this, Squide provides the registerHoneycombInstrumentation function.

# Setup the host application

Let's start by configuring the host application. First, open a terminal at the root of the host application and install the following packages:

pnpm add @squide/firefly-honeycomb @honeycombio/opentelemetry-web @opentelemetry/api @opentelemetry/auto-instrumentations-web
yarn add @squide/firefly-honeycomb @honeycombio/opentelemetry-web @opentelemetry/api @opentelemetry/auto-instrumentations-web
npm install @squide/firefly-honeycomb @honeycombio/opentelemetry-web @opentelemetry/api @opentelemetry/auto-instrumentations-web

# Register instrumentation

Then, update the host application bootstrapping code to register Honeycomb instrumentation:

host/src/bootstrap.tsx
import { ConsoleLogger, RuntimeContext, FireflyRuntime, bootstrap, type RemoteDefinition } from "@squide/firefly";
import { registerHoneycombInstrumentation } from "@squide/firefly-honeycomb";
import { register as registerMyLocalModule } from "@sample/local-module";
import { createRoot } from "react-dom/client";
import { App } from "./App.tsx";
import { registerHost } from "./register.tsx";

const Remotes: RemoteDefinition[] = [
    { name: "remote1" }
];

const runtime = new FireflyRuntime({
    loggers: [x => new ConsoleLogger(x)]
});

await bootstrap(runtime, {
    localModules: [registerHost, registerMyLocalModule],
    remotes: Remotes
});

// Register Honeycomb instrumentation.
registerHoneycombInstrumentation(runtime, "squide-sample", [/.+/g,], {
    endpoint: "https://squide-collector"
});

const root = createRoot(document.getElementById("root")!);

root.render(
    <RuntimeContext.Provider value={runtime}>
        <App />
    </RuntimeContext.Provider>
);

With instrumentation in place, a few traces are now available 👇

# Bootstrapping flow

The performance of an application bootstrapping flow can now be monitored:

Bootstrapping flow performance
Bootstrapping flow performance

# Deferred registration update

When a deferred registration is updated, the performance of the operation can be monitored:

Deferred registration update performance
Deferred registration update performance

# Fetch requests

Individual fetch request performance can be monitored from end to end:

Fetch instrumentation
Fetch instrumentation

# Document load

The loading performance of the DOM can be monitored:

Document load instrumentation
Document load instrumentation

# Unmanaged error

When an unmanaged error occurs, it's automatically recorded:

Recorded error
Recorded error

# Real User Monitoring (RUM)

The default instrumentation will automatically track the appropriate metrics to display RUM information:

Largest Contentful Paint
Largest Contentful Paint

Cumulative Layout Shift
Cumulative Layout Shift

Interaction to Next Paint
Interaction to Next Paint

# Set custom user attributes

Most application needs to set custom attributes on traces about the current user environment. To help with that, Squide expose the setGlobalSpanAttributes function.

Update your host application to include the setGlobalSpanAttributes function:

host/src/App.tsx
import { AppRouter, useProtectedDataQueries, useIsBootstrapping } from "@squide/firefly";
import { setGlobalSpanAttributes } from "@squide/firefly-honeycomb";
import { useEffect } from "react";
import { RouterProvider, createBrowserRouter, Outlet } from "react-router-dom";
import { SessionManagerContext, ApiError, isApiError, type Session } from "@sample/shared";
import { useSessionManagerInstance } from "./sessionManager.ts";

function BootstrappingRoute() {
    const [session] = useProtectedDataQueries([
        {
            queryKey: ["/api/session"],
            queryFn: async () => {
                const response = await fetch("/api/session");

                if (!response.ok) {
                    throw new ApiError(response.status, response.statusText);
                }

                const data = await response.json();

                const result: Session = {
                    user: {
                        id: data.id,
                    }
                };

                return result;
            }
        }
    ], error => isApiError(error) && error.status === 401);

    useEffect(() => {
        if (session) {
            // Update telemetry global attributes.
            setGlobalSpanAttributes({
                "app.user_id": session.user.id
            });
        }
    }, [session])

    const sessionManager = useSessionManagerInstance(session!);

    if (useIsBootstrapping()) {
        return <div>Loading...</div>;
    }

    return (
        <SessionManagerContext.Provider value={sessionManager}>
            <Outlet />
        </SessionManagerContext.Provider>
    );
}

export function App() {
    return (
        <AppRouter 
            waitForMsw
            waitForProtectedData
        >
            {({ rootRoute, registeredRoutes, routerProviderProps }) => {
                return (
                    <RouterProvider
                        router={createBrowserRouter([
                            {
                                element: rootRoute,
                                children: [
                                    {
                                        element: <BootstrappingRoute />,
                                        children: registeredRoutes
                                    }
                                ]
                            }
                        ])}
                        {...routerProviderProps}
                    />
                );
            }}
        </AppRouter>
    );
}

Now, every trace recorded after the session initialization will include the custom attributes app.user_id:

Custom attributes
Custom attributes

# Custom traces

Squide does not provide a proprietary API for traces. Applications are expected to use the OpenTelemetry API to send custom traces to Honeycomb:

host/src/Page.tsx
import { useEffect } from "react";
import { trace } from "@opentelemetry/api";

const tracer = trace.getTracer("my-tracer");

export function Page() {
    useEffect(() => {
        // OK, this is a pretty bad example.
        const span = tracer.startSpan("my-span");
        span.end();
    }, []);

    return (
        <div>Hello from a page!</div>
    );
}

# Try it 🚀

Start the application in a development environment using the dev script. Render a page, then navigate to your Honeycomb instance. Go to the "Query" page and type name = squide-bootstrapping into the "Where" input. Run the query, select the "Traces" tab at the bottom of the page and view the detail of a trace. You should view the performance of your application bootstrapping flow.

# Troubleshoot issues

If you are experiencing issues with this guide:

  • Set the runtime mode to development mode or register the Honeycomb instrumentation in debug.
  • Open the DevTools console. You'll see a log entry for every for each dispatched event, along with multiple console outputs from Honeycomb's SDK. Squide's bootstrapping instrumentation listens to events to send Honeycomb traces. Most events should match an Honeycomb trace and vice versa.
    • [squide] Dispatching event "squide-local-modules-registration-completed"
    • [squide] Dispatching event "squide-remote-modules-registration-completed"
    • [squide] Dispatching event "squide-public-data-fetch-started"
    • [squide] Dispatching event "squide-public-data-ready"
    • @honeycombio/opentelemetry-web: Honeycomb link: ...
  • Refer to a working example on GitHub.
  • Refer to the troubleshooting page.