๐Ÿ“Š

NutriBot โ€” RUM + GenAI Tracing

Application Insights Web SDK (TypeScript) ยท Azure Monitor ยท OTel GenAI Semantic Conventions
@microsoft/applicationinsights-web @microsoft/applicationinsights-react-js web-vitals OTel GenAI React 18 Vite + TypeScript W3C traceparent
Eventos RUM / sesion
38
page views + clicks + exceptions
Trazas GenAI / usuario
12
invoke_agent ยท chat ยท tool spans
Core Web Vitals
LCP 1.4s
CLS 0.04 ยท INP 68ms ยท GOOD
Correlacion distribuida
100%
W3C traceparent โ†’ Azure OpenAI
Arquitectura de telemetria
๐ŸŒ
Browser React
NutriBot SPA
โ†’
RUM auto-track
๐Ÿ“ก
App Insights SDK
appInsights.ts singleton
โ†’
HTTPS batch
โ˜๏ธ
Azure Monitor
West Europe
โ†’
traceparent
๐Ÿค–
Azure OpenAI
gpt-4o-mini backend
W3C traceparent propaga el operation_Id del browser al backend โ€” todos los spans del agente IA se correlacionan en un solo mapa de dependencias
Archivos generados
๐Ÿ“„ src/telemetry/appInsights.ts singleton
// NutriBot โ€” Application Insights singleton
// Inicializar UNA vez antes del router

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

export const browserHistory = createBrowserHistory();
export const reactPlugin = new ReactPlugin();
const clickPlugin = new ClickAnalyticsPlugin();

export const appInsights = new ApplicationInsights({
  config: {
    connectionString:
      import.meta.env.VITE_APPINSIGHTS_CONNECTION_STRING,
    extensions: [reactPlugin, clickPlugin],
    extensionConfig: {
      [reactPlugin.identifier]: {
        history: browserHistory
      },
      [clickPlugin.identifier]: {
        autoCapture: true,
        dataTags: {
          useDefaultContentNameOrId: true,
          customDataPrefix: "data-ai-"
        }
      }
    },
    // Correlacion distribuida W3C
    distributedTracingMode:
      DistributedTracingModes.AI_AND_W3C,
    enableCorsCorrelation: true,
    enableRequestHeaderTracking: true,
    enableResponseHeaderTracking: true,
    autoTrackPageVisitTime: true,
    disableFetchTracking: false,
    // GDPR: cookies desactivadas por defecto
    isCookieUseDisabled: true,
    excludeRequestFromAutoTrackingPatterns: [
      /livemetrics\.azure\.com/i
    ]
  }
});

// Enriquecimiento global de telemetria
appInsights.addTelemetryInitializer(
  (item: ITelemetryItem) => {
    item.tags ??= {};
    item.tags["ai.cloud.role"] = "nutribot-web";
    item.tags["ai.cloud.roleInstance"] =
      window.location.hostname;
    item.data ??= {};
    item.data["app.version"] =
      import.meta.env.VITE_APP_VERSION;    // "2.4.1"
    item.data["app.build"] =
      import.meta.env.VITE_BUILD_SHA;     // "a3f9c12"

    // Scrub tokens de query string (GDPR)
    if (item.baseData?.uri) {
      item.baseData.uri = item.baseData.uri
        .replace(
          /([?&](token|sig|key)=)[^&]+/gi,
          "$1REDACTED"
        );
    }
    // Descartar healthcheck
    if (item.baseType === "PageviewData" &&
        item.baseData?.uri?.endsWith("/healthz"))
      return false;
  }
);

appInsights.loadAppInsights();
appInsights.trackPageView(); // inicial

// Activar cookies cuando el usuario acepta el banner
export function enableTelemetryCookies() {
  appInsights.getCookieMgr().setEnabled(true);
  appInsights.config.disableTelemetry = false;
}
๐Ÿช src/telemetry/useAIAgentTracing.ts hook
// Hook React para trazas OTel GenAI del agente
// de planificacion de dieta (NutriBot)

import {
  SeverityLevel
} from "@microsoft/applicationinsights-web";
import { appInsights } from "./appInsights";

type GenAiAttrs = Record<
  string, string | number | boolean | undefined
>;

function startSpan(name: string, attrs: GenAiAttrs) {
  const id = crypto.randomUUID();
  const t0 = performance.now();
  return {
    end(
      ok: boolean,
      extra: GenAiAttrs = {},
      err?: Error
    ) {
      const ms = Math.round(performance.now() - t0);
      appInsights.trackDependencyData({
        id, name,
        duration: ms,
        success: ok,
        responseCode: err ? 500 : 200,
        type: "GenAI",
        target: String(
          attrs["gen_ai.provider.name"] ?? "genai"
        ),
        properties: {
          ...attrs, ...extra
        } as Record<string, string>
      });
      if (err)
        appInsights.trackException({
          exception: err,
          severityLevel: SeverityLevel.Error,
          properties: {
            "gen_ai.operation.name":
              String(attrs["gen_ai.operation.name"])
          }
        });
    }
  };
}

/** Invoca el agente de planificacion y emite
 *  trazas: invoke_agent โ†’ chat โ†’ execute_tool  */
export async function traceNutriBotAgent(
  prompt: string,
  tenantId: string
): Promise<string> {
  // 1. Span principal: invoke_agent
  const agent = startSpan(
    "invoke_agent NutriPlannerAgent", {
    "gen_ai.operation.name": "invoke_agent",
    "gen_ai.provider.name": "azure.ai.openai",
    "gen_ai.agent.name": "NutriPlannerAgent",
    "gen_ai.agent.id": "asst_nutri_v2_prod",
    "gen_ai.request.model": "gpt-4o-mini",
    "server.address":
      "nutribot.openai.azure.com",
    "tenant.id": tenantId
  });

  try {
    // 2. Span chat
    const chat = startSpan(
      "chat gpt-4o-mini", {
      "gen_ai.operation.name": "chat",
      "gen_ai.provider.name": "azure.ai.openai",
      "gen_ai.request.model": "gpt-4o-mini"
    });
    const res = await fetch(
      "/api/nutriplanner",
      { method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ prompt, tenantId }) }
    );
    const data = await res.json();
    chat.end(true, {
      "gen_ai.response.model": data.model,
      "gen_ai.response.id": data.id,
      "gen_ai.usage.input_tokens":
        data.usage.prompt_tokens,
      "gen_ai.usage.output_tokens":
        data.usage.completion_tokens,
      "gen_ai.output.type": "text"
    });

    // 3. Span tool: busqueda de alimentos
    const tool = startSpan(
      "execute_tool searchFoodDB", {
      "gen_ai.operation.name": "execute_tool",
      "gen_ai.tool.name": "searchFoodDB",
      "gen_ai.tool.type": "datastore",
      "gen_ai.tool.call.id": data.toolCallId
    });
    const foodRes = await fetch(
      `/api/foods?q=${data.foodQuery}`
    );
    tool.end(true);

    agent.end(true, {
      "gen_ai.usage.input_tokens":
        data.usage.prompt_tokens,
      "gen_ai.usage.output_tokens":
        data.usage.completion_tokens
    });
    return data.plan;
  } catch (err) {
    agent.end(false, {}, err as Error);
    throw err;
  }
}
โšก src/telemetry/webVitals.ts vitals
// Core Web Vitals โ†’ Azure Monitor
// Ejecutar una vez, tras loadAppInsights()

import {
  onCLS, onLCP, onINP,
  type Metric
} from "web-vitals";
import { appInsights } from "./appInsights";

function sendVital(m: Metric) {
  appInsights.trackMetric(
    {
      name: `web_vitals.${m.name.toLowerCase()}`,
      average: m.value
    },
    {
      rating: m.rating,         // "good"|"needs-improvement"|"poor"
      navigationType: m.navigationType,
      id: m.id,
      // NutriBot custom dimensions
      "page.route": window.location.pathname,
      "tenant.id": sessionStorage.getItem(
        "nb_tenant"
      ) ?? "unknown"
    }
  );
}

export function registerWebVitals() {
  onCLS(sendVital);   // Cumulative Layout Shift
  onLCP(sendVital);   // Largest Contentful Paint
  onINP(sendVital);   // Interaction to Next Paint
}

/* Umbral de referencia NutriBot (v2.4.1):
   LCP  โ‰ค 2.5 s  โ†’ resultado actual: 1.4 s  โœ… GOOD
   CLS  โ‰ค 0.10   โ†’ resultado actual: 0.04  โœ… GOOD
   INP  โ‰ค 200 ms โ†’ resultado actual: 68 ms  โœ… GOOD  */
โš›๏ธ src/App.tsx entry point
// Punto de entrada โ€” NutriBot SPA
// Inicializa telemetria antes del render

import "./telemetry/appInsights";  // โ‘  mรกs temprano posible
import { registerWebVitals }
  from "./telemetry/webVitals";
import { traceNutriBotAgent }
  from "./telemetry/useAIAgentTracing";
import {
  appInsights, enableTelemetryCookies
} from "./telemetry/appInsights";
import { Router, Route, Routes }
  from "react-router-dom";

registerWebVitals();  // โ‘ก Web Vitals

export function App() {
  const { tenantId, userId } = useAuth();

  // Identidad autenticada (sin PII โ€” usar IDs)
  React.useEffect(() => {
    if (userId)
      appInsights.setAuthenticatedUserContext(
        userId, tenantId, true
      );
    return () =>
      appInsights.clearAuthenticatedUserContext();
  }, [userId]);

  async function handlePlanRequest(
    prompt: string
  ) {
    appInsights.trackEvent(
      { name: "PlanRequestStarted" },
      { tenantId, promptLength: prompt.length }
    );
    try {
      const plan = await
        traceNutriBotAgent(prompt, tenantId);
      appInsights.trackEvent(
        { name: "PlanRequestCompleted" },
        { tenantId }
      );
      return plan;
    } catch (err) {
      appInsights.trackException({
        exception: err as Error,
        properties: { tenantId }
      });
    }
  }

  return (
    <Routes>
      <Route path="/"
        element={<Dashboard />} />
      <Route path="/plan"
        element={
          <PlannerPage
            onRequest={handlePlanRequest}
          />
        } />
      <Route path="/clinic/:id"
        element={<ClinicView />} />
    </Routes>
  );
}

// Boton CTA rastreado via Click Analytics
// (data-ai-* attributes en JSX)
function PlannerCTA() {
  return (
    <button
      data-ai-name="GenerarPlanBtn"
      data-ai-behavior="generate_plan"
      data-ai-parentid="planner_section"
      onClick={() =>
        handlePlanRequest("Plan semanal bajo en sodio")
      }
    >
      Generar plan nutricional โœจ
    </button>
  );
}

// Banner GDPR โ†’ activa cookies al aceptar
function CookieBanner() {
  return (
    <button onClick={enableTelemetryCookies}>
      Aceptar cookies de analisis
    </button>
  );
}
KQL โ€” consulta GenAI traces en App Insights
Log Analytics Workspace ยท NutriBot ยท dependencies (tipo GenAI)
// Rendimiento del agente NutriPlannerAgent por modelo โ€” ultimas 24h
dependencies
| where type == "GenAI"
| extend
    op      = tostring(customDimensions["gen_ai.operation.name"]),
    agent   = tostring(customDimensions["gen_ai.agent.name"]),
    model   = tostring(customDimensions["gen_ai.request.model"]),
    t_in    = toint(customDimensions["gen_ai.usage.input_tokens"]),
    t_out   = toint(customDimensions["gen_ai.usage.output_tokens"]),
    tenant  = tostring(customDimensions["tenant.id"])
| where agent == "NutriPlannerAgent"
| summarize
    calls     = count(),
    p95_ms    = percentile(duration, 95),
    avg_in    = avg(t_in),
    avg_out   = avg(t_out),
    errors    = countif(success == false)
  by op, model, tenant, bin(timestamp, 5m)
| order by timestamp desc

/* Resultado esperado (ultimas 5min):
   op              model         tenant        calls  p95_ms  avg_in  avg_out  errors
   invoke_agent    -             clinica-0032  14     1840    -       -        0
   chat            gpt-4o-mini   clinica-0032  14     1720    312     148      0
   execute_tool    -             clinica-0032  11     43      -       -        0    */
Errores comunes evitados
โš  Pitfall 1
Doble inicializacion โ€” importar el singleton en dos bundles produce page views duplicados. Solucion: un unico appInsights.ts exportado.
โš  Pitfall 2
enableAutoRouteTracking + manual โ€” activar ambos duplica page views en React Router. Usamos el ReactPlugin con history para router tracking.
โš  Pitfall 3
Contenido sensible GenAI โ€” gen_ai.input.messages y gen_ai.output.messages son opt-in. No se loguean sin flag runtime explicitoy aprobacion.
โš  Pitfall 4
CORS distribuido โ€” requiere que /api/nutriplanner exponga header Request-Context y acepte traceparent. Configurado en Azure API Management.
โš  Pitfall 5
React StrictMode โ€” doble invocacion de effects en dev puede llamar loadAppInsights() dos veces. Protegido con export de modulo singleton.
โš  Pitfall 6
Token usage en invoke_agent โ€” los tokens van en el span chat; se copian al parent invoke_agent solo cuando se conoce el total agregado definitivo.