npm package: @mukhy/mtn-telemetry-sdk
Description: MTN Telemetry SDK for React and React Native applications
Author: Muutassim Mukhtar
Latest version: 2.2.5

# MTN Telemetry SDK

A lightweight OpenTelemetry wrapper for React and React Native applications. Collects **traces**, **metrics**, **errors**, and **logs** with minimal configuration and exports them to any OTLP-compatible backend (Elastic APM, Grafana, Jaeger, etc.).

## Features

- One-call initialization for React and React Native
- Automatic instrumentation for:
  - **Fetch** requests (HTTP method, status, URL, duration)
  - **React Navigation** screen transitions
  - **AppState** foreground/background transitions
  - **Console** log/warn/error capture as OTLP log records
- Custom business event tracking (`trackEvent`)
- Async operation tracing with duration and error capture (`trackOperation`)
- Manual error recording (`recordError`)
- Global crash reporting for unhandled JS errors and promise rejections (`installCrashReporting`)
- Multi-step business journey tracking (`BusinessTracer`)
- Tracked UI component (`TrackedPressable`)
- Protobuf (binary) OTLP encoding for Elastic APM compatibility
- Debug mode with verbose OpenTelemetry diagnostic logging
- Built-in MTN SRE metadata on every telemetry entry
- TypeScript-ready with full type definitions

## Requirements

| Dependency   | Minimum version |
| ------------ | --------------- |
| React        | >= 18.0.0       |
| React Native | >= 0.72.0       |

## Installation

```bash
npm install @mukhy/mtn-telemetry-sdk
```

or

```bash
yarn add @mukhy/mtn-telemetry-sdk
```

---

## Quick Start

Create `src/telemetry.ts`:

```typescript
import { MTNOTel, type OTelRNOptions } from '@mukhy/mtn-telemetry-sdk';
import { Platform } from 'react-native';
import DeviceInfo from 'react-native-device-info';

export async function initTelemetry(
  navigationRef?: OTelRNOptions['navigationRef'],
) {
  const options: OTelRNOptions = {
    serviceName: `myApp-${Platform.OS === 'ios' ? 'iOS' : 'Android'}`,
    environment: 'production',
    debug: false,
    navigationRef,
    samplingRatio: 1,
    release: DeviceInfo.getVersion(),
    enableConsole: true,
    consoleMinSeverity: 'log',
    otlp: {
      tracesUrl: 'https://your-apm-server:8200/v1/traces',
      metricsUrl: 'https://your-apm-server:8200/v1/metrics',
      logsUrl: 'https://your-apm-server:8200/v1/logs',
      headers: {},
    },
  };

  return MTNOTel.init(options);
}
```

### 2. Initialize in your root component

In `App.tsx`:

```typescript
import React, { useEffect, useRef } from 'react';
import { initTelemetry } from './telemetry';
import { installCrashReporting } from '@mukhy/mtn-telemetry-sdk';
import { navigationRef } from './routes/navigationRef';

function App() {
  const telemetryRef = useRef(null);

  useEffect(() => {
    let uninstallCrash;

    initTelemetry(navigationRef.current)
      .then(instance => {
        telemetryRef.current = instance;
        uninstallCrash = installCrashReporting();
        console.log('Telemetry SDK + crash reporting initialized');
      })
      .catch(err => console.warn('Telemetry init failed', err));

    return () => {
      uninstallCrash?.();
      telemetryRef.current?.flushAndShutdown();
    };
  }, []);

  return (
    <NavigationContainer ref={navigationRef}>
      {/* ... your app */}
    </NavigationContainer>
  );
}
```

That's it. Fetch requests, screen transitions, AppState changes, unhandled errors, promise rejections, and console output are now automatically captured.

---

## Configuration Options (`OTelRNOptions`)

| Option               | Type                      | Default              | Description                                                            |
| -------------------- | ------------------------- | -------------------- | ---------------------------------------------------------------------- |
| `serviceName`        | `string`                  | **(required)**       | Service name in the OpenTelemetry resource.                            |
| `environment`        | `string`                  | `undefined`          | Deployment environment tag (e.g. `production`, `staging`).             |
| `release`            | `string`                  | `undefined`          | Release/build version identifier.                                      |
| `debug`              | `boolean`                 | `false`              | When `true`, enables verbose OTel diagnostic logging to the console.   |
| `otlp`               | `object`                  | `undefined`          | OTLP exporter configuration.                                           |
| `otlp.tracesUrl`     | `string`                  | `http://localhost:4318` | OTLP HTTP endpoint for traces.                                      |
| `otlp.metricsUrl`    | `string`                  | `undefined`          | OTLP HTTP endpoint for metrics.                                        |
| `otlp.logsUrl`       | `string`                  | `undefined`          | OTLP HTTP endpoint for logs.                                           |
| `otlp.headers`       | `Record<string, string>`  | `{}`                 | Custom headers for every OTLP export request (auth, etc.).             |
| `enableFetch`        | `boolean`                 | `true`               | Toggle automatic `fetch` instrumentation.                              |
| `enableNavigation`   | `boolean`                 | `true`               | Toggle React Navigation instrumentation.                               |
| `enableAppState`     | `boolean`                 | `true`               | Toggle AppState foreground/background spans and metrics.                |
| `enableConsole`      | `boolean`                 | `false`              | Capture `console.log/warn/error` and export as OTLP log records.       |
| `consoleMinSeverity` | `'log' \| 'warn' \| 'error'` | `'log'`          | Minimum console severity to capture (when `enableConsole` is true).     |
| `navigationRef`      | `NavigationContainerLike` | `undefined`          | Navigation container ref to attach during init.                        |
| `samplingRatio`      | `number`                  | `1.0`                | Trace sampling ratio (`0.0` – `1.0`).                                  |
| `attributes`         | `Record<string, any>`     | `{}`                 | Extra resource attributes merged into the OTel resource.               |

---

## API Reference

### `trackEvent(name, attributes?, isError?)`

Record a discrete business event as a span. The span is created and immediately ended.

```typescript
import { trackEvent } from '@mukhy/mtn-telemetry-sdk';

// Successful event
trackEvent('purchase.completed', {
  'purchase.product_name': '75MB Daily Plan',
  'purchase.amount': 75,
  'purchase.transaction_id': 'TXN-12345',
  'purchase.payment_method': 'Airtime',
});

// Error event (sets span status to ERROR)
trackEvent('purchase.failed', {
  'purchase.product_name': '75MB Daily Plan',
  'purchase.error': 'Insufficient balance',
}, true);
```

| Param        | Type                                      | Description                                |
| ------------ | ----------------------------------------- | ------------------------------------------ |
| `name`       | `string`                                  | Event/span name.                           |
| `attributes` | `Record<string, string\|number\|boolean>` | Key-value pairs attached to the span.      |
| `isError`    | `boolean`                                 | If `true`, marks the span status as ERROR. |

---

### `trackOperation(name, attributes, fn)`

Wrap an async function and measure it as a span. Captures duration, result, and any thrown errors automatically.

```typescript
import { trackOperation } from '@mukhy/mtn-telemetry-sdk';

const result = await trackOperation(
  'auth.login_with_otp',
  { 'auth.method': 'sms', 'auth.phone': '+234...' },
  () => AuthService.loginWithSMS(phone, otp),
);
```

| Param        | Type                                      | Description                                |
| ------------ | ----------------------------------------- | ------------------------------------------ |
| `name`       | `string`                                  | Operation/span name.                       |
| `attributes` | `Record<string, string\|number\|boolean>` | Key-value pairs attached to the span.      |
| `fn`         | `() => Promise<T>`                        | The async function to execute and measure. |

**Returns:** The resolved value of `fn()`. If `fn()` throws, the error is recorded on the span and re-thrown.

---

### `recordError(error, attributes?)`

Manually record a caught error as a traced span with stack trace.

```typescript
import { recordError } from '@mukhy/mtn-telemetry-sdk';

try {
  await riskyOperation();
} catch (err) {
  recordError(err, { screen: 'PaymentSummary', step: 'subscription_api' });
}
```

| Param        | Type                                      | Description                                  |
| ------------ | ----------------------------------------- | -------------------------------------------- |
| `error`      | `unknown`                                 | The error or exception to record.            |
| `attributes` | `Record<string, string\|number\|boolean>` | Extra context attached to the error span.    |

---

### `installCrashReporting()`

Install global handlers that automatically capture:
- Unhandled JavaScript errors (via React Native's `ErrorUtils`)
- Unhandled promise rejections

Call once after `MTNOTel.init()`. Returns a cleanup function.

```typescript
import { installCrashReporting } from '@mukhy/mtn-telemetry-sdk';

const uninstall = installCrashReporting();

// On unmount:
uninstall();
```

---

### `BusinessTracer`

Track multi-step user journeys (e.g. onboarding, purchase flow).

```typescript
import { BusinessTracer } from '@mukhy/mtn-telemetry-sdk';

BusinessTracer.start('PurchaseFlow', { 'bundle.category': 'DataPlan' });
BusinessTracer.step('BundleSelected', { 'bundle.name': '75MB Daily Plan' });
BusinessTracer.step('PaymentAuthorized', { 'payment.method': 'Airtime' });
BusinessTracer.complete({ 'purchase.status': 'success', 'transaction.id': 'TXN-123' });
```

| Method     | Parameters                    | Description                       |
| ---------- | ----------------------------- | --------------------------------- |
| `start`    | `(journeyName, attributes?)`  | Begin a named journey.            |
| `step`     | `(stepName, attributes?)`     | Record a step within the journey. |
| `complete` | `(attributes?)`               | Mark the journey as complete.     |

---

### `TrackedPressable`

A drop-in replacement for React Native's `Pressable` that automatically records user taps as business steps.

```tsx
import { TrackedPressable } from '@mukhy/mtn-telemetry-sdk';

<TrackedPressable
  accessibilityLabel="Buy Now"
  onPress={() => handleBuyNow()}
>
  <Text>Buy Now</Text>
</TrackedPressable>
```

Each press creates a `tap:Buy Now` business step span.

---

### `startActiveSpan(name, fn, kind?)`

Create a custom active span for full manual control. The span is automatically ended and errors are captured.

```typescript
import { startActiveSpan } from '@mukhy/mtn-telemetry-sdk';

const data = await startActiveSpan('fetchUserProfile', async (span) => {
  span.setAttribute('user.id', userId);
  const response = await fetch(`/api/users/${userId}`);
  return response.json();
});
```

---

## Built-in Instrumentations

| Instrumentation      | What it captures                                                                          |
| -------------------- | ----------------------------------------------------------------------------------------- |
| **Fetch**            | Every `fetch()` call: method, URL, status code, duration. OTLP export URLs auto-excluded. |
| **React Navigation** | Screen transitions: route name, duration on each screen.                                  |
| **AppState**         | Foreground/background transitions as spans and metrics.                                   |
| **Console**          | `console.log/warn/error` output as OTLP log records with trace context correlation.       |

---

## Built-in Metadata

Every telemetry entry automatically includes the following resource attributes:

| Field (Kibana)          | Value                      | Description                      |
| ----------------------- | -------------------------- | -------------------------------- |
| `labels.agentName`      | `mtn-apm-agent`            | Agent identifier                 |
| `labels.maintainer`     | `mtn-sre-team`             | Maintaining team                 |
| `labels.coreMaintainer` | `muutassim.mukhtar@mtn.com`| Core maintainer contact          |
| `service.name`          | e.g. `myMTNNG-Android`     | Service name from config         |
| `service.environment`   | e.g. `production`          | Environment from config          |
| `service.version`       | App version                | From `DeviceInfo.getVersion()`   |
| `labels.device_model`   | e.g. `sdk_gphone64_arm64`  | Device model                     |
| `labels.device_os`      | e.g. `Android 16`          | OS name and version              |
| `labels.app_bundle`     | e.g. `ng.mtn.nextgen`      | App bundle ID                    |
| `labels.app_build`      | Build number               | App build number                 |
| `labels.app_isEmulator` | `true` / `false`           | Whether running on emulator      |

---

## Elastic APM Notes

### Protobuf Encoding

The SDK uses **protobuf (binary) OTLP encoding** and `XMLHttpRequest` + `Blob` for reliable binary transport in React Native. This is required for Elastic APM servers that only accept `application/x-protobuf` payloads.

### Authentication

If your Elastic APM server requires authentication:

```typescript
otlp: {
  tracesUrl: 'http://your-apm-server:8200/v1/traces',
  headers: {
    Authorization: 'Basic ' + btoa('elastic:your-password'),
  },
},
```

### Android Cleartext HTTP

If your APM server uses `http://` (not `https://`), add the server's IP to your Android network security config:

```xml
<!-- android/app/src/main/res/xml/network_security_config.xml -->
<network-security-config>
  <domain-config cleartextTrafficPermitted="true">
    <domain includeSubdomains="true">10.1.153.199</domain>
  </domain-config>
</network-security-config>
```

---

## What Shows Up in Elastic APM

| Data Type          | Source                      | Kibana Index       | Example                                       |
| ------------------ | --------------------------- | ------------------ | --------------------------------------------- |
| **Transactions**   | Auto fetch instrumentation  | `traces-apm*`      | `HTTP GET https://api.example.com/v1/data`     |
| **Transactions**   | Auto navigation             | `traces-apm*`      | `Screen: HomeScreen`                           |
| **Spans**          | `trackEvent`                | `traces-apm*`      | `purchase.completed`, `bundle.selected`        |
| **Spans**          | `trackOperation`            | `traces-apm*`      | `auth.login_with_otp` (with duration)          |
| **Errors**         | `recordError`               | `logs-apm.error*`  | Caught exceptions with stack trace             |
| **Errors**         | `installCrashReporting`     | `logs-apm.error*`  | Unhandled JS errors and promise rejections      |
| **Errors**         | Auto fetch instrumentation  | `logs-apm.error*`  | Network request failures                       |
| **Metrics**        | AppState instrumentation    | Metrics index       | App foreground/background time                 |
| **Logs**           | Console instrumentation     | `logs-apm.app*`    | All `console.log/warn/error` output            |

### Kibana KQL Queries

| What                       | Query                                                                                   |
| -------------------------- | --------------------------------------------------------------------------------------- |
| All business events        | `service.framework.name: "mtn-sdk:events"`                                              |
| All SDK errors             | `service.framework.name: "mtn-sdk:errors"`                                              |
| Login attempts             | `transaction.name: "auth.login_with_otp"`                                               |
| Bundle selections          | `transaction.name: "bundle.selected"`                                                   |
| Purchase completed         | `transaction.name: "purchase.completed"`                                                |
| Purchase failed            | `transaction.name: "purchase.failed"`                                                   |
| Crash reports              | `labels.error_source: "globalHandler" OR labels.error_source: "unhandledRejection"`     |
| All SDK data               | `labels.agentName: "mtn-apm-agent"`                                                    |
| Filter out dev noise       | `NOT url.domain: "10.0.2.2" AND NOT url.domain: "play.google.com"`                     |

---

## Shutdown

`MTNOTel.init()` returns a singleton instance with a `flushAndShutdown()` method that flushes any pending telemetry and cleans up resources.

```typescript
const telemetry = await MTNOTel.init({ serviceName: 'my-app' });

// When the app is closing or during tests:
await telemetry.flushAndShutdown();
```

---

## Publishing

The SDK is published to npm as `@mukhy/mtn-telemetry-sdk`.

```bash
# Bump version in package.json, then:
npm run prepare   # cleans and builds
npm publish
```

---

## Contributing

1. Fork the repo
2. Create a feature branch: `git checkout -b feature/my-feature`
3. Commit your changes
4. Push and open a PR