{"version":3,"file":"span-name-normalizer.cjs","names":[],"sources":["../src/span-name-normalizer.ts"],"sourcesContent":["/**\n * Span Name Normalizer\n *\n * Normalizes span names to reduce cardinality from dynamic path segments.\n * This is critical for observability backends that charge by unique span names\n * or have cardinality limits.\n *\n * @example Basic usage with custom function\n * ```typescript\n * init({\n *   service: 'my-app',\n *   spanNameNormalizer: (name) => {\n *     return name.replace(/\\/[0-9]+/g, '/:id');\n *   }\n * })\n * ```\n *\n * @example Using built-in preset\n * ```typescript\n * init({\n *   service: 'my-app',\n *   spanNameNormalizer: 'rest-api'\n * })\n * ```\n */\n\nimport type {\n  SpanProcessor,\n  ReadableSpan,\n} from '@opentelemetry/sdk-trace-base';\nimport type { Context } from '@opentelemetry/api';\nimport type { Span } from '@opentelemetry/sdk-trace-base';\n\n/**\n * Function to normalize a span name\n * @param name - The original span name\n * @returns The normalized span name\n */\nexport type SpanNameNormalizerFn = (name: string) => string;\n\n/**\n * Built-in normalizer preset names\n */\nexport type SpanNameNormalizerPreset = 'rest-api' | 'graphql' | 'minimal';\n\n/**\n * Normalizer config - either a function or a preset name\n */\nexport type SpanNameNormalizerConfig =\n  | SpanNameNormalizerFn\n  | SpanNameNormalizerPreset;\n\nexport interface SpanNameNormalizingProcessorOptions {\n  /**\n   * Normalizer function or preset name\n   */\n  normalizer: SpanNameNormalizerConfig;\n}\n\n/**\n * Built-in normalizer patterns\n */\nconst NORMALIZER_PATTERNS = {\n  // Numeric IDs: /users/123 → /users/:id\n  numericId: /\\/\\d+(?=\\/|$)/g,\n\n  // UUIDs: /users/550e8400-e29b-41d4-a716-446655440000 → /users/:uuid\n  uuid: /\\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(?=\\/|$)/gi,\n\n  // Short UUIDs (without dashes): /users/550e8400e29b41d4a716446655440000 → /users/:uuid\n  shortUuid: /\\/[0-9a-f]{32}(?=\\/|$)/gi,\n\n  // MongoDB ObjectIds: /docs/507f1f77bcf86cd799439011 → /docs/:objectId\n  objectId: /\\/[0-9a-f]{24}(?=\\/|$)/gi,\n\n  // Hashes (6+ hex chars): /assets/abc123def.js → /assets/:hash.js\n  hash: /\\/[0-9a-f]{6,}(?=\\.[a-z]+$)/gi,\n\n  // ISO dates: /logs/2024-01-15 → /logs/:date\n  isoDate: /\\/\\d{4}-\\d{2}-\\d{2}(?=\\/|$)/g,\n\n  // Timestamps: /events/1705334400 → /events/:timestamp\n  timestamp: /\\/1[0-9]{9}(?=\\/|$)/g,\n\n  // Email-like segments: /users/john@example.com → /users/:email\n  email: /\\/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}(?=\\/|$)/g,\n} as const;\n\n/**\n * Built-in normalizer presets\n */\nconst NORMALIZER_PRESETS: Record<\n  SpanNameNormalizerPreset,\n  SpanNameNormalizerFn\n> = {\n  /**\n   * REST API preset - normalizes common REST path patterns\n   * Handles: numeric IDs, UUIDs, ObjectIds, dates, timestamps, emails\n   */\n  'rest-api': (name: string): string => {\n    return name\n      .replaceAll(NORMALIZER_PATTERNS.uuid, '/:uuid')\n      .replaceAll(NORMALIZER_PATTERNS.shortUuid, '/:uuid')\n      .replaceAll(NORMALIZER_PATTERNS.objectId, '/:objectId')\n      .replaceAll(NORMALIZER_PATTERNS.isoDate, '/:date')\n      .replaceAll(NORMALIZER_PATTERNS.timestamp, '/:timestamp')\n      .replaceAll(NORMALIZER_PATTERNS.email, '/:email')\n      .replaceAll(NORMALIZER_PATTERNS.numericId, '/:id');\n  },\n\n  /**\n   * GraphQL preset - normalizes GraphQL operation names and paths\n   * Keeps query/mutation names but normalizes embedded IDs\n   */\n  graphql: (name: string): string => {\n    // For GraphQL, normalize both path-style and embedded IDs\n    return name\n      .replaceAll(NORMALIZER_PATTERNS.uuid, '/:uuid')\n      .replaceAll(NORMALIZER_PATTERNS.numericId, '/:id');\n  },\n\n  /**\n   * Minimal preset - only normalizes numeric IDs and UUIDs\n   */\n  minimal: (name: string): string => {\n    return name\n      .replaceAll(NORMALIZER_PATTERNS.uuid, '/:uuid')\n      .replaceAll(NORMALIZER_PATTERNS.numericId, '/:id');\n  },\n};\n\n/**\n * Resolve normalizer config to a function\n */\nfunction resolveNormalizer(\n  config: SpanNameNormalizerConfig,\n): SpanNameNormalizerFn {\n  if (typeof config === 'function') {\n    return config;\n  }\n\n  const preset = NORMALIZER_PRESETS[config];\n  if (!preset) {\n    throw new Error(\n      `Unknown span name normalizer preset: \"${config}\". ` +\n        `Available presets: ${Object.keys(NORMALIZER_PRESETS).join(', ')}`,\n    );\n  }\n\n  return preset;\n}\n\n/**\n * Span processor that normalizes span names to reduce cardinality.\n *\n * Normalization happens in onStart() when we have access to the mutable Span.\n * This allows us to call span.updateName() before the span is finalized.\n *\n * Common use cases:\n * - REST APIs: /users/123/posts/456 → /users/:id/posts/:id\n * - UUIDs: /items/550e8400-e29b-41d4-a716-446655440000 → /items/:uuid\n * - Dates: /logs/2024-01-15 → /logs/:date\n */\nexport class SpanNameNormalizingProcessor implements SpanProcessor {\n  private readonly wrappedProcessor: SpanProcessor;\n  private readonly normalizer: SpanNameNormalizerFn;\n\n  constructor(\n    wrappedProcessor: SpanProcessor,\n    options: SpanNameNormalizingProcessorOptions,\n  ) {\n    this.wrappedProcessor = wrappedProcessor;\n    this.normalizer = resolveNormalizer(options.normalizer);\n  }\n\n  /**\n   * Normalize span name on start (when Span is mutable)\n   */\n  onStart(span: Span, parentContext: Context): void {\n    try {\n      const originalName = span.name;\n      const normalizedName = this.normalizer(originalName);\n\n      if (normalizedName !== originalName) {\n        span.updateName(normalizedName);\n      }\n    } catch {\n      // If normalizer throws, keep original name (fail-open)\n    }\n\n    this.wrappedProcessor.onStart(span, parentContext);\n  }\n\n  /**\n   * Pass through onEnd unchanged\n   */\n  onEnd(span: ReadableSpan): void {\n    this.wrappedProcessor.onEnd(span);\n  }\n\n  forceFlush(): Promise<void> {\n    return this.wrappedProcessor.forceFlush();\n  }\n\n  shutdown(): Promise<void> {\n    return this.wrappedProcessor.shutdown();\n  }\n}\n\n/**\n * Export built-in patterns for advanced users who want to compose their own normalizers\n */\nexport { NORMALIZER_PATTERNS, NORMALIZER_PRESETS };\n"],"mappings":";;;;;;AA8DA,MAAM,sBAAsB;CAE1B,WAAW;CAGX,MAAM;CAGN,WAAW;CAGX,UAAU;CAGV,MAAM;CAGN,SAAS;CAGT,WAAW;CAGX,OAAO;AACT;;;;AAKA,MAAM,qBAGF;;;;;CAKF,aAAa,SAAyB;EACpC,OAAO,KACJ,WAAW,oBAAoB,MAAM,QAAQ,CAAC,CAC9C,WAAW,oBAAoB,WAAW,QAAQ,CAAC,CACnD,WAAW,oBAAoB,UAAU,YAAY,CAAC,CACtD,WAAW,oBAAoB,SAAS,QAAQ,CAAC,CACjD,WAAW,oBAAoB,WAAW,aAAa,CAAC,CACxD,WAAW,oBAAoB,OAAO,SAAS,CAAC,CAChD,WAAW,oBAAoB,WAAW,MAAM;CACrD;;;;;CAMA,UAAU,SAAyB;EAEjC,OAAO,KACJ,WAAW,oBAAoB,MAAM,QAAQ,CAAC,CAC9C,WAAW,oBAAoB,WAAW,MAAM;CACrD;;;;CAKA,UAAU,SAAyB;EACjC,OAAO,KACJ,WAAW,oBAAoB,MAAM,QAAQ,CAAC,CAC9C,WAAW,oBAAoB,WAAW,MAAM;CACrD;AACF;;;;AAKA,SAAS,kBACP,QACsB;CACtB,IAAI,OAAO,WAAW,YACpB,OAAO;CAGT,MAAM,SAAS,mBAAmB;CAClC,IAAI,CAAC,QACH,MAAM,IAAI,MACR,yCAAyC,OAAO,wBACxB,OAAO,KAAK,kBAAkB,CAAC,CAAC,KAAK,IAAI,GACnE;CAGF,OAAO;AACT;;;;;;;;;;;;AAaA,IAAa,+BAAb,MAAmE;CACjE,AAAiB;CACjB,AAAiB;CAEjB,YACE,kBACA,SACA;EACA,KAAK,mBAAmB;EACxB,KAAK,aAAa,kBAAkB,QAAQ,UAAU;CACxD;;;;CAKA,QAAQ,MAAY,eAA8B;EAChD,IAAI;GACF,MAAM,eAAe,KAAK;GAC1B,MAAM,iBAAiB,KAAK,WAAW,YAAY;GAEnD,IAAI,mBAAmB,cACrB,KAAK,WAAW,cAAc;EAElC,QAAQ,CAER;EAEA,KAAK,iBAAiB,QAAQ,MAAM,aAAa;CACnD;;;;CAKA,MAAM,MAA0B;EAC9B,KAAK,iBAAiB,MAAM,IAAI;CAClC;CAEA,aAA4B;EAC1B,OAAO,KAAK,iBAAiB,WAAW;CAC1C;CAEA,WAA0B;EACxB,OAAO,KAAK,iBAAiB,SAAS;CACxC;AACF"}