or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

advanced.mdannotation-queues.mdanonymizer.mdclient-api.mddatasets.mdevaluation.mdfeedback.mdgetting-started.mdindex.mdjest.mdlangchain.mdopentelemetry.mdprompts.mdrun-trees.mdschemas.mdtesting.mdtracing.mdvercel.mdvitest.mdworkflows.mdwrappers.md
tile.json

anonymizer.mddocs/

Data Anonymization

LangSmith Data Anonymization provides utilities for removing sensitive information from traces before they are sent to LangSmith. The anonymizer allows you to redact PII (Personally Identifiable Information), credentials, API keys, and other sensitive data using pattern matching, custom rules, or processor functions.

Core Imports

import { createAnonymizer } from "langsmith/anonymizer";
import type {
  Anonymizer,
  ReplacerType,
  StringNode,
  StringNodeRule,
  StringNodeProcessor,
  AnonymizerOptions,
} from "langsmith/anonymizer";

Basic Usage

import { traceable } from "langsmith/traceable";
import { createAnonymizer } from "langsmith/anonymizer";

// Create anonymizer with simple rules
const anonymizer = createAnonymizer([
  { pattern: /\b\d{3}-\d{2}-\d{4}\b/g, replace: "[SSN]" },
  { pattern: /\b[\w\.-]+@[\w\.-]+\.\w+\b/g, replace: "[EMAIL]" },
  { pattern: /\bsk-[a-zA-Z0-9]{32,}\b/g, replace: "[API_KEY]" },
]);

// Use in traceable function
const processUserData = traceable(
  async (input: { text: string }) => {
    // Process data
    return { result: "processed data" };
  },
  {
    name: "process_user_data",
    processInputs: anonymizer,
    processOutputs: anonymizer,
  }
);

// Sensitive data will be redacted from traces
await processUserData({
  text: "Contact me at john.doe@example.com or SSN 123-45-6789",
});
// Traced as: "Contact me at [EMAIL] or SSN [SSN]"

Capabilities

Create Anonymizer

Create an anonymizer function that removes sensitive information from data structures.

/**
 * Create data anonymizer for removing sensitive information from traces
 * @param replacer - Replacer function, rules array, or processor
 * @param options - Configuration options
 * @returns Anonymizer function that anonymizes data structures
 */
function createAnonymizer(
  replacer: ReplacerType,
  options?: AnonymizerOptions
): Anonymizer;

type ReplacerType =
  | ((value: string, node: StringNode) => string)
  | StringNodeRule[]
  | StringNodeProcessor;

type Anonymizer = (data: any) => any;

interface AnonymizerOptions {
  /** Maximum depth to traverse in nested objects (default: 10) */
  maxDepth?: number;
  /** Whether to anonymize object keys (default: false) */
  anonymizeKeys?: boolean;
  /** Paths to exclude from anonymization */
  excludePaths?: (string | number)[][];
  /** Whether to handle circular references (default: true) */
  handleCircular?: boolean;
}

/**
 * Note: By default, anonymization rules apply to string VALUES, not object KEYS.
 * If you want to anonymize keys, use the `anonymizeKeys` option. Otherwise,
 * consider using a processor-based approach for key anonymization.
 */

Usage Examples:

import { createAnonymizer } from "langsmith/anonymizer";

// Using rule array
const ruleAnonymizer = createAnonymizer([
  { pattern: /\b\d{16}\b/g, replace: "[CARD]" },
  { pattern: /password:\s*\S+/gi, replace: "password: [REDACTED]" },
]);

// Using custom function
const functionAnonymizer = createAnonymizer((value, node) => {
  // Custom logic based on path
  if (node.path.includes("email")) {
    return "[EMAIL_REDACTED]";
  }
  // Replace SSNs
  return value.replace(/\b\d{3}-\d{2}-\d{4}\b/g, "[SSN]");
});

// Using processor object
const processorAnonymizer = createAnonymizer({
  maskNodes: (nodes) => {
    return nodes.reduce((result, node) => {
      if (typeof node.value === "string" && node.value.includes("secret")) {
        result.push({ ...node, value: "[SECRET]" });
      }
      return result;
    }, [] as StringNode[]);
  },
});

// With options
const configuredAnonymizer = createAnonymizer(
  [{ pattern: /api_key=\w+/g, replace: "api_key=[REDACTED]" }],
  {
    maxDepth: 5,
    excludePaths: [["metadata", "safe_field"], ["config", "public"]],
    anonymizeKeys: false,
    handleCircular: true,
  }
);

Anonymization Approaches

1. Rule-Based Anonymization

Use pattern-based rules to match and replace sensitive data.

/**
 * Rule for matching and replacing patterns in strings
 */
interface StringNodeRule {
  /** Regular expression or string to match */
  pattern: RegExp | string;
  /** Replacement string or function */
  replace: string | ((match: string, ...args: any[]) => string);
}

Usage Examples:

import { createAnonymizer } from "langsmith/anonymizer";

// Common PII patterns
const piiAnonymizer = createAnonymizer([
  // Email addresses
  {
    pattern: /\b[\w\.-]+@[\w\.-]+\.\w+\b/g,
    replace: "[EMAIL]",
  },
  // Phone numbers (various formats)
  {
    pattern: /\b\d{3}[-.\s]?\d{3}[-.\s]?\d{4}\b/g,
    replace: "[PHONE]",
  },
  // Social Security Numbers
  {
    pattern: /\b\d{3}-\d{2}-\d{4}\b/g,
    replace: "[SSN]",
  },
  // Credit card numbers
  {
    pattern: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g,
    replace: "[CARD]",
  },
  // IP addresses
  {
    pattern: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g,
    replace: "[IP]",
  },
]);

// API keys and tokens
const apiKeyAnonymizer = createAnonymizer([
  {
    pattern: /\bsk-[a-zA-Z0-9]{32,}\b/g,
    replace: "[OPENAI_KEY]",
  },
  {
    pattern: /\bghp_[a-zA-Z0-9]{36}\b/g,
    replace: "[GITHUB_TOKEN]",
  },
  {
    pattern: /\bgho_[a-zA-Z0-9]{36}\b/g,
    replace: "[GITHUB_OAUTH]",
  },
  {
    pattern: /\bglpat-[a-zA-Z0-9_\-]{20}\b/g,
    replace: "[GITLAB_TOKEN]",
  },
  {
    pattern: /Bearer\s+[a-zA-Z0-9\-._~+\/]+=*/g,
    replace: "Bearer [TOKEN]",
  },
  {
    pattern: /\bAKIA[0-9A-Z]{16}\b/g,
    replace: "[AWS_KEY]",
  },
]);

// Dynamic replacement with function
const smartAnonymizer = createAnonymizer([
  {
    pattern: /\b[\w\.-]+@[\w\.-]+\.\w+\b/g,
    replace: (match) => {
      // Preserve domain for debugging
      const domain = match.split("@")[1];
      return `[EMAIL@${domain}]`;
    },
  },
  {
    pattern: /\b\d{16}\b/g,
    replace: (match) => {
      // Keep last 4 digits of card
      return `[CARD-****${match.slice(-4)}]`;
    },
  },
  {
    pattern: /\b\d{3}-\d{2}-\d{4}\b/g,
    replace: (match) => {
      // Keep last 4 digits of SSN
      return `[SSN-**-${match.slice(-4)}]`;
    },
  },
]);

// Comprehensive PII protection
const comprehensivePII = createAnonymizer([
  // Personal Information
  { pattern: /\b[\w\.-]+@[\w\.-]+\.\w+\b/g, replace: "[EMAIL]" },
  { pattern: /\b\d{3}-\d{2}-\d{4}\b/g, replace: "[SSN]" },
  { pattern: /\b\d{3}[-.\s]?\d{3}[-.\s]?\d{4}\b/g, replace: "[PHONE]" },

  // Financial Information
  {
    pattern: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g,
    replace: "[CARD]",
  },
  { pattern: /\b\d{3}-\d{2}-\d{4}\b/g, replace: "[ROUTING]" },
  { pattern: /CVV:\s*\d{3,4}/gi, replace: "CVV: [REDACTED]" },

  // API Keys and Tokens
  { pattern: /\bsk-[a-zA-Z0-9]{32,}\b/g, replace: "[OPENAI_KEY]" },
  { pattern: /\bghp_[a-zA-Z0-9]{36}\b/g, replace: "[GITHUB_TOKEN]" },
  { pattern: /Bearer\s+[a-zA-Z0-9\-._~+\/]+=*/g, replace: "Bearer [TOKEN]" },

  // Passwords and Secrets
  { pattern: /password["\s:=]+\S+/gi, replace: "password=[REDACTED]" },
  { pattern: /secret["\s:=]+\S+/gi, replace: "secret=[REDACTED]" },
  { pattern: /api[_-]?key["\s:=]+\S+/gi, replace: "api_key=[REDACTED]" },

  // Network Information
  { pattern: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, replace: "[IP]" },
  {
    pattern: /\b[0-9a-f]{1,4}:[0-9a-f:]+\b/gi,
    replace: "[IPv6]",
  },

  // URLs with credentials
  {
    pattern: /https?:\/\/[^:]+:[^@]+@[^\s]+/g,
    replace: (match) => {
      try {
        const url = new URL(match);
        return `${url.protocol}//[USER]:[PASS]@${url.host}${url.pathname}`;
      } catch {
        return "[URL_WITH_CREDS]";
      }
    },
  },
]);

2. Function-Based Anonymization

Use custom functions for complex anonymization logic.

/**
 * Custom replacer function
 * @param value - The string value to potentially replace
 * @param node - Information about the node location
 * @returns Anonymized string or original value
 */
type ReplacerFunction = (value: string, node: StringNode) => string;

interface StringNode {
  /** Path to the node in the data structure */
  path: (string | number)[];
  /** The string value at this node */
  value: string;
}

Usage Examples:

import { createAnonymizer } from "langsmith/anonymizer";

// Path-aware anonymization
const pathBasedAnonymizer = createAnonymizer((value, node) => {
  const pathString = node.path.join(".");

  // Redact specific fields completely
  if (
    pathString.includes("password") ||
    pathString.includes("secret") ||
    pathString.includes("token")
  ) {
    return "[REDACTED]";
  }

  // Redact API keys
  if (pathString.includes("apiKey") || pathString.includes("api_key")) {
    return "[API_KEY]";
  }

  // Anonymize emails in user fields
  if (pathString.includes("user.email") || pathString.includes("email")) {
    return value.replace(/\b[\w\.-]+@[\w\.-]+\.\w+\b/g, "[EMAIL]");
  }

  // Keep track of what paths we see for debugging
  if (process.env.DEBUG_ANONYMIZER) {
    console.log(`Processing path: ${pathString}`);
  }

  // Apply standard PII patterns to everything else
  let result = value;
  result = result.replace(/\b\d{3}-\d{2}-\d{4}\b/g, "[SSN]");
  result = result.replace(/\b\d{3}-\d{3}-\d{4}\b/g, "[PHONE]");

  return result;
});

// Context-aware anonymization
const contextAnonymizer = createAnonymizer((value, node) => {
  // Check parent path
  const parentPath = node.path.slice(0, -1).join(".");

  // If in credentials object, redact everything
  if (parentPath.includes("credentials") || parentPath.includes("auth")) {
    return "[CREDENTIAL]";
  }

  // If in headers, check for sensitive headers
  if (parentPath === "headers") {
    const key = node.path[node.path.length - 1];
    if (key === "authorization" || key === "cookie") {
      return "[AUTH_HEADER]";
    }
  }

  // Apply standard PII patterns
  let result = value;
  result = result.replace(/\b[\w\.-]+@[\w\.-]+\.\w+\b/g, "[EMAIL]");
  result = result.replace(/\b\d{3}-\d{2}-\d{4}\b/g, "[SSN]");
  result = result.replace(/\bsk-[a-zA-Z0-9]+\b/g, "[API_KEY]");

  return result;
});

// Selective anonymization with logging
const auditedAnonymizer = createAnonymizer((value, node) => {
  const pathString = node.path.join(".");

  // Check if value contains sensitive patterns
  const patterns = [
    { name: "EMAIL", regex: /\b[\w\.-]+@[\w\.-]+\.\w+\b/g },
    { name: "PHONE", regex: /\b\d{3}-\d{3}-\d{4}\b/g },
    { name: "SSN", regex: /\b\d{3}-\d{2}-\d{4}\b/g },
    { name: "API_KEY", regex: /\bsk-[a-zA-Z0-9]+\b/g },
  ];

  let result = value;
  let modified = false;

  for (const { name, regex } of patterns) {
    const matches = value.match(regex);
    if (matches) {
      result = result.replace(regex, `[${name}]`);
      modified = true;

      // Audit log
      console.log(`[AUDIT] Anonymized ${name} at path: ${pathString}`);
      console.log(`[AUDIT] Matched ${matches.length} occurrence(s)`);
    }
  }

  return modified ? result : value;
});

// Multi-stage anonymization
const multiStageAnonymizer = createAnonymizer((value, node) => {
  // Stage 1: Remove exact matches
  let result = value;
  const exactRedactions = ["CONFIDENTIAL", "INTERNAL_ONLY", "SECRET"];

  for (const term of exactRedactions) {
    if (result.includes(term)) {
      result = result.replace(new RegExp(term, "gi"), "[REDACTED]");
    }
  }

  // Stage 2: Pattern-based redaction
  result = result.replace(/\b[\w\.-]+@[\w\.-]+\.\w+\b/g, "[EMAIL]");
  result = result.replace(/\bsk-[a-zA-Z0-9]+\b/g, "[API_KEY]");

  // Stage 3: Context-based redaction
  if (node.path.some(p => p.toString().toLowerCase().includes("private"))) {
    result = "[PRIVATE_DATA]";
  }

  return result;
});

3. Processor-Based Anonymization

Use a processor object for structured anonymization logic.

/**
 * Processor interface for string nodes
 */
interface StringNodeProcessor {
  /**
   * Process an array of string nodes and return modified nodes
   * @param nodes - Array of string nodes to process
   * @returns Array of modified string nodes (only nodes that were changed)
   */
  maskNodes(nodes: StringNode[]): StringNode[];
}

Usage Examples:

import { createAnonymizer, StringNode } from "langsmith/anonymizer";

// Basic processor
const basicProcessor = createAnonymizer({
  maskNodes: (nodes) => {
    return nodes.reduce((result, node) => {
      // Redact based on path
      if (node.path.some((p) => p.toString().toLowerCase().includes("password"))) {
        result.push({ ...node, value: "[PASSWORD_REDACTED]" });
        return result;
      }

      // Redact emails
      if (node.value.includes("@")) {
        const newValue = node.value.replace(/\b[\w\.-]+@[\w\.-]+\.\w+\b/g, "[EMAIL_REDACTED]");
        result.push({ ...node, value: newValue });
      }

      return result;
    }, [] as StringNode[]);
  },
});

// Advanced processor with state
class StatefulProcessor implements StringNodeProcessor {
  private redactionCount = 0;
  private redactedPaths = new Set<string>();
  private redactionsByType = new Map<string, number>();

  maskNodes(nodes: StringNode[]): StringNode[] {
    return nodes.reduce((result, node) => {
      const pathString = node.path.join(".");

      // Pattern matching with statistics
      const patterns = [
        { type: "EMAIL", regex: /\b[\w\.-]+@[\w\.-]+\.\w+\b/g, replacement: "[EMAIL]" },
        { type: "SSN", regex: /\b\d{3}-\d{2}-\d{4}\b/g, replacement: "[SSN]" },
        { type: "PHONE", regex: /\b\d{3}-\d{3}-\d{4}\b/g, replacement: "[PHONE]" },
        { type: "API_KEY", regex: /\bsk-[a-zA-Z0-9]+\b/g, replacement: "[API_KEY]" },
        { type: "CARD", regex: /\b\d{16}\b/g, replacement: "[CARD]" },
      ];

      let newValue = node.value;
      let modified = false;

      for (const { type, regex, replacement } of patterns) {
        const matches = node.value.match(regex);
        if (matches) {
          newValue = newValue.replace(regex, replacement);
          modified = true;

          // Track statistics
          this.redactionCount += matches.length;
          this.redactedPaths.add(pathString);

          const currentCount = this.redactionsByType.get(type) || 0;
          this.redactionsByType.set(type, currentCount + matches.length);
        }
      }

      if (modified) {
        result.push({ ...node, value: newValue });
      }

      return result;
    }, [] as StringNode[]);
  }

  getStats() {
    return {
      totalRedactions: this.redactionCount,
      uniquePaths: this.redactedPaths.size,
      byType: Object.fromEntries(this.redactionsByType),
      paths: Array.from(this.redactedPaths),
    };
  }

  reset() {
    this.redactionCount = 0;
    this.redactedPaths.clear();
    this.redactionsByType.clear();
  }
}

const processor = new StatefulProcessor();
const statefulAnonymizer = createAnonymizer(processor);

// Use the anonymizer
const result = statefulAnonymizer({
  user: { email: "user@example.com", phone: "555-123-4567" },
  payment: { card: "4532123456789012" },
  api: { key: "sk-abc123xyz789" },
  ssn: "123-45-6789",
});

console.log(processor.getStats());
// {
//   totalRedactions: 4,
//   uniquePaths: 4,
//   byType: { EMAIL: 1, PHONE: 1, CARD: 1, API_KEY: 1, SSN: 1 },
//   paths: ['user.email', 'user.phone', 'payment.card', 'api.key', 'ssn']
// }

// Advanced processor with validation
class ValidatingProcessor implements StringNodeProcessor {
  private rules: Array<{
    name: string;
    pattern: RegExp;
    replacement: string;
    validate?: (match: string) => boolean;
  }>;
  private stats: Map<string, number>;
  private errors: Array<{ path: string; error: string }>;

  constructor() {
    this.rules = [
      {
        name: "EMAIL",
        pattern: /\b[\w\.-]+@[\w\.-]+\.\w+\b/g,
        replacement: "[EMAIL]",
        validate: (match) => match.includes("@") && match.includes("."),
      },
      {
        name: "SSN",
        pattern: /\b\d{3}-\d{2}-\d{4}\b/g,
        replacement: "[SSN]",
        validate: (match) => {
          const parts = match.split("-");
          return parts.length === 3 && parts[0].length === 3;
        },
      },
      { name: "PHONE", pattern: /\b\d{3}-\d{3}-\d{4}\b/g, replacement: "[PHONE]" },
      { name: "CARD", pattern: /\b\d{16}\b/g, replacement: "[CARD]" },
    ];
    this.stats = new Map();
    this.errors = [];
  }

  maskNodes(nodes: StringNode[]): StringNode[] {
    return nodes.reduce((result, node) => {
      let modified = false;
      let newValue = node.value;
      const pathString = node.path.join(".");

      for (const rule of this.rules) {
        const matches = node.value.match(rule.pattern);
        if (matches) {
          // Validate matches if validator provided
          if (rule.validate) {
            for (const match of matches) {
              if (!rule.validate(match)) {
                this.errors.push({
                  path: pathString,
                  error: `Invalid ${rule.name} format: ${match}`,
                });
              }
            }
          }

          newValue = newValue.replace(rule.pattern, rule.replacement);
          modified = true;

          // Track statistics
          const count = this.stats.get(rule.name) || 0;
          this.stats.set(rule.name, count + matches.length);

          // Log for audit
          if (process.env.ANONYMIZER_AUDIT) {
            console.log(`[${new Date().toISOString()}] Anonymized ${rule.name} at path: ${pathString}`);
          }
        }
      }

      if (modified) {
        result.push({ ...node, value: newValue });
      }

      return result;
    }, [] as StringNode[]);
  }

  getStats() {
    return {
      redactions: Object.fromEntries(this.stats),
      errors: this.errors,
      totalRedactions: Array.from(this.stats.values()).reduce((a, b) => a + b, 0),
    };
  }

  reset() {
    this.stats.clear();
    this.errors = [];
  }
}

Types and Interfaces

StringNode Interface

Information about a string node in the data structure.

interface StringNode {
  /**
   * Path to the node in the data structure
   * Array of keys/indices from root to this node
   */
  path: (string | number)[];

  /**
   * The string value at this node
   */
  value: string;
}

StringNodeRule Interface

Rule for pattern-based anonymization.

interface StringNodeRule {
  /**
   * Pattern to match
   * Can be a RegExp or string
   */
  pattern: RegExp | string;

  /**
   * Replacement value
   * Can be a string or function that receives the match
   */
  replace: string | ((match: string, ...args: any[]) => string);
}

StringNodeProcessor Interface

Processor interface for custom anonymization logic.

interface StringNodeProcessor {
  /**
   * Process an array of string nodes and return modified nodes
   * @param nodes - Array of string nodes to process
   * @returns Array of modified string nodes (only nodes that were changed)
   */
  maskNodes(nodes: StringNode[]): StringNode[];
}

ReplacerType Type

Union type for all supported replacer types.

type ReplacerType =
  | ((value: string, node: StringNode) => string)
  | StringNodeRule[]
  | StringNodeProcessor;

Anonymizer Type

Function type that anonymizes data structures.

/**
 * Function that recursively anonymizes data structures
 * @param data - Any data structure (objects, arrays, primitives)
 * @returns Anonymized copy of the data
 */
type Anonymizer = (data: any) => any;

AnonymizerOptions Interface

Configuration options for anonymizer behavior.

interface AnonymizerOptions {
  /**
   * Maximum depth to traverse in nested objects
   * Default: 10
   */
  maxDepth?: number;

  /**
   * Whether to anonymize object keys in addition to values
   * Default: false
   */
  anonymizeKeys?: boolean;

  /**
   * Paths to exclude from anonymization
   * Each path is an array of keys/indices
   * Example: [['metadata', 'safe_field'], ['config', 'public']]
   */
  excludePaths?: (string | number)[][];

  /**
   * Whether to handle circular references
   * Default: true
   * Note: The handleCircular option has limitations and may not work in all cases.
   * For complex circular reference handling, consider preprocessing the data
   * structure before anonymization.
   */
  handleCircular?: boolean;
}

Advanced Patterns

Path-Based Selective Anonymization

Anonymize only specific paths in your data while excluding safe paths.

import { createAnonymizer } from "langsmith/anonymizer";

// Exclude safe paths, anonymize everything else
const selectiveAnonymizer = createAnonymizer(
  (value, node) => {
    const pathString = node.path.join(".");

    // List of sensitive field patterns
    const sensitivePatterns = [
      /password/i,
      /secret/i,
      /token/i,
      /key/i,
      /credential/i,
      /auth/i,
      /ssn/i,
      /social.?security/i,
    ];

    // Check if path contains sensitive keywords
    const isSensitive = sensitivePatterns.some((pattern) =>
      pattern.test(pathString)
    );

    if (isSensitive) {
      return "[REDACTED]";
    }

    // Apply standard PII patterns to all other fields
    let result = value;
    result = result.replace(/\b[\w\.-]+@[\w\.-]+\.\w+\b/g, "[EMAIL]");
    result = result.replace(/\b\d{3}-\d{2}-\d{4}\b/g, "[SSN]");
    result = result.replace(/\b\d{3}-\d{3}-\d{4}\b/g, "[PHONE]");

    return result;
  },
  {
    // Exclude specific safe paths
    excludePaths: [
      ["metadata", "version"],
      ["metadata", "timestamp"],
      ["config", "public"],
      ["config", "settings"],
      ["settings", "theme"],
      ["settings", "language"],
    ],
  }
);

const data = {
  user: {
    email: "user@example.com", // Will be anonymized
    password: "secret123", // Will be redacted
    ssn: "123-45-6789", // Will be redacted
  },
  metadata: {
    version: "1.0.0", // Will NOT be anonymized (excluded)
    timestamp: "2024-01-01T00:00:00Z", // Will NOT be anonymized (excluded)
  },
  config: {
    apiKey: "sk-abc123", // Will be redacted (sensitive keyword)
    public: "public-value", // Will NOT be anonymized (excluded)
  },
};

const anonymized = selectiveAnonymizer(data);

Multi-Pattern Anonymization

Combine multiple pattern types for comprehensive coverage.

import { createAnonymizer } from "langsmith/anonymizer";

const comprehensiveAnonymizer = createAnonymizer([
  // Personal Information
  { pattern: /\b[\w\.-]+@[\w\.-]+\.\w+\b/g, replace: "[EMAIL]" },
  { pattern: /\b\d{3}-\d{2}-\d{4}\b/g, replace: "[SSN]" },
  { pattern: /\b\d{3}[-.\s]?\d{3}[-.\s]?\d{4}\b/g, replace: "[PHONE]" },

  // Financial Information
  {
    pattern: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g,
    replace: "[CARD]",
  },
  { pattern: /CVV:\s*\d{3,4}/gi, replace: "CVV: [REDACTED]" },
  { pattern: /\b\d{9}\b/g, replace: "[ROUTING]" },

  // API Keys and Tokens
  { pattern: /\bsk-[a-zA-Z0-9]{32,}\b/g, replace: "[OPENAI_KEY]" },
  { pattern: /\bghp_[a-zA-Z0-9]{36}\b/g, replace: "[GITHUB_TOKEN]" },
  { pattern: /\bgho_[a-zA-Z0-9]{36}\b/g, replace: "[GITHUB_OAUTH]" },
  { pattern: /\bglpat-[a-zA-Z0-9_\-]{20}\b/g, replace: "[GITLAB_TOKEN]" },
  { pattern: /Bearer\s+[a-zA-Z0-9\-._~+\/]+=*/g, replace: "Bearer [TOKEN]" },
  { pattern: /\bAKIA[0-9A-Z]{16}\b/g, replace: "[AWS_KEY]" },
  { pattern: /\bAIza[0-9A-Za-z\-_]{35}\b/g, replace: "[GOOGLE_API_KEY]" },

  // Passwords and Secrets
  { pattern: /password["\s:=]+\S+/gi, replace: "password=[REDACTED]" },
  { pattern: /secret["\s:=]+\S+/gi, replace: "secret=[REDACTED]" },
  { pattern: /api[_-]?key["\s:=]+\S+/gi, replace: "api_key=[REDACTED]" },

  // Network Information
  { pattern: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, replace: "[IP]" },
  {
    pattern: /\b[0-9a-f]{1,4}:[0-9a-f:]+\b/gi,
    replace: "[IPv6]",
  },

  // URLs with credentials
  {
    pattern: /https?:\/\/[^:]+:[^@]+@[^\s]+/g,
    replace: (match) => {
      try {
        const url = new URL(match);
        return `${url.protocol}//[USER]:[PASS]@${url.host}${url.pathname}`;
      } catch {
        return "[URL_WITH_CREDENTIALS]";
      }
    },
  },

  // Database connection strings
  {
    pattern: /mongodb(\+srv)?:\/\/[^:]+:[^@]+@[^\s]+/g,
    replace: "mongodb://[USER]:[PASS]@[HOST]/[DB]",
  },
  {
    pattern: /postgres:\/\/[^:]+:[^@]+@[^\s]+/g,
    replace: "postgres://[USER]:[PASS]@[HOST]/[DB]",
  },
]);

Custom Processor with Validation

Create a processor that validates and logs anonymization.

import { createAnonymizer, StringNode } from "langsmith/anonymizer";

class ValidatingProcessor implements StringNodeProcessor {
  private rules: Array<{
    name: string;
    pattern: RegExp;
    replacement: string;
    validate?: (match: string) => boolean;
  }>;
  private stats: Map<string, number>;
  private validationErrors: Array<{ path: string; error: string }>;

  constructor() {
    this.rules = [
      {
        name: "EMAIL",
        pattern: /\b[\w\.-]+@[\w\.-]+\.\w+\b/g,
        replacement: "[EMAIL]",
        validate: (match) => {
          // Validate email format
          return match.includes("@") && match.split("@")[1].includes(".");
        },
      },
      {
        name: "SSN",
        pattern: /\b\d{3}-\d{2}-\d{4}\b/g,
        replacement: "[SSN]",
        validate: (match) => {
          // Validate SSN format
          const parts = match.split("-");
          return (
            parts.length === 3 &&
            parts[0].length === 3 &&
            parts[1].length === 2 &&
            parts[2].length === 4
          );
        },
      },
      { name: "PHONE", pattern: /\b\d{3}-\d{3}-\d{4}\b/g, replacement: "[PHONE]" },
      { name: "CARD", pattern: /\b\d{16}\b/g, replacement: "[CARD]" },
      { name: "API_KEY", pattern: /\bsk-[a-zA-Z0-9]{32,}\b/g, replacement: "[API_KEY]" },
    ];
    this.stats = new Map();
    this.validationErrors = [];
  }

  maskNodes(nodes: StringNode[]): StringNode[] {
    return nodes.reduce((result, node) => {
      let modified = false;
      let newValue = node.value;
      const pathString = node.path.join(".");

      for (const rule of this.rules) {
        const matches = node.value.match(rule.pattern);
        if (matches) {
          // Validate each match if validator provided
          if (rule.validate) {
            for (const match of matches) {
              if (!rule.validate(match)) {
                this.validationErrors.push({
                  path: pathString,
                  error: `Invalid ${rule.name} format detected: ${match.substring(0, 10)}...`,
                });
              }
            }
          }

          newValue = newValue.replace(rule.pattern, rule.replacement);
          modified = true;

          // Track statistics
          const count = this.stats.get(rule.name) || 0;
          this.stats.set(rule.name, count + matches.length);

          // Audit log
          console.log(`Anonymized ${rule.name} at path: ${pathString} (${matches.length} occurrences)`);
        }
      }

      if (modified) {
        result.push({ ...node, value: newValue });
      }

      return result;
    }, [] as StringNode[]);
  }

  getStats() {
    return {
      redactions: Object.fromEntries(this.stats),
      validationErrors: this.validationErrors,
      totalRedactions: Array.from(this.stats.values()).reduce((a, b) => a + b, 0),
    };
  }

  reset() {
    this.stats.clear();
    this.validationErrors = [];
  }
}

// Use the validating processor
const processor = new ValidatingProcessor();
const anonymizer = createAnonymizer(processor);

// Process data
const data = {
  users: [
    { email: "alice@example.com", phone: "555-123-4567" },
    { email: "invalid-email", ssn: "123-45-6789" },
  ],
};

const anonymized = anonymizer(data);

// Check statistics and errors
console.log(processor.getStats());
// {
//   redactions: { EMAIL: 2, PHONE: 1, SSN: 1 },
//   validationErrors: [
//     { path: 'users.1.email', error: 'Invalid EMAIL format detected: invalid-em...' }
//   ],
//   totalRedactions: 4
// }

Environment-Aware Anonymization

Apply different anonymization rules based on environment.

import { createAnonymizer } from "langsmith/anonymizer";

function createEnvironmentAwareAnonymizer() {
  const isDevelopment = process.env.NODE_ENV === "development";
  const isProduction = process.env.NODE_ENV === "production";

  if (isDevelopment) {
    // Less aggressive in development, preserve some info for debugging
    return createAnonymizer([
      {
        pattern: /\b[\w\.-]+@[\w\.-]+\.\w+\b/g,
        replace: (match) => {
          // Preserve domain for debugging
          const [, domain] = match.split("@");
          return `[user@${domain}]`;
        },
      },
      {
        pattern: /\bsk-[a-zA-Z0-9]+\b/g,
        replace: (match) => `[KEY-${match.slice(-4)}]`,
      },
      {
        pattern: /\b\d{3}-\d{2}-\d{4}\b/g,
        replace: (match) => `[SSN-**-${match.slice(-4)}]`,
      },
    ]);
  } else if (isProduction) {
    // Aggressive anonymization in production
    return createAnonymizer([
      { pattern: /\b[\w\.-]+@[\w\.-]+\.\w+\b/g, replace: "[EMAIL]" },
      { pattern: /\bsk-[a-zA-Z0-9]+\b/g, replace: "[API_KEY]" },
      { pattern: /\b\d{3}-\d{2}-\d{4}\b/g, replace: "[SSN]" },
      { pattern: /\b\d{16}\b/g, replace: "[CARD]" },
      { pattern: /password.*/gi, replace: "password: [REDACTED]" },
      { pattern: /secret.*/gi, replace: "secret: [REDACTED]" },
    ]);
  }

  // Default/staging: moderate anonymization
  return createAnonymizer([
    { pattern: /\b[\w\.-]+@[\w\.-]+\.\w+\b/g, replace: "[EMAIL]" },
    { pattern: /\bsk-[a-zA-Z0-9]+\b/g, replace: "[API_KEY]" },
    { pattern: /\b\d{3}-\d{2}-\d{4}\b/g, replace: "[SSN]" },
  ]);
}

const anonymizer = createEnvironmentAwareAnonymizer();

Chaining Multiple Anonymizers

Combine multiple anonymizers for layered protection.

import { createAnonymizer, Anonymizer } from "langsmith/anonymizer";

// Create specialized anonymizers
const piiAnonymizer = createAnonymizer([
  { pattern: /\b[\w\.-]+@[\w\.-]+\.\w+\b/g, replace: "[EMAIL]" },
  { pattern: /\b\d{3}-\d{2}-\d{4}\b/g, replace: "[SSN]" },
  { pattern: /\b\d{3}-\d{3}-\d{4}\b/g, replace: "[PHONE]" },
]);

const credentialAnonymizer = createAnonymizer([
  { pattern: /\bsk-[a-zA-Z0-9]+\b/g, replace: "[API_KEY]" },
  { pattern: /password.*/gi, replace: "password: [REDACTED]" },
  { pattern: /Bearer\s+\S+/g, replace: "Bearer [TOKEN]" },
]);

const financialAnonymizer = createAnonymizer([
  { pattern: /\b\d{16}\b/g, replace: "[CARD]" },
  { pattern: /CVV:\s*\d{3,4}/gi, replace: "CVV: [REDACTED]" },
  { pattern: /\b\d{9}\b/g, replace: "[ROUTING]" },
]);

// Chain them together
function chainAnonymizers(...anonymizers: Anonymizer[]): Anonymizer {
  return (data: any) => {
    let result = data;
    for (const anonymizer of anonymizers) {
      result = anonymizer(result);
    }
    return result;
  };
}

const comprehensiveAnonymizer = chainAnonymizers(
  piiAnonymizer,
  credentialAnonymizer,
  financialAnonymizer
);

// Use the chained anonymizer
const sensitiveData = {
  user: "alice@example.com",
  ssn: "123-45-6789",
  phone: "555-123-4567",
  apiKey: "sk-abc123xyz",
  password: "secret123",
  card: "4532123456789012",
  cvv: "123",
  routing: "021000021",
};

const anonymized = comprehensiveAnonymizer(sensitiveData);
// All patterns from all anonymizers will be applied

Conditional Anonymization

Apply anonymization conditionally based on data characteristics.

import { createAnonymizer } from "langsmith/anonymizer";

const conditionalAnonymizer = createAnonymizer((value, node) => {
  const pathString = node.path.join(".");

  // Don't anonymize if explicitly marked as public
  if (pathString.includes(".public.") || pathString.endsWith(".public")) {
    return value;
  }

  // Aggressive anonymization for production data
  if (pathString.includes("production") || pathString.includes("prod")) {
    // Replace entire value if in production path
    return "[PRODUCTION_DATA]";
  }

  // Check data classification tags
  if (node.path.includes("classification")) {
    const classification = value.toLowerCase();
    if (classification.includes("confidential") || classification.includes("sensitive")) {
      return "[CLASSIFIED]";
    }
  }

  // Standard PII patterns for everything else
  let result = value;
  result = result.replace(/\b[\w\.-]+@[\w\.-]+\.\w+\b/g, "[EMAIL]");
  result = result.replace(/\b\d{3}-\d{2}-\d{4}\b/g, "[SSN]");

  return result;
});

Integration Examples

With Traceable Functions

Integrate anonymizers with traceable functions for automatic trace redaction.

import { traceable } from "langsmith/traceable";
import { createAnonymizer } from "langsmith/anonymizer";

// Create comprehensive anonymizer
const anonymizer = createAnonymizer([
  { pattern: /\b[\w\.-]+@[\w\.-]+\.\w+\b/g, replace: "[EMAIL]" },
  { pattern: /\bsk-[a-zA-Z0-9]{32,}\b/g, replace: "[API_KEY]" },
  { pattern: /password["\s:=]+\S+/gi, replace: "password=[REDACTED]" },
  { pattern: /\b\d{3}-\d{2}-\d{4}\b/g, replace: "[SSN]" },
]);

// Apply to both inputs and outputs
const sensitiveFunction = traceable(
  async (input: { userEmail: string; apiKey: string; query: string }) => {
    // Process with real data
    const result = await processQuery(input.query, input.apiKey);
    return { response: result, userEmail: input.userEmail };
  },
  {
    name: "sensitive_query",
    run_type: "chain",
    processInputs: anonymizer,
    processOutputs: anonymizer,
  }
);

// Traces will have anonymized data
await sensitiveFunction({
  userEmail: "alice@company.com",
  apiKey: "sk-abc123xyz789",
  query: "Show me records for SSN 123-45-6789",
});
// Traced as:
// inputs: {
//   userEmail: "[EMAIL]",
//   apiKey: "[API_KEY]",
//   query: "Show me records for SSN [SSN]"
// }

async function processQuery(query: string, apiKey: string) {
  return "Processed result";
}

With LangChain

Anonymize LangChain traces automatically.

import { traceable, getLangchainCallbacks } from "langsmith/langchain";
import { createAnonymizer } from "langsmith/anonymizer";
import { ChatOpenAI } from "@langchain/openai";

const anonymizer = createAnonymizer([
  { pattern: /\b[\w\.-]+@[\w\.-]+\.\w+\b/g, replace: "[EMAIL]" },
  { pattern: /\b\d{3}-\d{2}-\d{4}\b/g, replace: "[SSN]" },
  { pattern: /\b\d{3}-\d{3}-\d{4}\b/g, replace: "[PHONE]" },
]);

const chain = traceable(
  async (input: { question: string }) => {
    const model = new ChatOpenAI({
      callbacks: getLangchainCallbacks(),
    });

    const response = await model.invoke(input.question);
    return { answer: response.content };
  },
  {
    name: "qa_chain",
    run_type: "chain",
    processInputs: anonymizer,
    processOutputs: anonymizer,
  }
);

// Personal information in traces will be anonymized
await chain({
  question: "My email is user@example.com and phone is 555-123-4567, can you help?",
});
// Traced as: "My email is [EMAIL] and phone is [PHONE], can you help?"

With RunTree

Manually anonymize run tree data.

import { RunTree } from "langsmith";
import { createAnonymizer } from "langsmith/anonymizer";

const anonymizer = createAnonymizer([
  { pattern: /\b[\w\.-]+@[\w\.-]+\.\w+\b/g, replace: "[EMAIL]" },
  { pattern: /\bsk-[a-zA-Z0-9]+\b/g, replace: "[API_KEY]" },
  { pattern: /password.*/gi, replace: "password: [REDACTED]" },
]);

// Create run with sensitive data
const runTree = new RunTree({
  name: "user_registration",
  run_type: "chain",
  inputs: {
    email: "newuser@example.com",
    password: "secret123",
    apiKey: "sk-abc123xyz789",
  },
});

// Manually anonymize before posting
const anonymizedInputs = anonymizer(runTree.inputs);
runTree.inputs = anonymizedInputs;

// Continue with run
await runTree.end({
  outputs: anonymizer({
    success: true,
    userEmail: "newuser@example.com",
  }),
});

await runTree.postRun();

With Client Configuration

Set global anonymization at the client level.

import { Client } from "langsmith";
import { createAnonymizer } from "langsmith/anonymizer";

const anonymizer = createAnonymizer([
  { pattern: /\b[\w\.-]+@[\w\.-]+\.\w+\b/g, replace: "[EMAIL]" },
  { pattern: /\bsk-[a-zA-Z0-9]+\b/g, replace: "[API_KEY]" },
  { pattern: /\b\d{3}-\d{2}-\d{4}\b/g, replace: "[SSN]" },
]);

// Apply anonymization at client level
const client = new Client({
  apiKey: process.env.LANGSMITH_API_KEY,
  anonymizer: anonymizer,  // All traces through this client will be anonymized
});

// Or use hideInputs/hideOutputs with anonymizer
const client2 = new Client({
  hideInputs: (inputs) => anonymizer(inputs),
  hideOutputs: (outputs) => anonymizer(outputs),
});

Global Anonymization Strategy

Set up a global anonymization strategy for your application.

import { traceable, TraceableConfig } from "langsmith/traceable";
import { createAnonymizer } from "langsmith/anonymizer";

// Create global anonymizer
export const globalAnonymizer = createAnonymizer([
  // PII
  { pattern: /\b[\w\.-]+@[\w\.-]+\.\w+\b/g, replace: "[EMAIL]" },
  { pattern: /\b\d{3}-\d{2}-\d{4}\b/g, replace: "[SSN]" },
  { pattern: /\b\d{3}-\d{3}-\d{4}\b/g, replace: "[PHONE]" },

  // Credentials
  { pattern: /\bsk-[a-zA-Z0-9]+\b/g, replace: "[API_KEY]" },
  { pattern: /password.*/gi, replace: "password: [REDACTED]" },
  { pattern: /Bearer\s+\S+/g, replace: "Bearer [TOKEN]" },

  // Financial
  { pattern: /\b\d{16}\b/g, replace: "[CARD]" },
]);

// Helper to create anonymized traceable functions
export function createAnonymizedTraceable<T extends (...args: any[]) => any>(
  func: T,
  config?: Omit<TraceableConfig<T>, "processInputs" | "processOutputs">
) {
  return traceable(func, {
    ...config,
    processInputs: globalAnonymizer,
    processOutputs: globalAnonymizer,
  } as TraceableConfig<T>);
}

// Use throughout your application
const myFunction = createAnonymizedTraceable(
  async (input: { email: string; data: string }) => {
    return { result: processData(input.data), userEmail: input.email };
  },
  { name: "my_function", run_type: "chain" }
);

// All traces automatically anonymized
await myFunction({
  email: "user@example.com",
  data: "Process this data",
});

function processData(data: string) {
  return data.toUpperCase();
}

Best Practices

Testing Anonymization

Always test your anonymization rules to ensure they work correctly.

import { createAnonymizer } from "langsmith/anonymizer";

const anonymizer = createAnonymizer([
  { pattern: /\b[\w\.-]+@[\w\.-]+\.\w+\b/g, replace: "[EMAIL]" },
  { pattern: /\b\d{3}-\d{2}-\d{4}\b/g, replace: "[SSN]" },
  { pattern: /\bsk-[a-zA-Z0-9]{32,}\b/g, replace: "[API_KEY]" },
]);

// Test cases
const testCases = [
  {
    name: "Email redaction",
    input: { email: "test@example.com" },
    expected: { email: "[EMAIL]" },
  },
  {
    name: "SSN redaction",
    input: { ssn: "123-45-6789" },
    expected: { ssn: "[SSN]" },
  },
  {
    name: "Nested email",
    input: { nested: { user: { email: "user@test.com" } } },
    expected: { nested: { user: { email: "[EMAIL]" } } },
  },
  {
    name: "Multiple patterns",
    input: { text: "Contact: alice@example.com, SSN: 123-45-6789" },
    expected: { text: "Contact: [EMAIL], SSN: [SSN]" },
  },
  {
    name: "API key in string",
    input: { config: "api_key=sk-abc123xyz789def456ghi" },
    expected: { config: "api_key=[API_KEY]" },
  },
];

// Run tests
for (const testCase of testCases) {
  const result = anonymizer(testCase.input);
  const passed = JSON.stringify(result) === JSON.stringify(testCase.expected);

  if (passed) {
    console.log(`✓ ${testCase.name}`);
  } else {
    console.error(`✗ ${testCase.name}`);
    console.error(`  Expected: ${JSON.stringify(testCase.expected)}`);
    console.error(`  Got: ${JSON.stringify(result)}`);
  }
}

// Unit tests with Jest/Vitest
describe("Anonymizer", () => {
  it("should redact emails", () => {
    const result = anonymizer({ email: "test@example.com" });
    expect(result.email).toBe("[EMAIL]");
  });

  it("should handle nested structures", () => {
    const result = anonymizer({
      level1: {
        level2: {
          email: "nested@example.com",
        },
      },
    });
    expect(result.level1.level2.email).toBe("[EMAIL]");
  });

  it("should handle arrays", () => {
    const result = anonymizer({
      users: [
        { email: "user1@example.com" },
        { email: "user2@example.com" },
      ],
    });
    expect(result.users[0].email).toBe("[EMAIL]");
    expect(result.users[1].email).toBe("[EMAIL]");
  });
});

Performance Considerations

For high-volume tracing, consider performance implications.

import { createAnonymizer } from "langsmith/anonymizer";

// Use compiled RegExp patterns (faster than string patterns)
const optimizedAnonymizer = createAnonymizer([
  { pattern: /\b[\w\.-]+@[\w\.-]+\.\w+\b/g, replace: "[EMAIL]" },  // Pre-compiled
  { pattern: /\b\d{3}-\d{2}-\d{4}\b/g, replace: "[SSN]" },         // Pre-compiled
]);

// Limit depth for deeply nested objects
const boundedAnonymizer = createAnonymizer(
  [{ pattern: /sensitive/gi, replace: "[REDACTED]" }],
  { maxDepth: 5 }  // Only traverse 5 levels deep
);

// Exclude large safe objects from traversal
const selectiveAnonymizer = createAnonymizer(
  [{ pattern: /\b[\w\.-]+@[\w\.-]+\.\w+\b/g, replace: "[EMAIL]" }],
  {
    excludePaths: [
      ["metadata", "large_safe_object"],
      ["config", "public_settings"],
      ["logs"],  // Don't traverse logs
    ],
  }
);

// Benchmark anonymization
function benchmarkAnonymizer(anonymizer: any, data: any, iterations: number = 1000) {
  const start = performance.now();

  for (let i = 0; i < iterations; i++) {
    anonymizer(data);
  }

  const end = performance.now();
  const avgTime = (end - start) / iterations;

  console.log(`Average anonymization time: ${avgTime.toFixed(2)}ms`);
  return avgTime;
}

// Example benchmark
const testData = {
  users: Array.from({ length: 100 }, (_, i) => ({
    email: `user${i}@example.com`,
    ssn: "123-45-6789",
    nested: {
      phone: "555-123-4567",
    },
  })),
};

benchmarkAnonymizer(optimizedAnonymizer, testData);

Validation and Error Handling

Ensure anonymization doesn't corrupt data structure.

import { createAnonymizer } from "langsmith/anonymizer";

// Anonymizer with validation
const safeAnonymizer = createAnonymizer((value, node) => {
  try {
    // Try to anonymize
    let result = value;
    result = result.replace(/\b[\w\.-]+@[\w\.-]+\.\w+\b/g, "[EMAIL]");
    result = result.replace(/\b\d{3}-\d{2}-\d{4}\b/g, "[SSN]");

    // Validate result is still a string
    if (typeof result !== 'string') {
      console.error(`Anonymization produced non-string at path: ${node.path.join(".")}`);
      return value;  // Return original on error
    }

    return result;
  } catch (error) {
    console.error(`Anonymization error at path ${node.path.join(".")}: ${error.message}`);
    return value;  // Return original on error
  }
});

// Verify anonymization doesn't break JSON
function verifyAnonymization(anonymizer: any, data: any) {
  const anonymized = anonymizer(data);

  // Test that result can be serialized
  try {
    JSON.stringify(anonymized);
    console.log("✓ Anonymized data is JSON-serializable");
  } catch (error) {
    console.error("✗ Anonymized data cannot be serialized:", error.message);
  }

  // Test that structure is preserved
  const originalKeys = Object.keys(data).sort();
  const anonymizedKeys = Object.keys(anonymized).sort();

  if (JSON.stringify(originalKeys) === JSON.stringify(anonymizedKeys)) {
    console.log("✓ Data structure preserved");
  } else {
    console.error("✗ Data structure changed");
    console.error("  Original keys:", originalKeys);
    console.error("  Anonymized keys:", anonymizedKeys);
  }
}

Audit Logging

Track what gets anonymized for compliance and debugging.

import { createAnonymizer, StringNode } from "langsmith/anonymizer";

class AuditingAnonymizer implements StringNodeProcessor {
  private auditLog: Array<{
    timestamp: string;
    path: string;
    type: string;
    redacted: boolean;
  }> = [];

  private rules = [
    { name: "EMAIL", pattern: /\b[\w\.-]+@[\w\.-]+\.\w+\b/g, replacement: "[EMAIL]" },
    { name: "SSN", pattern: /\b\d{3}-\d{2}-\d{4}\b/g, replacement: "[SSN]" },
    { name: "API_KEY", pattern: /\bsk-[a-zA-Z0-9]+\b/g, replacement: "[API_KEY]" },
  ];

  maskNodes(nodes: StringNode[]): StringNode[] {
    return nodes.reduce((result, node) => {
      let modified = false;
      let newValue = node.value;
      const pathString = node.path.join(".");

      for (const rule of this.rules) {
        if (rule.pattern.test(node.value)) {
          newValue = newValue.replace(rule.pattern, rule.replacement);
          modified = true;

          // Log redaction
          this.auditLog.push({
            timestamp: new Date().toISOString(),
            path: pathString,
            type: rule.name,
            redacted: true,
          });
        }
      }

      if (modified) {
        result.push({ ...node, value: newValue });
      }

      return result;
    }, [] as StringNode[]);
  }

  getAuditLog() {
    return this.auditLog;
  }

  exportAuditLog(format: "json" | "csv" = "json") {
    if (format === "json") {
      return JSON.stringify(this.auditLog, null, 2);
    } else {
      // CSV export
      const header = "Timestamp,Path,Type,Redacted\n";
      const rows = this.auditLog
        .map(entry => `${entry.timestamp},${entry.path},${entry.type},${entry.redacted}`)
        .join("\n");
      return header + rows;
    }
  }
}

const auditor = new AuditingAnonymizer();
const auditedAnonymizer = createAnonymizer(auditor);

// Use and review audit log
const data = {
  user: "alice@example.com",
  ssn: "123-45-6789",
  apiKey: "sk-abc123",
};

const anonymized = auditedAnonymizer(data);
console.log("Audit log:", auditor.exportAuditLog("json"));

Advanced Use Cases

Regex Library for Common Patterns

Create a reusable library of anonymization patterns.

import { createAnonymizer, StringNodeRule } from "langsmith/anonymizer";

// Define pattern library
export const AnonymizationPatterns = {
  // Personal Information
  EMAIL: { pattern: /\b[\w\.-]+@[\w\.-]+\.\w+\b/g, replace: "[EMAIL]" },
  SSN: { pattern: /\b\d{3}-\d{2}-\d{4}\b/g, replace: "[SSN]" },
  PHONE_US: { pattern: /\b\d{3}[-.\s]?\d{3}[-.\s]?\d{4}\b/g, replace: "[PHONE]" },
  PHONE_INTL: { pattern: /\+\d{1,3}[-.\s]?\d{1,14}/g, replace: "[PHONE_INTL]" },

  // Financial
  CREDIT_CARD: { pattern: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g, replace: "[CARD]" },
  CVV: { pattern: /CVV:\s*\d{3,4}/gi, replace: "CVV: [REDACTED]" },
  ROUTING_NUMBER: { pattern: /\b\d{9}\b/g, replace: "[ROUTING]" },

  // API Keys
  OPENAI_KEY: { pattern: /\bsk-[a-zA-Z0-9]{32,}\b/g, replace: "[OPENAI_KEY]" },
  ANTHROPIC_KEY: { pattern: /\bsk-ant-[a-zA-Z0-9\-]{95}\b/g, replace: "[ANTHROPIC_KEY]" },
  GITHUB_TOKEN: { pattern: /\bghp_[a-zA-Z0-9]{36}\b/g, replace: "[GITHUB_TOKEN]" },
  GITHUB_OAUTH: { pattern: /\bgho_[a-zA-Z0-9]{36}\b/g, replace: "[GITHUB_OAUTH]" },
  GITLAB_TOKEN: { pattern: /\bglpat-[a-zA-Z0-9_\-]{20}\b/g, replace: "[GITLAB_TOKEN]" },
  AWS_KEY: { pattern: /\bAKIA[0-9A-Z]{16}\b/g, replace: "[AWS_KEY]" },
  GOOGLE_API_KEY: { pattern: /\bAIza[0-9A-Za-z\-_]{35}\b/g, replace: "[GOOGLE_API_KEY]" },

  // Secrets
  BEARER_TOKEN: { pattern: /Bearer\s+[a-zA-Z0-9\-._~+\/]+=*/g, replace: "Bearer [TOKEN]" },
  PASSWORD: { pattern: /password["\s:=]+\S+/gi, replace: "password=[REDACTED]" },
  SECRET: { pattern: /secret["\s:=]+\S+/gi, replace: "secret=[REDACTED]" },

  // Network
  IPV4: { pattern: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, replace: "[IP]" },
  IPV6: { pattern: /\b[0-9a-f]{1,4}:[0-9a-f:]+\b/gi, replace: "[IPv6]" },
  URL_WITH_CREDS: {
    pattern: /https?:\/\/[^:]+:[^@]+@[^\s]+/g,
    replace: (match: string) => {
      try {
        const url = new URL(match);
        return `${url.protocol}//[USER]:[PASS]@${url.host}${url.pathname}`;
      } catch {
        return "[URL_WITH_CREDENTIALS]";
      }
    },
  },
};

// Create preset anonymizers
export function createPIIAnonymizer() {
  return createAnonymizer([
    AnonymizationPatterns.EMAIL,
    AnonymizationPatterns.SSN,
    AnonymizationPatterns.PHONE_US,
  ]);
}

export function createCredentialAnonymizer() {
  return createAnonymizer([
    AnonymizationPatterns.OPENAI_KEY,
    AnonymizationPatterns.ANTHROPIC_KEY,
    AnonymizationPatterns.GITHUB_TOKEN,
    AnonymizationPatterns.AWS_KEY,
    AnonymizationPatterns.BEARER_TOKEN,
    AnonymizationPatterns.PASSWORD,
    AnonymizationPatterns.SECRET,
  ]);
}

export function createFinancialAnonymizer() {
  return createAnonymizer([
    AnonymizationPatterns.CREDIT_CARD,
    AnonymizationPatterns.CVV,
    AnonymizationPatterns.ROUTING_NUMBER,
  ]);
}

export function createComprehensiveAnonymizer() {
  const allPatterns = Object.values(AnonymizationPatterns);
  return createAnonymizer(allPatterns as StringNodeRule[]);
}

// Use preset anonymizers
const piiAnon = createPIIAnonymizer();
const credAnon = createCredentialAnonymizer();
const finAnon = createFinancialAnonymizer();
const fullAnon = createComprehensiveAnonymizer();

Circular Reference Handling

Handle circular references in complex data structures.

import { createAnonymizer } from "langsmith/anonymizer";

const circularSafeAnonymizer = createAnonymizer(
  [{ pattern: /\b[\w\.-]+@[\w\.-]+\.\w+\b/g, replace: "[EMAIL]" }],
  { handleCircular: true }  // Enable circular reference handling
);

// Create data with circular reference
const user: any = {
  name: "Alice",
  email: "alice@example.com",
};
user.self = user;  // Circular reference

// Anonymize without error
const anonymized = circularSafeAnonymizer(user);
console.log(anonymized.email);  // "[EMAIL]"
console.log(anonymized.self.email);  // "[EMAIL]"
console.log(anonymized.self === anonymized);  // true (circular ref preserved)

Key Anonymization

Anonymize sensitive data in object keys, not just values.

import { createAnonymizer } from "langsmith/anonymizer";

const keyAnonymizer = createAnonymizer(
  (value, node) => {
    // Anonymize values
    return value.replace(/\b[\w\.-]+@[\w\.-]+\.\w+\b/g, "[EMAIL]");
  },
  { anonymizeKeys: true }  // Also anonymize keys
);

const data = {
  "user_email_alice@example.com": "value1",
  "contact": "bob@example.com",
};

const anonymized = keyAnonymizer(data);
// Keys are also anonymized:
// {
//   "user_email_[EMAIL]": "value1",
//   "contact": "[EMAIL]"
// }

See Also

  • Tracing - Configure tracing with anonymization
  • Client API - LangSmith client documentation
  • Run Trees - Manual run tree creation
  • Getting Started - Basic setup and configuration