APM Tips

Application Insights

Tracking users and sessions in API apps

Updated · 6 min read · By APM Tips editorial team

Application Insights fills user_Id and session_Id from the ai_user and ai_session cookies that its JavaScript SDK sets in the browser, so a pure API called by mobile apps, other services or scripts has none and both columns stay empty. Set them yourself: in the classic SDK 2.x with a telemetry initializer that reads the caller's claims and a session header, and with OpenTelemetry by setting enduser.id (user_AuthenticatedId) and, in Java and Node.js, enduser.pseudo.id (user_Id) on the request span. Use opaque IDs, never names or email addresses.

The Users, Sessions and Cohorts views, the “users affected” count on a failure and every dcount(user_Id) query depend on two context fields that an API doesn’t fill in by itself. That’s not a bug in the API: the browser SDK simply isn’t there to provide them.

Where user and session IDs come from

Every telemetry item can carry three identity fields:

FieldWorkspace columnMeaningDefault source
user_IdUserIdAnonymous user IDai_user cookie set by the JavaScript SDK
user_AuthenticatedIdUserAuthenticatedIdKnown, signed-in usersetAuthenticatedUserContext in the JavaScript SDK
session_IdSessionIdSession IDai_session cookie set by the JavaScript SDK

In a server-rendered site with the JavaScript SDK on its pages, the browser sends those cookies with every request and the server SDK copies them onto request telemetry. That’s what the classic UserTelemetryInitializer, SessionTelemetryInitializer and AuthenticatedUserIdTelemetryInitializer do. Microsoft’s data model also uses session IDs to measure user impact: the number of distinct sessions with failed calls or errors.

An API called by a mobile app, a single-page app on another domain, a partner’s service or a script has no such cookies. The fields stay empty, the usage views stay empty, and failure counts can’t say how many people were hit.

Classic SDK 2.x

In the .NET SDK 2.x (deprecated, retiring March 31, 2027), set the fields in an initializer that reads the current request. This one takes the user from the token’s subject claim and the session from a header your clients agree to send:

using System.Security.Claims;
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.AspNetCore.Http;

public class ApiUserInitializer : ITelemetryInitializer
{
    private readonly IHttpContextAccessor _http;
    public ApiUserInitializer(IHttpContextAccessor http) => _http = http;

    public void Initialize(ITelemetry telemetry)
    {
        var ctx = _http.HttpContext;
        if (ctx is null) return;

        var subject = ctx.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        if (!string.IsNullOrEmpty(subject) && string.IsNullOrEmpty(telemetry.Context.User.AuthenticatedUserId))
            telemetry.Context.User.AuthenticatedUserId = subject;

        var session = ctx.Request.Headers["X-Session-Id"].ToString();
        if (!string.IsNullOrEmpty(session) && string.IsNullOrEmpty(telemetry.Context.Session.Id))
            telemetry.Context.Session.Id = session;
    }
}

Register it after AddApplicationInsightsTelemetry() with AddHttpContextAccessor() and AddSingleton<ITelemetryInitializer, ApiUserInitializer>(). Because initializers run on every item, the dependencies, traces and exceptions inside the request get the same values, so “which users hit this exception” works too.

Two details from Microsoft’s guidance apply. The authenticated ID doesn’t need to be a sign-in name, only unique per user, and if it also passes through the JavaScript SDK it must not contain spaces or the characters ,;=|. And for apps that do serve pages, ASP.NET Core’s EnableAuthenticationTrackingJavaScript option injects the signed-in user name into the JavaScript SDK automatically; single-page apps still call setAuthenticatedUserContext themselves.

Today: SDK 3.x and the Microsoft OpenTelemetry Distro

SDK 3.x removed the user and session initializers. For classic ASP.NET their work moved into internal processors; for ASP.NET Core, Microsoft says there’s no cookie-based replacement for the session initializer. The context properties survive, though: Context.User.Id, AuthenticatedUserId, AccountId and Context.Session.Id are public on TelemetryClient.Context and on individual items. Client-level context applies to traces and logs, not metrics:

var evt = new EventTelemetry("OrderPlaced");
evt.Context.User.AuthenticatedUserId = subject;
evt.Context.Session.Id = sessionId;
telemetryClient.TrackEvent(evt);

For requests and dependencies that OpenTelemetry instrumentation records automatically, use a span processor, the same as with the distro.

OpenTelemetry has standard attributes for the user. Microsoft documents the mapping in its Java and Node.js examples: enduser.id becomes user_AuthenticatedId and enduser.pseudo.id becomes user_Id. The .NET and Python examples set enduser.id. In .NET, set it on the server span:

using System.Diagnostics;
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using OpenTelemetry;

public class EndUserProcessor(IHttpContextAccessor http) : BaseProcessor<Activity>
{
    public override void OnEnd(Activity activity)
    {
        if (activity.Kind != ActivityKind.Server) return;
        var subject = http.HttpContext?.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        if (subject is not null) activity.SetTag("enduser.id", subject);
    }
}

Register it with ConfigureOpenTelemetryTracerProvider((sp, t) => t.AddProcessor(new EndUserProcessor(sp.GetRequiredService<IHttpContextAccessor>()))) before the distro, and add AddHttpContextAccessor(). Node.js sets the same attributes on the active span with span.setAttribute("enduser.id", ...); Python sets enduser.id in a span processor.

Sessions are the gap. Microsoft’s OpenTelemetry pages don’t document an attribute that maps to session_Id. A session value you set as a span attribute arrives in customDimensions, which is enough for KQL but not for the portal’s session views.

Decide what a session means for your API

Browsers have an obvious session: the ai_session cookie. An API has to agree one with its clients, and the choice matters more than the code:

  • Client-created session ID. A mobile or desktop client creates a random ID when it starts (or after a period of inactivity) and sends it on every call in a header such as X-Session-Id. This matches the browser model most closely and is the easiest to reason about in queries.
  • Installation or device ID as the anonymous user. A random ID stored once per installation fits user_Id (or enduser.pseudo.id), and lets you count users who never sign in.
  • No session at all. For server-to-server callers, a session is often meaningless. Leave it empty and identify the calling service by its own cloud role name instead.

Whatever you pick, validate it on the way in: accept only the format you expect, cap its length, and ignore anything else. These values are copied onto every item in the operation, so a careless client can fill your tables with noise.

Browser front end, API back end

When a single-page app uses the JavaScript SDK and calls an API on the same site, the browser sends the ai_user and ai_session cookies with those calls and the classic server SDK picks them up. When the API lives on another origin, the cookies usually don’t travel with the request, so the API sees nothing. Pass the values you need in headers the API reads, as above, and register the header in the API’s CORS policy.

Privacy

Microsoft’s instruction is short: consult applicable privacy laws before you set the authenticated user ID. In practice:

  • Use the identity provider’s opaque subject ID, not an email address, user name or phone number.
  • Keep session IDs random and short-lived; don’t reuse a token or anything that grants access.
  • Remember the values flow to every item in the operation, including logs and exceptions, and stay for the table’s retention period.

Query users and sessions

requests
| where timestamp > ago(7d)
| where isnotempty(user_AuthenticatedId)
| summarize users = dcount(user_AuthenticatedId), sessions = dcount(session_Id) by bin(timestamp, 1d)

Users affected by failures in the last day:

requests
| where timestamp > ago(1d) and tostring(success) =~ "false"
| summarize affected_users = dcount(user_AuthenticatedId) by name
| order by affected_users desc

With sampling on, these are estimates: sampling keeps whole operations, so users with only a few requests in the period are more likely to be missed. In the workspace, use UserAuthenticatedId, SessionId and AppRequests.

Troubleshooting

SymptomLikely causeFix
user_Id and session_Id empty for API callsNo ai_user/ai_session cookies on API requestsSet them in an initializer or processor
Only requests have the user, not dependencies (2.x)Value set on the request object onlyUse an initializer so every item gets it
User set, but not on logs (OpenTelemetry)Span processor onlyAdd a log record processor, or rely on the request join by operation_Id
Email addresses visible in user_AuthenticatedIdSign-in name used as the IDSwitch to the subject claim; purge or let retention expire the old data

Queries for this topic

From the KQL library: explained queries for the Application Insights tables, ready to copy.

  • Daily active users and sessions

    requestspageViews usage

    Returns: One point per UTC day for the last 30 complete days with the number of distinct user IDs and distinct session IDs seen on requests and page views.

Frequently asked questions

What is the difference between user_Id and user_AuthenticatedId?

user_Id is an anonymous identifier, set from the ai_user cookie in browser scenarios. user_AuthenticatedId is a known identifier for a signed-in user. OpenTelemetry maps enduser.id to user_AuthenticatedId and enduser.pseudo.id to user_Id, per Microsoft's Java and Node.js examples.

Is there an OpenTelemetry attribute for session_Id?

Microsoft's OpenTelemetry documentation doesn't name one. A session value you set as a span attribute lands in customDimensions, which works for queries but not for the portal's session views. SDK 3.x still lets you set Context.Session.Id on items you track yourself.

Can I put the user's email address in user_AuthenticatedId?

You can, but don't. Microsoft says to consult applicable privacy laws before setting the authenticated user ID, and notes it doesn't need to be the sign-in name, only unique per user. An opaque ID from your identity provider's subject claim does the job.

Sources, checked

  1. Monitor .NET and Node.js applications with Application Insights (Classic API 2.x)learn.microsoft.com · checked
  2. Add and modify OpenTelemetry in Application Insights (Set the user ID)learn.microsoft.com · checked
  3. Migration guidance: Application Insights 2.x to 3.x (telemetry context)github.com · checked
  4. Application Insights telemetry data modellearn.microsoft.com · checked
  5. AppRequests table referencelearn.microsoft.com · checked
  6. Telemetry sampling in Azure Application Insights (Classic API)learn.microsoft.com · checked