# O11y Reporter

A lightweight telemetry reporting service for Salesforce extensions that enables sending metrics and events to Salesforce's observability platform.

## Usage

The O11y Reporter service provides a simple way to send telemetry events and metrics to Salesforce's observability platform. Here's how to use it:

### 1. Initialize the Service

```typescript
import { O11yService } from "@salesforce/o11y-reporter";

// Get the singleton instance
const o11yService = O11yService.getInstance(extensionName);

// Initialize with your extension name and upload endpoint (static endpoint only)
await o11yService.initialize(
  "your-extension-name",
  "https://your-upload-endpoint",
);
```

**Dynamic endpoint (authenticated org):** To send telemetry to the current org’s endpoint (with automatic fallback to the static endpoint if the connection is unavailable), pass an optional `getConnection` callback and, if needed, an options object with `dynamicO11yUploadEndpointPath`. If you omit the path, a default is used.

```typescript
import { O11yService, type Connection } from "@salesforce/o11y-reporter";

const o11yService = O11yService.getInstance(extensionName);

await o11yService.initialize(
  "your-extension-name",
  "https://fallback-static-endpoint",
  () => yourWorkspaceContext.getConnection(),
  { dynamicO11yUploadEndpointPath: "/your/telemetry/path" },
);
```

The `Connection` type is re-exported from `@salesforce/core`; depend on it when using `getConnection`.

### 2. Send Telemetry Events

#### Using Default Schema

```typescript
// Send a telemetry event with properties (uses default sf_a4dInstrumentation schema)
o11yService.logEvent({
  name: "extension/eventName",
  properties: {
    // Add your custom properties here
    customProperty: "value",
  },
  measurements: {
    // Optional measurements
    duration: 100,
  },
});

// Send an exception event
o11yService.logEvent({
  exception: new Error("Error message"),
  properties: {
    // Add your custom properties here
    errorType: "RuntimeError",
  },
  measurements: {
    // Optional measurements
    errorCount: 1,
  },
});
```

#### Using Custom Schema

The service supports consumer-provided O11y schemas. This allows you to use custom schemas from the `o11y_schema` package instead of the default `sf_a4dInstrumentation` schema.

**Step 1: Add `o11y_schema` to your `package.json`:**

```json
{
  "dependencies": {
    "o11y_schema": "^256.154.0"
  }
}
```

**Step 2: Import and use the schema with `logEventWithSchema(properties, schema)` (exactly two parameters):**

```typescript
import { O11yService } from "@salesforce/o11y-reporter";
// Import the schema object from o11y_schema
// @ts-expect-error - o11y_schema package doesn't provide TypeScript declarations
import { a4dInstrumentationSchema } from "o11y_schema/sf_a4dInstrumentation";

const o11yService = O11yService.getInstance("your-extension");
await o11yService.initialize("your-extension", "https://your-endpoint");

// Two parameters only: (properties, schema). Shape of properties depends on your schema.
o11yService.logEventWithSchema(
  { customProperty: "value" },
  a4dInstrumentationSchema,
);

// You can also use different schemas for different events
// @ts-expect-error - o11y_schema package doesn't provide TypeScript declarations
import { anotherSchema } from "o11y_schema/another_schema";

o11yService.logEventWithSchema({ foo: "bar" }, anotherSchema);
```

**Note:** `logEventWithSchema(properties, schema)` takes exactly **two** parameters: the event properties object and the schema. You must provide a valid schema object (e.g. `Record<string, unknown>`). If you don't need a custom schema, use `logEvent(properties)` which uses the default schema.

**Behavior:** Events sent with `logEventWithSchema(properties, schema)` are encoded using the schema you pass (e.g. `PdpEvent`, `a4dInstrumentationSchema`). They are not wrapped in the default A4dInstrumentation schema; the payload uses your schema's format directly (e.g. `sf.pdp.PdpEvent` in the binary payload).

**Note:** The `o11y_schema` package doesn't provide TypeScript declarations. You may need to add `@ts-expect-error` comments or configure your TypeScript with `skipLibCheck: true` in `tsconfig.json`.

**With automatic batching enabled, events are automatically uploaded based on threshold (50KB) or periodic flush (30 seconds). You can also manually flush if needed:**

```typescript
await o11yService.forceFlush();
```

### 3. Enable Automatic Batching

The service supports automatic batching of events for efficient telemetry collection. Events are automatically buffered and uploaded based on size threshold or periodic flush intervals.

```typescript
// Enable automatic batching with default settings (30-second flush interval)
const cleanup = o11yService.enableAutoBatching();

// Or customize the batching behavior
const cleanup = o11yService.enableAutoBatching({
  flushInterval: 30_000, // 30 seconds (default)
  enableShutdownHook: true, // Enable shutdown hooks (default: true)
  enableBeforeExitHook: true, // Enable beforeExit hook (default: true)
});

// Later, if you need to stop batching
cleanup();
```

**Batching Behavior:**

- Events are buffered in memory until upload conditions are met
- **Threshold-based upload**: Events are uploaded when buffer size reaches 50KB
- **Periodic flush**: Events are automatically flushed every 30 seconds (configurable)
- **Shutdown hooks**: Events are automatically flushed on application shutdown (SIGINT, SIGTERM, beforeExit)

**Benefits:**

- Reduces network overhead by batching multiple events
- Improves performance by avoiding per-event uploads
- Ensures events are not lost on application shutdown

### 4. Manual Flush

You can manually flush buffered events at any time:

```typescript
// Force an immediate flush of all buffered events
await o11yService.forceFlush();

// Or use the upload method (alias for forceFlush when batching is enabled)
await o11yService.upload();
```

### 5. Check Batch Status

Monitor the current batch status:

```typescript
const status = o11yService.getBatchStatus();
console.log(`Buffer size: ${status.estimatedByteSize} bytes`);
console.log(`Has data: ${status.hasData}`);
console.log(`Over threshold: ${status.isOverThreshold}`);
```

## Configuration

The service can be configured with the following options:

- `o11yUploadEndpoint`: The endpoint URL for uploading telemetry data

### Batching Options

When enabling automatic batching, you can configure the following options:

```typescript
interface BatchingOptions {
  /** Periodic flush interval in milliseconds (default: 30000) */
  flushInterval?: number;
  /** Buffer size threshold in bytes before triggering upload (default: 50000 = 50KB) */
  thresholdBytes?: number;
  /** Threshold check interval in milliseconds (default: 2000) */
  checkInterval?: number;
  /** Enable shutdown hooks (default: true) */
  enableShutdownHook?: boolean;
  /** Enable beforeExit hook (default: true). Note: beforeExit won't fire for STDIO servers where stdin stays open */
  enableBeforeExitHook?: boolean;
}
```

**Configuration Examples:**

```typescript
// Default configuration (30-second flush, shutdown hooks enabled)
o11yService.enableAutoBatching();

// Custom flush interval (60 seconds)
o11yService.enableAutoBatching({ flushInterval: 60_000 });

// Disable shutdown hooks (not recommended)
o11yService.enableAutoBatching({ enableShutdownHook: false });

// Disable beforeExit hook (useful for STDIO servers)
o11yService.enableAutoBatching({ enableBeforeExitHook: false });
```

## Best Practices

1. **Initialize Early**: Initialize the service as early as possible in your extension's lifecycle.

2. **Error Handling**: Always wrap telemetry calls in try-catch blocks to prevent them from affecting your main application flow.

3. **Property Naming**: Use consistent property names and avoid sending sensitive information.

4. **Enable Automatic Batching**: Use `enableAutoBatching()` to automatically batch and upload events efficiently. This is recommended for most use cases.

5. **Manual Flush for Critical Events**: For critical events that need immediate upload, call `forceFlush()` after logging the event.

## Example Implementation

Here's a complete example of how to use the O11y Reporter in your extension:

```typescript
import { O11yService } from "@salesforce/o11y-reporter";

class YourExtension {
  private o11yService: O11yService;

  constructor() {
    this.o11yService = O11yService.getInstance(extensionName);
  }

  async initialize() {
    await this.o11yService.initialize(
      "your-extension",
      "https://your-endpoint",
    );

    // Enable automatic batching for efficient telemetry collection
    this.o11yService.enableAutoBatching({
      flushInterval: 30_000, // 30 seconds
      enableShutdownHook: true, // Flush on shutdown
    });
  }

  async trackUserAction(actionName: string, properties: Record<string, any>) {
    try {
      // Using default schema
      this.o11yService.logEvent({
        name: `user/${actionName}`,
        properties: {
          ...properties,
          timestamp: new Date().toISOString(),
        },
      });

      // With batching enabled, events are automatically uploaded
      // For critical events, you can force an immediate flush:
      await this.o11yService.forceFlush();
    } catch (error) {
      // Log error but don't throw
      console.error("Failed to send telemetry:", error);
    }
  }

  async trackUserActionWithSchema(
    actionName: string,
    properties: Record<string, any>,
    schema: Record<string, unknown>,
  ) {
    try {
      // Two parameters: caller's properties (conform to schema) and schema.
      this.o11yService.logEventWithSchema(properties, schema);

      // With batching enabled, events are automatically uploaded
      await this.o11yService.forceFlush();
    } catch (error) {
      console.error("Failed to send telemetry:", error);
    }
  }

  async trackError(error: Error, context: Record<string, any>) {
    try {
      this.o11yService.logEvent({
        exception: error,
        properties: {
          ...context,
          timestamp: new Date().toISOString(),
        },
      });

      await this.o11yService.upload();
    } catch (err) {
      console.error("Failed to send error telemetry:", err);
    }
  }
}
```

## License

This project is licensed under the Terms of Use. See the [LICENSE.txt](LICENSE.txt) file for details.
