ApplicationInsights-JS icon indicating copy to clipboard operation
ApplicationInsights-JS copied to clipboard

Same trace_id in traceparent for multiple requests per page

Open Muhammad-Faraz-Ali opened this issue 11 months ago • 6 comments

I'm working on end-to-end transaction and logging my logs on azure monitor application insights

SDKs/Packages:

Frontend (React): "@microsoft/applicationinsights-web": "^3.3.5" Backend (Node): "@azure/monitor-opentelemetry": "^1.8.1"

app-insights.service.ts

import {
  ApplicationInsights,
  DistributedTracingModes,
} from "@microsoft/applicationinsights-web";
import { ReactPlugin } from "@microsoft/applicationinsights-react-js";
import { ClickAnalyticsPlugin } from "@microsoft/applicationinsights-clickanalytics-js";

const CONNECTION_STRING = import.meta.env.VITE_APPINSIGHTS_CONNECTION_STRING;

const reactPlugin = new ReactPlugin();
const clickPluginInstance = new ClickAnalyticsPlugin();
const clickPluginConfig = {
  autoCapture: true,
  urlCollectQuery: true,
};

const configObj = {
  connectionString: CONNECTION_STRING,
  extensions: [reactPlugin, clickPluginInstance],
  autoTrackPageVisitTime: true,
  enableAutoRouteTracking: true,
  enableRequestHeaderTracking: true,
  enableResponseHeaderTracking: true,

  disableCorrelationHeaders: false, // Ensure headers are injected
  distributedTracingMode: DistributedTracingModes.W3C, // Use W3C Trace Context
  enableCorsCorrelation: true,  
  correlationHeaderExcludedDomains: [], 
  // Ensure telemetry isn’t tied to a single session trace
  enableAjaxPerfTracking: true, // Track performance of AJAX requests separately
  extensionConfig: {
    [clickPluginInstance.identifier]: clickPluginConfig,
  },
};

const appInsights = new ApplicationInsights({ config: configObj });

// Load AppInsights early and add telemetry initializer for debugging
appInsights.loadAppInsights();

export { reactPlugin, appInsights };

Component#1 Home.tsx

import React, { useState, useEffect } from "react";
import { Container, Typography, Card, CardContent, CardActions, TextField, Button,   IconButton,   Select,   MenuItem, FormControl, InputLabel, Grid, Box,
} from "@mui/material";
import {
  Delete as DeleteIcon,
  Edit as EditIcon,
  CheckCircle as CheckCircleIcon,
  RadioButtonUnchecked as RadioButtonUncheckedIcon,
} from "@mui/icons-material";
import { appInsights } from "../../logging/appinsights.service"; //appInsights obj
import { SeverityLevel } from "@microsoft/applicationinsights-web";

interface Todo {
  _id: string;
  title: string;
  description: string;
  completed: boolean;
  priority: "low" | "medium" | "high";
  dueDate?: string;
  createdAt: string;
  updatedAt: string;
}

function Home() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [title, setTitle] = useState("");
  const [description, setDescription] = useState("");
  const [priority, setPriority] = useState<"low" | "medium" | "high">("medium");
  const [dueDate, setDueDate] = useState("");
  const [editingId, setEditingId] = useState<string | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  appInsights.trackPageView({ name: "Home" });

  const API_URL = "http://localhost:4001/api/todos";

  useEffect(() => {
    fetchTodos();
  }, []);

  const fetchTodos = async () => {
    appInsights.trackTrace({
      message: "Fetch Todos",
      severityLevel: SeverityLevel.Information,
    });
    try {
      setLoading(true);
      const response = await fetch(API_URL, {
        method: "GET", // or 'POST', 'PUT', 'DELETE', etc.
        headers: {
          "Content-Type": "application/json", // Set content type
          Authorization: "Bearer YOUR_TOKEN", // Attach Bearer token
          "Custom-Header": "CustomValue", // Custom headers
          user_id: "123",
        },
      });
      if (!response.ok) {
        throw new Error("Failed to fetch todos");
      }
      const data = await response.json();
      setTodos(data);
      setError(null);
    } catch (err) {
      setError("Error fetching todos. Please try again.");
      console.error(err);
    } finally {
      setLoading(false);
    }
  };

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    if (!title.trim()) {
      setError("Title is required");
      return;
    }

    try {
      const todoData = {
        title,
        description,
        priority,
        dueDate: dueDate || undefined,
      };

      if (editingId) {
        // Update existing todo
        const response = await fetch(`${API_URL}/${editingId}`, {
          method: "PUT",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(todoData),
        });

        if (!response.ok) {
          throw new Error("Failed to update todo");
        }
      } else {
        // Create new todo
        const response = await fetch(API_URL, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(todoData),
        });

        if (!response.ok) {
          throw new Error("Failed to create todo");
        }
      }

      // Reset form and refresh todos
      setTitle("");
      setDescription("");
      setPriority("medium");
      setDueDate("");
      setEditingId(null);
      setError(null);
      fetchTodos();
    } catch (err) {
      setError("Error saving todo. Please try again.");
      console.error(err);
    }
  };

  const handleDelete = async (id: string) => {
    try {
      const response = await fetch(`${API_URL}/${id}`, {
        method: "DELETE",
      });

      if (!response.ok) {
        throw new Error("Failed to delete todo");
      }

      fetchTodos();
    } catch (err) {
      setError("Error deleting todo. Please try again.");
      console.error(err);
    }
  };


  const handleEdit = (todo: Todo) => {
    setTitle(todo.title);
    setDescription(todo.description);
    setPriority(todo.priority);
    setDueDate(todo.dueDate ? todo.dueDate.split("T")[0] : "");
    setEditingId(todo._id);
  };

  const getPriorityColor = (priority: string) => {
    switch (priority) {
      case "high":
        return "text-red-600";
      case "medium":
        return "text-yellow-600";
      case "low":
        return "text-green-600";
      default:
        return "text-gray-600";
    }
  };

  return (
    <Container sx={{ py: 8 }}>
      <Typography variant="h3" align="center" gutterBottom>
        Todo Application
      </Typography>
      <Button
        onClick={() => fetchTodos()}
        sx={{ mr: 2 }}
        data-content="Fetch todos"
      >
        Fetch Todos
      </Button>

      {error && (
        <Box
          sx={{
            bgcolor: "error.main",
            color: "white",
            p: 2,
            mb: 4,
            borderRadius: 1,
          }}
        >
          {error}
        </Box>
      )}

      <Card sx={{ mb: 8, p: 3 }}>
        <Typography variant="h5" gutterBottom>
          {editingId ? "Edit Todo" : "Add New Todo"}
        </Typography>
        <Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
          <TextField
            fullWidth
            label="Title *"
            value={title}
            onChange={(e) => setTitle(e.target.value)}
            margin="normal"
          />
          <TextField
            fullWidth
            label="Description"
            value={description}
            onChange={(e) => setDescription(e.target.value)}
            margin="normal"
            multiline
            rows={3}
          />
          <Grid container spacing={2} sx={{ mt: 1 }}>
            <Grid item xs={12} md={6}>
              <FormControl fullWidth>
                <InputLabel>Priority</InputLabel>
                <Select
                  value={priority}
                  onChange={(e) =>
                    setPriority(e.target.value as "low" | "medium" | "high")
                  }
                >
                  <MenuItem value="low">Low</MenuItem>
                  <MenuItem value="medium">Medium</MenuItem>
                  <MenuItem value="high">High</MenuItem>
                </Select>
              </FormControl>
            </Grid>
            <Grid item xs={12} md={6}>
              <TextField
                fullWidth
                label="Due Date"
                type="date"
                value={dueDate}
                onChange={(e) => setDueDate(e.target.value)}
                InputLabelProps={{ shrink: true }}
              />
            </Grid>
          </Grid>
          <Box sx={{ display: "flex", justifyContent: "flex-end", mt: 2 }}>
            {editingId && (
              <Button
                onClick={() => {
                  setTitle("");
                  setDescription("");
                  setPriority("medium");
                  setDueDate("");
                  setEditingId(null);
                }}
                sx={{ mr: 2 }}
              >
                Cancel
              </Button>
            )}
            <Button type="submit" variant="contained" color="primary">
              {editingId ? "Update Todo" : "Add Todo"}
            </Button>
          </Box>
        </Box>
      </Card>

      <Card>
        <CardContent>
          <Typography variant="h5" gutterBottom>
            My Todos
          </Typography>
          {loading ? (
            <Typography align="center">Loading todos...</Typography>
          ) : todos.length === 0 ? (
            <Typography align="center" color="textSecondary">
              No todos yet. Add one above!
            </Typography>
          ) : (
            <Box>
              {todos.map((todo) => (
                <Card key={todo._id} sx={{ mb: 2 }}>
                  <CardContent sx={{ display: "flex", alignItems: "center" }}>
                    <IconButton
                      onClick={() =>
                        handleToggleComplete(todo._id, todo.completed)
                      }
                    >
                      {todo.completed ? (
                        <CheckCircleIcon color="success" />
                      ) : (
                        <RadioButtonUncheckedIcon />
                      )}
                    </IconButton>
                    <Box sx={{ flexGrow: 1 }}>
                      <Typography
                        variant="h6"
                        sx={{
                          textDecoration: todo.completed
                            ? "line-through"
                            : "none",
                        }}
                      >
                        {todo.title}
                      </Typography>
                      {todo.description && (
                        <Typography variant="body2" color="textSecondary">
                          {todo.description}
                        </Typography>
                      )}
                      <Box sx={{ display: "flex", gap: 2, mt: 1 }}>
                        <Typography
                          variant="body2"
                          color={getPriorityColor(todo.priority)}
                        >
                          {todo.priority.charAt(0).toUpperCase() +
                            todo.priority.slice(1)}
                        </Typography>
                        {todo.dueDate && (
                          <Typography variant="body2" color="textSecondary">
                            Due: {new Date(todo.dueDate).toLocaleDateString()}
                          </Typography>
                        )}
                        <Typography variant="body2" color="textSecondary">
                          Created:{" "}
                          {new Date(todo.createdAt).toLocaleDateString()}
                        </Typography>
                      </Box>
                    </Box>
                    <CardActions>
                      <IconButton
                        onClick={() => handleEdit(todo)}
                        color="primary"
                      >
                        <EditIcon />
                      </IconButton>
                      <IconButton
                        onClick={() => handleDelete(todo._id)}
                        color="error"
                      >
                        <DeleteIcon />
                      </IconButton>
                    </CardActions>
                  </CardContent>
                </Card>
              ))}
            </Box>
          )}
        </CardContent>
      </Card>
    </Container>
  );
}

export default Home;

Component#2 Catalog.tsx

import React, { useMemo, useState } from "react";
import { useGetSupplierCatalogQuery } from "../../api/catalog/supplier";
import { Table } from "../../components";
import { useUrlState } from "../../hooks/use-url-state/use-url-state.hook";
import {
  Box,
  Card,
  TextField,
  MenuItem,
  Grid,
  Typography,
  Divider,
  Button,
} from "@mui/material";
import { useNavigate } from "react-router-dom";
import CatalogIcon from "@mui/icons-material/LibraryBooks";
import { paths } from "../../router/paths.constants";
import { getFilteredSuppliers } from "./supplier-catalog.utils";
import { columnDefs, unitOfMeasureOptions } from "./supplier-catalog.constants";
import { useAppInsightsContext } from "@microsoft/applicationinsights-react-js";
import { SeverityLevel } from "@microsoft/applicationinsights-web";
import ErrorIcon from "@mui/icons-material/Error";

export const Catalog: React.FC = () => {
  const navigate = useNavigate();
  const appInsights = useAppInsightsContext();
  const [page, setPage] = useUrlState({ name: "page", value: 0 });
  const [pageSize, setPageSize] = useUrlState({ name: "pageSize", value: 5 });
  const [partNumber, setPartNumber] = useState("");
  const [partDescription, setPartDescription] = useState("");
  const [unitOfMeasure, setUnitOfMeasure] = useState("");
  const [classification, setClassification] = useState("");
  const [packageQuantity, setPackageQuantity] = useState<number | string>("");

  const { data, isLoading } = useGetSupplierCatalogQuery({
    page,
    size: pageSize,
  });

  const rows = useMemo(() => {
    if (!data) return [];
    const filteredRows = getFilteredSuppliers(
      data.supplierCatalog,
      partNumber,
      partDescription,
      unitOfMeasure,
      classification,
      packageQuantity,
    );
    return filteredRows;
  }, [
    data,
    partNumber,
    partDescription,
    unitOfMeasure,
    classification,
    packageQuantity,
  ]);

  function trackException() {
    appInsights.trackException({
      error: new Error("some error"),
      severityLevel: SeverityLevel.Error,
    });
  }

  function trackTrace() {
    appInsights.trackTrace({
      message: "some trace",
      severityLevel: SeverityLevel.Information,
    });
  }

  function trackEvent() {
    appInsights.trackEvent({ name: "some event" });
  }

  return (
    <Card>
      <Box p={2}>
        <Box
          display="flex"
          justifyContent="space-between"
          alignItems="center"
          mb={1}
        >
          <Typography variant="h5" fontWeight="bold">
            Supplier Catalog
          </Typography>
          <Button
            variant="outlined"
            size="large"
            sx={{ borderRadius: "16px" }}
            startIcon={<CatalogIcon />}
            onClick={() => navigate(paths.CUSTOMER_CATALOG)}
          >
            Customer Catalog
          </Button>
          <Button
            variant="outlined"
            size="large"
            sx={{ borderRadius: "16px" }}
            startIcon={<CatalogIcon />}
            onClick={trackEvent}
          >
            Track Event
          </Button>
          <Button
            variant="outlined"
            size="large"
            sx={{ borderRadius: "16px" }}
            startIcon={<CatalogIcon />}
            onClick={trackTrace}
          >
            Track Trace
          </Button>
          <Button
            variant="outlined"
            size="large"
            sx={{ borderRadius: "16px" }}
            startIcon={<ErrorIcon />}
            onClick={trackException}
          >
            Track Exception
          </Button>
          <Button
            variant="outlined"
            size="large"
            sx={{ borderRadius: "16px" }}
            startIcon={<ErrorIcon />}
            onClick={() => console.log("hello")}
          >
            Auto click analytics
          </Button>
        </Box>
        <Grid container spacing={2} mt={1}>
          <Grid item xs={12} sm={6} md={4}>
            <TextField
              fullWidth
              size="small"
              label="Part Number"
              value={partNumber}
              onChange={(e) => setPartNumber(e.target.value)}
            />
          </Grid>
          <Grid item xs={12} sm={6} md={4}>
            <TextField
              fullWidth
              size="small"
              label="Part Description"
              value={partDescription}
              onChange={(e) => setPartDescription(e.target.value)}
            />
          </Grid>
          <Grid item xs={12} sm={6} md={4}>
            <TextField
              fullWidth
              size="small"
              select
              label="Unit of Measure"
              value={unitOfMeasure}
              onChange={(e) => setUnitOfMeasure(e.target.value)}
            >
              <MenuItem value="All">All</MenuItem>
              {unitOfMeasureOptions.map((option) => (
                <MenuItem key={option} value={option}>
                  {option}
                </MenuItem>
              ))}
            </TextField>
          </Grid>
          <Grid item xs={12} sm={6} md={4}>
            <TextField
              fullWidth
              size="small"
              label="Classification"
              value={classification}
              onChange={(e) => setClassification(e.target.value)}
            />
          </Grid>
          <Grid item xs={12} sm={6} md={4}>
            <TextField
              fullWidth
              size="small"
              label="Package Quantity"
              type="number"
              value={packageQuantity}
              onChange={(e) =>
                setPackageQuantity(
                  e.target.value === "" ? "" : parseInt(e.target.value, 10),
                )
              }
            />
          </Grid>
        </Grid>
      </Box>
      <Divider />
      <Box p={2}>
        <Table
          columnDefs={columnDefs}
          rows={rows}
          loading={isLoading}
          onPageChange={(newPage) => setPage(newPage)}
          onPageSizeChange={(newPageSize) => setPageSize(newPageSize)}
          initialPageSize={pageSize}
          initialOrderBy={columnDefs[0]}
          page={page}
          startSerial={page * pageSize + 1}
          total={5000}
          isListedPagination
        />
      </Box>
    </Card>
  );
};

Observation/Behavior: When I click on a button (it can hit a HTTP request e.g Home.tsx component case), the traceparent is being attached to every HTTP request, in the traceparent the same trace_id is being used untill I refresh the page or move to another page. (Note: span_id is different as expected) e.g 00-a7d90bf55exxxxxxxxxxxxxx4edec37c-26248dxxxxee486d-01 00-a7d90bf55exxxxxxxxxxxxxx4edec37c-01946ffxxxx34cc5-01

Question: Is this a expected behavior or not? If not what is the solution, otherwise how to use/regenerate new trace_id for every outgoing HTTP request

Technically, For every outgoing request it should use the new trace_id in traceparent

Muhammad-Faraz-Ali avatar Mar 06 '25 12:03 Muhammad-Faraz-Ali

If the page URL does not change then yes.

Basically the enableAutoRouteTracking: true, configuration internally enables an event listener on the browsers history and when this event is fired by the browser we get indirectly informed that the logical "page" changed and we update the "traceId", if this config is not enabled (the default) or the page URL is not changed (including just "#" changes) then this code will not be triggered.

And the default operation is that on initialization a new random traceId is created, hence, why it changes on a new page load.

As you may have noted, for every HTTP request (fetch or XMLHttpRequest) a new spanId is generated by default, which is the basic operation of the W3C Tracing capability (all child operations have a new span id but share the same traceid)

MSNev avatar Mar 06 '25 17:03 MSNev

Thanks @MSNev for the quick respond, really appreciate

  1. If I go with the default enableAutoRouteTracking: false then it should generate new trace_id, right? If yes then what should be the sdk's behavior?
  2. How to generate new traceparent or trace_id for every outgoing request? or Suppose I have multiple transactions in a single page e.g Add, Edit, and Delete etc, How Can I track each transaction separately?
  3. Can it be the reason that I'm importing multiple instances of "appinsights" in multiple components e.g import { appInsights } from "../../logging/appinsights.service"; //appInsights obj

Muhammad-Faraz-Ali avatar Mar 06 '25 19:03 Muhammad-Faraz-Ali

enableAutoRouteTracking: false then it should generate new trace_id, right?

No, only on page load (initialization of the SDK)

How to generate new traceparent or trace_id for every outgoing request?

Do you really want a new trace Id for every outgoing request (fetch / XHR)? If you do this the app map (or any other downstream user of the request) won't be linked to the same "page"

Suppose I have multiple transactions in a single page e.g Add, Edit, and Delete etc, How Can I track each transaction separately?

Hmm, this depends on how you implement them, if we assume that they have different outgoing requests (fetch/XHR) then I would postulate that they should not and that the existing new span id is the correct way. But if you want these to be different logical "pages" then either, you need to change the browser URL and have enableAutoRouteTracking set to true or you will need to "set" (change) the traceid yourself.

Can it be the reason that I'm importing multiple instances of "appinsights" in multiple components

Hmm, this is always problematic, if you are importing, which is also creating and initializing a new instance then this will cause a problem in a SPA as you will actually end up with multiple SDK's all sending out their own outgoing request (resulting in duplicate telemetry) and they will each have their own traceid....

However! The W3C TraceParent specification states "that there can be only one" ( no highlander reference 😄 ), that is requests can only have a single "traceparent" header sent with the request and it can only contain a single valid traceparent encoded value -- most systems "if" something sends multiple headers or a single header with a comma separated "list" will take either the first or last (most take the first).

So (if) you have multiple instances then the first one loaded / initialized will actually set the and be the traceparent (traceid / spanid) that is used.

So What can I do!

Well, from an Application Insights perspective either

  • Only load / initialize 1 SDK instance and have the other "imports" check for the global singleton (where ever your framework keeps singletons (could just be a window global instance or some other object / registry in a closure) -- this avoids the duplicate telemetry and ensures that you have a little more control over the traceid
  • You can change the traceId yourself either
    • Globally, appInsights.getTraceCtx().setTraceId(generateW3CId()), once changed all reqests from this instance will use the new TraceId
    • Or for each fetch / XHR request (doesn't change the global parent page) via a Dependency Listeners, the listener receives an object which contains the current traceId, spanId and traceFlags that will be assigned to the traceparent header -- just change their values to what you want. A listener is called "before" the request is sent or the header added to the request.

MSNev avatar Mar 06 '25 20:03 MSNev

I'm very thankful to you @MSNev for the detailed answers, they really helped me to understand the behavior of sdk.

Understanding:

If I understand correctly, According to you, Default behavior is correct->attaching same trace_id with "traceparent" in all requests under same page url is a best practise rather than attaching different trace_id's with each request.?

->One confusion/question: How will I track/search my single transaction on azure appinsights, as all transactions of that specific page have same trace_ids?


I missed one point, I'm not initializing sdk in every component rather I'm importing it using the useAppInsightsContext() hook e.g

app.tsx

import {
  AppInsightsContext,
  AppInsightsErrorBoundary,
} from "@microsoft/applicationinsights-react-js";
import { reactPlugin } from "../logging/appinsights.service";
import ErrorFallback from "../logging/error-fallback/error-fallback.component";
import { Provider } from "react-redux";

const App= () => {
 

  return (
    <AppInsightsContext.Provider value={reactPlugin}>
      <AppInsightsErrorBoundary
        onError={() => <ErrorFallback />}
        appInsights={reactPlugin}
      >
        <Provider store={store}>
          
          <Home/>
          <Catalog/>
        
        </Provider>
      </AppInsightsErrorBoundary>
    </AppInsightsContext.Provider>
  );
};

export default App;

Catalog.tsx

import { useAppInsightsContext } from "@microsoft/applicationinsights-react-js";
import { SeverityLevel } from "@microsoft/applicationinsights-web";

export const Catalog: React.FC = () => {
  const navigate = useNavigate();
  const appInsights = useAppInsightsContext(); //Here it is
  ......
  .....

Muhammad-Faraz-Ali avatar Mar 09 '25 20:03 Muhammad-Faraz-Ali

If I understand correctly, According to you, Default behavior is correct->attaching same trace_id with "traceparent" in all requests under same page url is a best practise rather than attaching different trace_id's with each request.?

Correct. Although, its not "strictly" the "same page url" but the same logical "page" (view), ie. It is valid for an application to use the same URL for "different" logical pages, but this can make it difficult to determine the "source" page for event, so most people have a different URL whether thats a full URL (which depending on your framework may be a full round trip to a server) or a hash based URL (eg. https://my.app.domain/#page1) which will add to the local browser history but not trigger a request to the hosting server.

How will I track/search my single transaction on azure appinsights, as all transactions of that specific page have same trace_ids?

If you want the dependencies (outgoing request) to be reported under a different traceId (also known as the RequestId) then the traceId must be change manually or automatically as described (by the history listener, config and changing the URL) based on how / when you want the traceId changed. Where the "manually" could also include just using a Dependency Listener to "change" the traceId value added to the fetch / XHR requests (this later option wont change the traceId used for any other events triggered from the main page (like button clicks or any other manual track events).

MSNev avatar Mar 10 '25 19:03 MSNev

It would have been nice if this behavior ends up in the documentation of the enableAutoRouteTracking setting because it was not clear to me this is a side-effect of setting it to true.

JoepKockelkorn avatar Sep 03 '25 13:09 JoepKockelkorn