Setting User ID In Next.js And TRPC With Azure Application Insights
Introduction
In this article, we will explore how to set up user identification in a Next.js and tRPC web application using Azure Application Insights. This is crucial for gaining insights into user behavior, debugging issues, and understanding application performance in a user-centric manner. We'll cover the setup process, including integrating with your authentication system (Auth.js with a database session strategy), and ensuring that user IDs are correctly tracked within Application Insights using OpenTelemetry.js. Leveraging the power of Next.js for the frontend and tRPC for the backend, with Azure as the cloud provider, this comprehensive guide will help you implement robust user tracking in your T3 web application.
Prerequisites
Before diving into the implementation details, ensure you have the following prerequisites in place:
- A Next.js application: You should have a working Next.js application set up. If you are starting from scratch, you can use
create-next-app
to bootstrap a new project. - tRPC integration: tRPC should be configured in your Next.js application for backend communication. tRPC allows you to build fully typesafe APIs without the need for schemas or code generation.
- Auth.js set up: Authentication should be implemented using Auth.js with a database session strategy. This will provide the user context that we need to track.
- Azure account and Application Insights resource: You need an active Azure subscription and an Application Insights resource created in your Azure portal.
- Basic understanding of OpenTelemetry: Familiarity with OpenTelemetry concepts will be beneficial as we will be using OpenTelemetry.js to send telemetry data to Azure Application Insights.
Setting up Azure Application Insights
To begin, you'll need to configure Azure Application Insights. If you haven't already, create an Application Insights resource in the Azure portal. Once created, you will receive an Instrumentation Key (also known as an iKey), which is a unique identifier for your Application Insights resource. This key is essential for your application to send telemetry data to Azure. You can find the Instrumentation Key in the Overview section of your Application Insights resource in the Azure portal.
Installing Necessary Packages
First, we'll install the required packages for OpenTelemetry and Azure Monitor. OpenTelemetry is an observability framework providing a standard way to generate and collect telemetry data, while Azure Monitor Exporter sends this data to Azure Application Insights. Run the following commands in your project:
npm install @opentelemetry/sdk @opentelemetry/resources @opentelemetry/semantic-conventions @opentelemetry/exporter-azure-monitor @opentelemetry/auto-instrumentations-node @opentelemetry/instrumentation @opentelemetry/instrumentation-fetch
yarn add @opentelemetry/sdk @opentelemetry/resources @opentelemetry/semantic-conventions @opentelemetry/exporter-azure-monitor @opentelemetry/auto-instrumentations-node @opentelemetry/instrumentation @opentelemetry/instrumentation-fetch
These packages include:
@opentelemetry/sdk
: The OpenTelemetry SDK provides the core functionality for telemetry collection.@opentelemetry/resources
: This package helps define and attach resource attributes (like service name) to your telemetry.@opentelemetry/semantic-conventions
: Provides standard attribute names and values for telemetry data.@opentelemetry/exporter-azure-monitor
: The Azure Monitor Exporter sends telemetry data to Azure Application Insights.@opentelemetry/auto-instrumentations-node
: Automatically instruments Node.js applications to collect telemetry data without manual instrumentation.@opentelemetry/instrumentation
: Base package for creating custom instrumentation.@opentelemetry/instrumentation-fetch
: Instrument Fetch API to automatically track requests.
Configuring OpenTelemetry
Create a new file, for example, telemetry.ts
, to configure OpenTelemetry. This file will initialize the OpenTelemetry SDK and set up the Azure Monitor Exporter.
// telemetry.ts
import { NodeSDK } from '@opentelemetry/sdk';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
import { AzureMonitorExporter } from '@opentelemetry/exporter-azure-monitor';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
const resource = new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'nextjs-trpc-app',
[SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0',
});
const exporter = new AzureMonitorExporter({
connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING,
});
const sdk = new NodeSDK({
resource: resource,
traceExporter: exporter,
instrumentations: [
getNodeAutoInstrumentations(),
new FetchInstrumentation(),
],
});
registerInstrumentations({
instrumentations: [
new FetchInstrumentation(),
],
});
sdk.start()
.then(() => console.log('Telemetry initialized'))
.catch((error) => console.log('Error initializing telemetry', error));
export default sdk;
In this configuration:
- We create a
Resource
to define service attributes like service name and version. This helps in identifying the source of telemetry data in Application Insights. - The
AzureMonitorExporter
is configured using the connection string from the environment variables. Make sure to set theAPPLICATIONINSIGHTS_CONNECTION_STRING
environment variable in your.env
file or deployment settings. - We use
getNodeAutoInstrumentations
to automatically instrument common Node.js libraries, andFetchInstrumentation
to monitor fetch API calls. This reduces the need for manual instrumentation. - The SDK is started, which initializes the telemetry pipeline. It's crucial to handle any errors during initialization to prevent issues with telemetry collection.
Initializing Telemetry in Your Application
To initialize telemetry when your application starts, import and initialize the SDK in your main entry point, such as _app.tsx
or _app.js
.
// _app.tsx or _app.js
import '../styles/globals.css';
import type { AppProps } from 'next/app';
import telemetry from '../telemetry';
import { useEffect } from 'react';
function MyApp({ Component, pageProps }: AppProps) {
useEffect(() => {
// Initialize telemetry
telemetry;
// Optional: Dispose of the SDK when the component unmounts
return () => {
telemetry.shutdown();
};
}, []);
return <Component {...pageProps} />;
}
export default MyApp;
Initializing telemetry in _app.tsx
ensures that it starts when your application loads. The useEffect
hook is used to run the initialization code once when the component mounts. Additionally, the optional shutdown call ensures that resources are properly released when the component unmounts.
Setting User ID
Now, let's focus on setting the user ID for each request. This involves accessing the user session and setting the user context in OpenTelemetry spans. We'll need to modify both the frontend and backend to achieve this.
Accessing User Session
First, ensure you can access the user session information in both your Next.js frontend and tRPC backend. With Auth.js, you can use the getSession
function to retrieve the session data. This data typically includes the user's ID, email, and other relevant information.
Frontend (Next.js)
In your Next.js components, you can use the useSession
hook from next-auth/react
to access the session.
// components/UserProfile.tsx
import { useSession } from 'next-auth/react';
import { useEffect } from 'react';
import { trace } from '@opentelemetry/api';
function UserProfile() {
const { data: session } = useSession();
useEffect(() => {
if (session?.user?.id) {
const tracer = trace.getTracer('nextjs-frontend');
const span = tracer.startSpan('UserProfile Component');
span.setAttribute('app.user.id', session.user.id);
span.end();
}
}, [session]);
if (session) {
return (
<div>
<p>Welcome, {session.user?.name}!</p>
{/* Other user profile content */}
</div>
);
}
return <p>Loading...</p>;
}
export default UserProfile;
In this example:
- We use
useSession
to access the session data. - Inside a
useEffect
hook, we check if the session and user ID are available. - If available, we create a new OpenTelemetry span using
trace.getTracer
. This allows us to manually add attributes to the span. - We set the
app.user.id
attribute on the span with the user's ID from the session. This attribute will be sent to Application Insights. - The span is ended after setting the attribute. It's important to end spans to ensure proper telemetry data collection.
Backend (tRPC)
In your tRPC backend, you can access the session in your procedures using the context. The context should be set up to include the session data. Here’s how you can modify your tRPC context to include the session.
// src/server/context.ts
import * as trpc from '@trpc/server';
import * as trpcNext from '@trpc/server/adapters/next';
import { getSession } from 'next-auth/react';
export async function createContext(
opts: trpcNext.CreateNextContextOptions
) {
const session = await getSession(opts);
return {
session,
};
}
type Context = trpc.inferAsyncReturnType<typeof createContext>;
export const createRouter = () => trpc.router<Context>();
export type { Context };
This code snippet shows how to:
- Import necessary modules from tRPC and
next-auth/react
. - Create an asynchronous
createContext
function that retrieves the session usinggetSession
. - Include the session in the context object, which will be available in your tRPC procedures.
- Define a
Context
type for type safety. - Export a
createRouter
function to create tRPC routers with the context.
Setting User ID in tRPC Procedures
Now that the session is available in the context, you can access the user ID in your tRPC procedures and set it as an attribute on the OpenTelemetry span. This ensures that backend operations are also associated with the user.
// src/server/routers/example.ts
import { createRouter } from '../context';
import { trace } from '@opentelemetry/api';
export const exampleRouter = createRouter()
.query('hello', {
resolve({ ctx }) {
const tracer = trace.getTracer('trpc-backend');
const span = tracer.startSpan('example.hello');
if (ctx.session?.user?.id) {
span.setAttribute('app.user.id', ctx.session.user.id);
}
span.end();
return {greeting: `Hello`};
},
});
In this example:
- We import
trace
from@opentelemetry/api
. - Inside the
hello
query procedure, we get the OpenTelemetry tracer and start a new span. - We check if the user ID is available in the session and set it as an attribute on the span.
- The span is ended after setting the attribute.
Propagating User ID
To maintain the user context across the frontend and backend, it's essential to propagate the user ID in HTTP requests. This can be done by adding the user ID as a header in the request and extracting it in the backend.
Frontend (Adding User ID to Headers)
When making requests from the frontend to the backend (e.g., using fetch
or tRPC's client), add the user ID to the request headers.
// utils/api.ts
import { getSession } from 'next-auth/react';
export async function fetchData(url: string, options: RequestInit = {}) {
const session = await getSession();
const userId = session?.user?.id;
const headers = {
...options.headers,
'x-user-id': userId || '',
};
const response = await fetch(url, {
...options,
headers,
});
return response;
}
In this utility function:
- We retrieve the session using
getSession
. - We extract the user ID from the session.
- We add the user ID to the request headers using the
x-user-id
header. - The modified headers are included in the
fetch
request.
Backend (Extracting User ID from Headers)
In your tRPC middleware or context, extract the user ID from the request headers and set it on the active span. This ensures that the user context is maintained throughout the request lifecycle.
// src/server/context.ts
import * as trpc from '@trpc/server';
import * as trpcNext from '@trpc/server/adapters/next';
import { getSession } from 'next-auth/react';
import { IncomingHttpHeaders } from 'http';
import { trace, context, propagation } from '@opentelemetry/api';
interface ExtendedContext {
session: Awaited<ReturnType<typeof getSession>> | null;
userId: string | null;
}
export async function createContext(
opts: trpcNext.CreateNextContextOptions
): Promise<ExtendedContext> {
const session = await getSession(opts);
const userId = getUserIdFromHeader(opts.req.headers);
return {
session,
userId,
};
}
function getUserIdFromHeader(headers: IncomingHttpHeaders): string | null {
const userId = headers['x-user-id'] as string | undefined;
return userId || null;
}
export type Context = trpc.inferAsyncReturnType<typeof createContext>;
export const createRouter = () => trpc.router<Context>();
export type { Context, ExtendedContext };
// src/server/routers/example.ts
import { createRouter } from '../context';
import { trace, context, propagation } from '@opentelemetry/api';
export const exampleRouter = createRouter()
.middleware(async ({ ctx, next }) => {
const userId = ctx.userId;
if (userId) {
const tracer = trace.getTracer('trpc-middleware');
const span = tracer.startSpan('tRPC Middleware');
span.setAttribute('app.user.id', userId);
span.end();
}
return next();
})
.query('hello', {
resolve({ ctx }) {
const tracer = trace.getTracer('trpc-backend');
const span = tracer.startSpan('example.hello');
if (ctx.session?.user?.id) {
span.setAttribute('app.user.id', ctx.session.user.id);
}
span.end();
return { greeting: `Hello` };
},
});
Key points in this setup:
- In
src/server/context.ts
, we define anExtendedContext
interface to include theuserId
. - The
createContext
function now extracts thex-user-id
header usinggetUserIdFromHeader
and includes it in the context. - In
src/server/routers/example.ts
, we add a middleware to extract theuserId
from the context and set it as an attribute on a new span. This ensures that the user ID is tracked for all tRPC procedures.
Viewing User Data in Azure Application Insights
Once you have set up user ID tracking, you can view the data in Azure Application Insights. Go to your Application Insights resource in the Azure portal and navigate to the Search tab. You can then filter events by the app.user.id
attribute to see telemetry data associated with specific users.
You can also use the Users tool in Application Insights to gain insights into user behavior, such as the number of active users, their demographics, and their usage patterns. This provides a more aggregated view of user activity.
Conclusion
Setting user IDs in your Next.js and tRPC web application is essential for effective monitoring and debugging using Azure Application Insights. By integrating OpenTelemetry, you can capture and propagate user context across your frontend and backend, providing a comprehensive view of user interactions. This guide has walked you through the process of configuring OpenTelemetry, accessing user sessions, setting user IDs in spans, propagating user IDs in HTTP requests, and viewing user data in Azure Application Insights. Implementing these steps will empower you to gain deeper insights into your application's performance and user behavior, leading to better user experiences and more efficient troubleshooting.