mirror of
https://github.com/zoriya/drizzle-otel.git
synced 2025-12-06 00:46:09 +00:00
autumn telemetry sdk
This commit is contained in:
5
packages/otel-autumn/.npmignore
Normal file
5
packages/otel-autumn/.npmignore
Normal file
@@ -0,0 +1,5 @@
|
||||
src/
|
||||
tsconfig.json
|
||||
*.test.ts
|
||||
.gitignore
|
||||
node_modules/
|
||||
13
packages/otel-autumn/CHANGELOG.md
Normal file
13
packages/otel-autumn/CHANGELOG.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# @kubiks/otel-autumn
|
||||
|
||||
## 1.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- Initial release of OpenTelemetry instrumentation for Autumn billing SDK
|
||||
- Instrument core billing operations: check, track, checkout, attach, cancel
|
||||
- Comprehensive span attributes for billing observability
|
||||
- Support for feature access control monitoring
|
||||
- Usage tracking instrumentation
|
||||
- Checkout flow tracing with Stripe integration details
|
||||
- Product lifecycle management (attach/cancel) tracing
|
||||
21
packages/otel-autumn/LICENSE
Normal file
21
packages/otel-autumn/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Kubiks
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
220
packages/otel-autumn/README.md
Normal file
220
packages/otel-autumn/README.md
Normal file
@@ -0,0 +1,220 @@
|
||||
# @kubiks/otel-autumn
|
||||
|
||||
OpenTelemetry instrumentation for the [Autumn](https://useautumn.com) billing SDK.
|
||||
Capture spans for every billing operation including feature checks, usage tracking, checkout flows, product attachments, and cancellations with detailed metadata.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @kubiks/otel-autumn
|
||||
# or
|
||||
pnpm add @kubiks/otel-autumn
|
||||
```
|
||||
|
||||
**Peer Dependencies:** `@opentelemetry/api` >= 1.9.0, `autumn-js` >= 0.1.0
|
||||
|
||||
## Quick Start
|
||||
|
||||
```ts
|
||||
import { Autumn } from "autumn-js";
|
||||
import { instrumentAutumn } from "@kubiks/otel-autumn";
|
||||
|
||||
const autumn = instrumentAutumn(
|
||||
new Autumn({
|
||||
secretKey: process.env.AUTUMN_SECRET_KEY!,
|
||||
})
|
||||
);
|
||||
|
||||
// All operations are now automatically traced
|
||||
const checkResult = await autumn.check({
|
||||
customer_id: "user_123",
|
||||
feature_id: "messages",
|
||||
});
|
||||
|
||||
await autumn.track({
|
||||
customer_id: "user_123",
|
||||
feature_id: "messages",
|
||||
value: 1,
|
||||
});
|
||||
```
|
||||
|
||||
`instrumentAutumn` wraps your Autumn client instance — no configuration changes needed. Every SDK call creates a client span with detailed billing attributes.
|
||||
|
||||
## What Gets Traced
|
||||
|
||||
This instrumentation wraps the core Autumn billing methods:
|
||||
|
||||
- **`check`** - Feature access and product status checks
|
||||
- **`track`** - Usage event tracking
|
||||
- **`checkout`** - Checkout session creation
|
||||
- **`attach`** - Product attachment to customers
|
||||
- **`cancel`** - Product cancellation
|
||||
|
||||
Each operation creates a dedicated span with operation-specific attributes.
|
||||
|
||||
## Span Attributes
|
||||
|
||||
### Common Attributes (All Operations)
|
||||
|
||||
| Attribute | Description | Example |
|
||||
| ----------------------- | ----------------------------------- | ------------------ |
|
||||
| `billing.system` | Constant value `autumn` | `autumn` |
|
||||
| `billing.operation` | Operation type | `check`, `track` |
|
||||
| `autumn.resource` | Resource being accessed | `features`, `products` |
|
||||
| `autumn.target` | Full operation target | `features.check` |
|
||||
| `autumn.customer_id` | Customer ID | `user_123` |
|
||||
| `autumn.entity_id` | Entity ID (if applicable) | `org_456` |
|
||||
|
||||
### Check Operation
|
||||
|
||||
| Attribute | Description | Example |
|
||||
| ------------------------- | --------------------------------- | ------------ |
|
||||
| `autumn.feature_id` | Feature being checked | `messages` |
|
||||
| `autumn.product_id` | Product being checked | `pro` |
|
||||
| `autumn.allowed` | Whether access is allowed | `true` |
|
||||
| `autumn.balance` | Current balance/remaining uses | `42` |
|
||||
| `autumn.usage` | Current usage | `8` |
|
||||
| `autumn.included_usage` | Included usage in plan | `50` |
|
||||
| `autumn.unlimited` | Whether usage is unlimited | `false` |
|
||||
| `autumn.required_balance` | Required balance for operation | `1` |
|
||||
|
||||
### Track Operation
|
||||
|
||||
| Attribute | Description | Example |
|
||||
| -------------------------- | ------------------------------ | ----------------- |
|
||||
| `autumn.feature_id` | Feature being tracked | `messages` |
|
||||
| `autumn.event_name` | Custom event name | `message_sent` |
|
||||
| `autumn.value` | Usage value tracked | `1` |
|
||||
| `autumn.event_id` | Generated event ID | `evt_123` |
|
||||
| `autumn.idempotency_key` | Idempotency key for dedup | `msg_456` |
|
||||
|
||||
### Checkout Operation
|
||||
|
||||
| Attribute | Description | Example |
|
||||
| --------------------------- | ---------------------------------- | ------------------------- |
|
||||
| `autumn.product_id` | Product being purchased | `pro` |
|
||||
| `autumn.product_ids` | Multiple products (comma-separated)| `pro, addon_analytics` |
|
||||
| `autumn.checkout_url` | Stripe checkout URL | `https://checkout.stripe.com/...` |
|
||||
| `autumn.has_prorations` | Whether prorations apply | `true` |
|
||||
| `autumn.total_amount` | Total checkout amount | `2000` (cents) |
|
||||
| `autumn.currency` | Currency code | `usd` |
|
||||
| `autumn.force_checkout` | Whether to force Stripe checkout | `false` |
|
||||
| `autumn.invoice` | Whether to create invoice | `true` |
|
||||
|
||||
### Attach Operation
|
||||
|
||||
| Attribute | Description | Example |
|
||||
| ------------------------ | ------------------------------- | -------------------------------- |
|
||||
| `autumn.product_id` | Product being attached | `pro` |
|
||||
| `autumn.success` | Whether attachment succeeded | `true` |
|
||||
| `autumn.checkout_url` | Checkout URL if payment needed | `https://checkout.stripe.com/...`|
|
||||
|
||||
### Cancel Operation
|
||||
|
||||
| Attribute | Description | Example |
|
||||
| ---------------------- | -------------------------------- | ---------- |
|
||||
| `autumn.product_id` | Product being cancelled | `pro` |
|
||||
| `autumn.success` | Whether cancellation succeeded | `true` |
|
||||
|
||||
## Configuration
|
||||
|
||||
You can optionally configure the instrumentation:
|
||||
|
||||
```ts
|
||||
import { instrumentAutumn } from "@kubiks/otel-autumn";
|
||||
|
||||
const autumn = instrumentAutumn(client, {
|
||||
// Capture customer data in spans (default: false)
|
||||
captureCustomerData: true,
|
||||
|
||||
// Capture product options/configuration (default: false)
|
||||
captureOptions: true,
|
||||
});
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Feature Access Control
|
||||
|
||||
```ts
|
||||
const autumn = instrumentAutumn(new Autumn({ secretKey: process.env.AUTUMN_SECRET_KEY! }));
|
||||
|
||||
// Check if user can access a feature
|
||||
const result = await autumn.check({
|
||||
customer_id: "user_123",
|
||||
feature_id: "messages",
|
||||
required_balance: 1,
|
||||
});
|
||||
|
||||
if (result.data?.allowed) {
|
||||
// User has access
|
||||
console.log(`Remaining: ${result.data.balance}`);
|
||||
}
|
||||
```
|
||||
|
||||
### Usage Tracking
|
||||
|
||||
```ts
|
||||
// Track feature usage
|
||||
await autumn.track({
|
||||
customer_id: "user_123",
|
||||
feature_id: "messages",
|
||||
value: 1,
|
||||
idempotency_key: `msg_${messageId}`, // Prevent double-counting
|
||||
});
|
||||
```
|
||||
|
||||
### Checkout Flow
|
||||
|
||||
```ts
|
||||
// Create a checkout session for a product
|
||||
const result = await autumn.checkout({
|
||||
customer_id: "user_123",
|
||||
product_id: "pro",
|
||||
force_checkout: false, // Use billing portal if payment method exists
|
||||
});
|
||||
|
||||
if (result.data?.url) {
|
||||
// Redirect to Stripe checkout
|
||||
console.log(`Checkout URL: ${result.data.url}`);
|
||||
}
|
||||
```
|
||||
|
||||
### Product Management
|
||||
|
||||
```ts
|
||||
// Attach a free product
|
||||
const attachResult = await autumn.attach({
|
||||
customer_id: "user_123",
|
||||
product_id: "free",
|
||||
});
|
||||
|
||||
// Cancel a subscription
|
||||
const cancelResult = await autumn.cancel({
|
||||
customer_id: "user_123",
|
||||
product_id: "pro",
|
||||
});
|
||||
```
|
||||
|
||||
## Observability Benefits
|
||||
|
||||
This instrumentation helps you:
|
||||
|
||||
- **Monitor billing operations** - Track success rates, latencies, and errors for all billing calls
|
||||
- **Debug checkout issues** - See complete checkout flows including prorations and payment failures
|
||||
- **Analyze feature usage** - Understand which features are being checked and tracked most
|
||||
- **Track customer journey** - Follow customers through check → track → checkout flows
|
||||
- **Identify bottlenecks** - Find slow billing operations impacting user experience
|
||||
- **Audit billing events** - Complete trace of all billing-related operations
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always track server-side** - Use `check` and `track` on the backend for security
|
||||
2. **Use idempotency keys** - Prevent duplicate tracking with `idempotency_key`
|
||||
3. **Monitor check failures** - Alert on high rates of `allowed: false` checks
|
||||
4. **Track checkout abandonment** - Monitor spans where checkout URLs are generated but not completed
|
||||
5. **Correlate with business metrics** - Link billing spans to revenue and conversion metrics
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
53
packages/otel-autumn/package.json
Normal file
53
packages/otel-autumn/package.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "@kubiks/otel-autumn",
|
||||
"version": "1.0.0",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"description": "OpenTelemetry instrumentation for the Autumn billing SDK",
|
||||
"author": "Kubiks",
|
||||
"license": "MIT",
|
||||
"repository": "kubiks-inc/otel",
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"LICENSE",
|
||||
"README.md"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "pnpm clean && tsc",
|
||||
"clean": "rimraf dist",
|
||||
"prepublishOnly": "pnpm build",
|
||||
"type-check": "tsc --noEmit",
|
||||
"unit-test": "vitest --run",
|
||||
"unit-test-watch": "vitest"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/sdk-trace-base": "^2.1.0",
|
||||
"@types/node": "18.15.11",
|
||||
"autumn-js": "^0.1.40",
|
||||
"rimraf": "3.0.2",
|
||||
"typescript": "^5",
|
||||
"vitest": "0.33.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.9.0 <2.0.0",
|
||||
"autumn-js": ">=0.1.0"
|
||||
}
|
||||
}
|
||||
204
packages/otel-autumn/src/index.test.ts
Normal file
204
packages/otel-autumn/src/index.test.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { trace, type Tracer, type Span } from "@opentelemetry/api";
|
||||
import { instrumentAutumn } from "./index";
|
||||
import type { Autumn } from "autumn-js";
|
||||
|
||||
describe("instrumentAutumn", () => {
|
||||
let mockClient: Autumn;
|
||||
let mockTracer: Tracer;
|
||||
let mockSpan: Span;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock span
|
||||
mockSpan = {
|
||||
setAttributes: vi.fn(),
|
||||
setAttribute: vi.fn(),
|
||||
setStatus: vi.fn(),
|
||||
end: vi.fn(),
|
||||
recordException: vi.fn(),
|
||||
} as any;
|
||||
|
||||
// Mock tracer
|
||||
mockTracer = {
|
||||
startSpan: vi.fn().mockReturnValue(mockSpan),
|
||||
} as any;
|
||||
|
||||
// Mock trace.getTracer
|
||||
vi.spyOn(trace, "getTracer").mockReturnValue(mockTracer);
|
||||
|
||||
// Mock Autumn client
|
||||
mockClient = {
|
||||
check: vi.fn().mockResolvedValue({ data: { allowed: true, balance: 5 } }),
|
||||
track: vi.fn().mockResolvedValue({ data: { id: "evt_123" } }),
|
||||
checkout: vi.fn().mockResolvedValue({ data: { url: "https://checkout.stripe.com" } }),
|
||||
attach: vi.fn().mockResolvedValue({ data: { success: true } }),
|
||||
cancel: vi.fn().mockResolvedValue({ data: { success: true } }),
|
||||
} as any;
|
||||
});
|
||||
|
||||
it("should not instrument the same client twice", () => {
|
||||
const instrumented1 = instrumentAutumn(mockClient);
|
||||
const instrumented2 = instrumentAutumn(instrumented1);
|
||||
|
||||
expect(instrumented1).toBe(instrumented2);
|
||||
});
|
||||
|
||||
describe("check method", () => {
|
||||
it("should create a span for check operations", async () => {
|
||||
const instrumented = instrumentAutumn(mockClient);
|
||||
|
||||
await instrumented.check({
|
||||
customer_id: "user_123",
|
||||
feature_id: "messages",
|
||||
});
|
||||
|
||||
expect(mockTracer.startSpan).toHaveBeenCalledWith(
|
||||
"autumn.check",
|
||||
expect.objectContaining({ kind: expect.any(Number) })
|
||||
);
|
||||
});
|
||||
|
||||
it("should set span attributes for check", async () => {
|
||||
const instrumented = instrumentAutumn(mockClient);
|
||||
|
||||
await instrumented.check({
|
||||
customer_id: "user_123",
|
||||
feature_id: "messages",
|
||||
required_balance: 1,
|
||||
});
|
||||
|
||||
expect(mockSpan.setAttribute).toHaveBeenCalledWith("autumn.customer_id", "user_123");
|
||||
expect(mockSpan.setAttribute).toHaveBeenCalledWith("autumn.feature_id", "messages");
|
||||
expect(mockSpan.setAttribute).toHaveBeenCalledWith("autumn.required_balance", 1);
|
||||
});
|
||||
|
||||
it("should handle check errors", async () => {
|
||||
mockClient.check = vi.fn().mockRejectedValue(new Error("API Error"));
|
||||
const instrumented = instrumentAutumn(mockClient);
|
||||
|
||||
await expect(
|
||||
instrumented.check({ customer_id: "user_123", feature_id: "messages" })
|
||||
).rejects.toThrow("API Error");
|
||||
|
||||
expect(mockSpan.recordException).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("track method", () => {
|
||||
it("should create a span for track operations", async () => {
|
||||
const instrumented = instrumentAutumn(mockClient);
|
||||
|
||||
await instrumented.track({
|
||||
customer_id: "user_123",
|
||||
feature_id: "messages",
|
||||
value: 1,
|
||||
});
|
||||
|
||||
expect(mockTracer.startSpan).toHaveBeenCalledWith(
|
||||
"autumn.track",
|
||||
expect.objectContaining({ kind: expect.any(Number) })
|
||||
);
|
||||
});
|
||||
|
||||
it("should set span attributes for track", async () => {
|
||||
const instrumented = instrumentAutumn(mockClient);
|
||||
|
||||
await instrumented.track({
|
||||
customer_id: "user_123",
|
||||
feature_id: "messages",
|
||||
value: 1,
|
||||
idempotency_key: "msg_456",
|
||||
});
|
||||
|
||||
expect(mockSpan.setAttribute).toHaveBeenCalledWith("autumn.customer_id", "user_123");
|
||||
expect(mockSpan.setAttribute).toHaveBeenCalledWith("autumn.feature_id", "messages");
|
||||
expect(mockSpan.setAttribute).toHaveBeenCalledWith("autumn.value", 1);
|
||||
expect(mockSpan.setAttribute).toHaveBeenCalledWith("autumn.idempotency_key", "msg_456");
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkout method", () => {
|
||||
it("should create a span for checkout operations", async () => {
|
||||
const instrumented = instrumentAutumn(mockClient);
|
||||
|
||||
await instrumented.checkout({
|
||||
customer_id: "user_123",
|
||||
product_id: "pro",
|
||||
});
|
||||
|
||||
expect(mockTracer.startSpan).toHaveBeenCalledWith(
|
||||
"autumn.checkout",
|
||||
expect.objectContaining({ kind: expect.any(Number) })
|
||||
);
|
||||
});
|
||||
|
||||
it("should set span attributes for checkout", async () => {
|
||||
const instrumented = instrumentAutumn(mockClient);
|
||||
|
||||
await instrumented.checkout({
|
||||
customer_id: "user_123",
|
||||
product_id: "pro",
|
||||
force_checkout: true,
|
||||
});
|
||||
|
||||
expect(mockSpan.setAttribute).toHaveBeenCalledWith("autumn.customer_id", "user_123");
|
||||
expect(mockSpan.setAttribute).toHaveBeenCalledWith("autumn.product_id", "pro");
|
||||
expect(mockSpan.setAttribute).toHaveBeenCalledWith("autumn.force_checkout", true);
|
||||
});
|
||||
|
||||
it("should handle multiple product IDs", async () => {
|
||||
const instrumented = instrumentAutumn(mockClient);
|
||||
|
||||
await instrumented.checkout({
|
||||
customer_id: "user_123",
|
||||
product_ids: ["pro", "addon_analytics"],
|
||||
});
|
||||
|
||||
expect(mockSpan.setAttribute).toHaveBeenCalledWith("autumn.product_ids", "pro, addon_analytics");
|
||||
});
|
||||
});
|
||||
|
||||
describe("attach method", () => {
|
||||
it("should create a span for attach operations", async () => {
|
||||
const instrumented = instrumentAutumn(mockClient);
|
||||
|
||||
await instrumented.attach({
|
||||
customer_id: "user_123",
|
||||
product_id: "free",
|
||||
});
|
||||
|
||||
expect(mockTracer.startSpan).toHaveBeenCalledWith(
|
||||
"autumn.attach",
|
||||
expect.objectContaining({ kind: expect.any(Number) })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("cancel method", () => {
|
||||
it("should create a span for cancel operations", async () => {
|
||||
const instrumented = instrumentAutumn(mockClient);
|
||||
|
||||
await instrumented.cancel({
|
||||
customer_id: "user_123",
|
||||
product_id: "pro",
|
||||
});
|
||||
|
||||
expect(mockTracer.startSpan).toHaveBeenCalledWith(
|
||||
"autumn.cancel",
|
||||
expect.objectContaining({ kind: expect.any(Number) })
|
||||
);
|
||||
});
|
||||
|
||||
it("should set span attributes for cancel", async () => {
|
||||
const instrumented = instrumentAutumn(mockClient);
|
||||
|
||||
await instrumented.cancel({
|
||||
customer_id: "user_123",
|
||||
product_id: "pro",
|
||||
});
|
||||
|
||||
expect(mockSpan.setAttribute).toHaveBeenCalledWith("autumn.customer_id", "user_123");
|
||||
expect(mockSpan.setAttribute).toHaveBeenCalledWith("autumn.product_id", "pro");
|
||||
});
|
||||
});
|
||||
});
|
||||
529
packages/otel-autumn/src/index.ts
Normal file
529
packages/otel-autumn/src/index.ts
Normal file
@@ -0,0 +1,529 @@
|
||||
import {
|
||||
context,
|
||||
SpanKind,
|
||||
SpanStatusCode,
|
||||
trace,
|
||||
type Span,
|
||||
} from "@opentelemetry/api";
|
||||
import type { Autumn } from "autumn-js";
|
||||
|
||||
const DEFAULT_TRACER_NAME = "@kubiks/otel-autumn";
|
||||
const INSTRUMENTED_FLAG = Symbol("kubiksOtelAutumnInstrumented");
|
||||
|
||||
// Semantic attribute constants
|
||||
export const SEMATTRS_BILLING_SYSTEM = "billing.system" as const;
|
||||
export const SEMATTRS_BILLING_OPERATION = "billing.operation" as const;
|
||||
export const SEMATTRS_AUTUMN_RESOURCE = "autumn.resource" as const;
|
||||
export const SEMATTRS_AUTUMN_TARGET = "autumn.target" as const;
|
||||
|
||||
// Customer attributes
|
||||
export const SEMATTRS_AUTUMN_CUSTOMER_ID = "autumn.customer_id" as const;
|
||||
export const SEMATTRS_AUTUMN_ENTITY_ID = "autumn.entity_id" as const;
|
||||
|
||||
// Product attributes
|
||||
export const SEMATTRS_AUTUMN_PRODUCT_ID = "autumn.product_id" as const;
|
||||
export const SEMATTRS_AUTUMN_PRODUCT_IDS = "autumn.product_ids" as const;
|
||||
export const SEMATTRS_AUTUMN_PRODUCT_NAME = "autumn.product_name" as const;
|
||||
export const SEMATTRS_AUTUMN_PRODUCT_SCENARIO = "autumn.product_scenario" as const;
|
||||
|
||||
// Feature attributes
|
||||
export const SEMATTRS_AUTUMN_FEATURE_ID = "autumn.feature_id" as const;
|
||||
export const SEMATTRS_AUTUMN_FEATURE_NAME = "autumn.feature_name" as const;
|
||||
export const SEMATTRS_AUTUMN_ALLOWED = "autumn.allowed" as const;
|
||||
export const SEMATTRS_AUTUMN_BALANCE = "autumn.balance" as const;
|
||||
export const SEMATTRS_AUTUMN_USAGE = "autumn.usage" as const;
|
||||
export const SEMATTRS_AUTUMN_INCLUDED_USAGE = "autumn.included_usage" as const;
|
||||
export const SEMATTRS_AUTUMN_UNLIMITED = "autumn.unlimited" as const;
|
||||
export const SEMATTRS_AUTUMN_REQUIRED_BALANCE = "autumn.required_balance" as const;
|
||||
|
||||
// Checkout attributes
|
||||
export const SEMATTRS_AUTUMN_CHECKOUT_URL = "autumn.checkout_url" as const;
|
||||
export const SEMATTRS_AUTUMN_HAS_PRORATIONS = "autumn.has_prorations" as const;
|
||||
export const SEMATTRS_AUTUMN_TOTAL_AMOUNT = "autumn.total_amount" as const;
|
||||
export const SEMATTRS_AUTUMN_CURRENCY = "autumn.currency" as const;
|
||||
export const SEMATTRS_AUTUMN_FORCE_CHECKOUT = "autumn.force_checkout" as const;
|
||||
export const SEMATTRS_AUTUMN_INVOICE = "autumn.invoice" as const;
|
||||
|
||||
// Track attributes
|
||||
export const SEMATTRS_AUTUMN_EVENT_NAME = "autumn.event_name" as const;
|
||||
export const SEMATTRS_AUTUMN_VALUE = "autumn.value" as const;
|
||||
export const SEMATTRS_AUTUMN_EVENT_ID = "autumn.event_id" as const;
|
||||
export const SEMATTRS_AUTUMN_IDEMPOTENCY_KEY = "autumn.idempotency_key" as const;
|
||||
|
||||
// Attach/Cancel attributes
|
||||
export const SEMATTRS_AUTUMN_SUCCESS = "autumn.success" as const;
|
||||
|
||||
export interface InstrumentationConfig {
|
||||
/**
|
||||
* Whether to capture customer data in spans.
|
||||
* @default false
|
||||
*/
|
||||
captureCustomerData?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to capture product options/configuration in spans.
|
||||
* @default false
|
||||
*/
|
||||
captureOptions?: boolean;
|
||||
}
|
||||
|
||||
interface InstrumentedAutumn extends Autumn {
|
||||
[INSTRUMENTED_FLAG]?: true;
|
||||
}
|
||||
|
||||
function finalizeSpan(span: Span, error?: unknown): void {
|
||||
if (error) {
|
||||
if (error instanceof Error) {
|
||||
span.recordException(error);
|
||||
} else {
|
||||
span.recordException(new Error(String(error)));
|
||||
}
|
||||
span.setStatus({ code: SpanStatusCode.ERROR });
|
||||
} else {
|
||||
span.setStatus({ code: SpanStatusCode.OK });
|
||||
}
|
||||
span.end();
|
||||
}
|
||||
|
||||
function annotateCheckSpan(
|
||||
span: Span,
|
||||
params: {
|
||||
customer_id: string;
|
||||
feature_id?: string;
|
||||
product_id?: string;
|
||||
entity_id?: string;
|
||||
required_balance?: number;
|
||||
},
|
||||
): void {
|
||||
span.setAttributes({
|
||||
[SEMATTRS_BILLING_SYSTEM]: "autumn",
|
||||
[SEMATTRS_BILLING_OPERATION]: "check",
|
||||
[SEMATTRS_AUTUMN_RESOURCE]: params.feature_id ? "features" : "products",
|
||||
[SEMATTRS_AUTUMN_TARGET]: params.feature_id
|
||||
? "features.check"
|
||||
: "products.check",
|
||||
});
|
||||
|
||||
span.setAttribute(SEMATTRS_AUTUMN_CUSTOMER_ID, params.customer_id);
|
||||
|
||||
if (params.feature_id) {
|
||||
span.setAttribute(SEMATTRS_AUTUMN_FEATURE_ID, params.feature_id);
|
||||
}
|
||||
|
||||
if (params.product_id) {
|
||||
span.setAttribute(SEMATTRS_AUTUMN_PRODUCT_ID, params.product_id);
|
||||
}
|
||||
|
||||
if (params.entity_id) {
|
||||
span.setAttribute(SEMATTRS_AUTUMN_ENTITY_ID, params.entity_id);
|
||||
}
|
||||
|
||||
if (typeof params.required_balance === "number") {
|
||||
span.setAttribute(SEMATTRS_AUTUMN_REQUIRED_BALANCE, params.required_balance);
|
||||
}
|
||||
}
|
||||
|
||||
function annotateCheckResponse(span: Span, response: Record<string, unknown>): void {
|
||||
if (typeof response.allowed === "boolean") {
|
||||
span.setAttribute(SEMATTRS_AUTUMN_ALLOWED, response.allowed);
|
||||
}
|
||||
|
||||
if (typeof response.balance === "number") {
|
||||
span.setAttribute(SEMATTRS_AUTUMN_BALANCE, response.balance);
|
||||
}
|
||||
|
||||
if (typeof response.usage === "number") {
|
||||
span.setAttribute(SEMATTRS_AUTUMN_USAGE, response.usage);
|
||||
}
|
||||
|
||||
if (typeof response.included_usage === "number") {
|
||||
span.setAttribute(SEMATTRS_AUTUMN_INCLUDED_USAGE, response.included_usage);
|
||||
}
|
||||
|
||||
if (typeof response.unlimited === "boolean") {
|
||||
span.setAttribute(SEMATTRS_AUTUMN_UNLIMITED, response.unlimited);
|
||||
}
|
||||
|
||||
if (typeof response.status === "string") {
|
||||
span.setAttribute(SEMATTRS_AUTUMN_PRODUCT_SCENARIO, response.status);
|
||||
}
|
||||
}
|
||||
|
||||
function annotateTrackSpan(
|
||||
span: Span,
|
||||
params: {
|
||||
customer_id: string;
|
||||
feature_id?: string;
|
||||
event_name?: string;
|
||||
value?: number;
|
||||
entity_id?: string;
|
||||
idempotency_key?: string;
|
||||
},
|
||||
): void {
|
||||
span.setAttributes({
|
||||
[SEMATTRS_BILLING_SYSTEM]: "autumn",
|
||||
[SEMATTRS_BILLING_OPERATION]: "track",
|
||||
[SEMATTRS_AUTUMN_RESOURCE]: "events",
|
||||
[SEMATTRS_AUTUMN_TARGET]: "events.track",
|
||||
});
|
||||
|
||||
span.setAttribute(SEMATTRS_AUTUMN_CUSTOMER_ID, params.customer_id);
|
||||
|
||||
if (params.feature_id) {
|
||||
span.setAttribute(SEMATTRS_AUTUMN_FEATURE_ID, params.feature_id);
|
||||
}
|
||||
|
||||
if (params.event_name) {
|
||||
span.setAttribute(SEMATTRS_AUTUMN_EVENT_NAME, params.event_name);
|
||||
}
|
||||
|
||||
if (typeof params.value === "number") {
|
||||
span.setAttribute(SEMATTRS_AUTUMN_VALUE, params.value);
|
||||
}
|
||||
|
||||
if (params.entity_id) {
|
||||
span.setAttribute(SEMATTRS_AUTUMN_ENTITY_ID, params.entity_id);
|
||||
}
|
||||
|
||||
if (params.idempotency_key) {
|
||||
span.setAttribute(SEMATTRS_AUTUMN_IDEMPOTENCY_KEY, params.idempotency_key);
|
||||
}
|
||||
}
|
||||
|
||||
function annotateTrackResponse(span: Span, response: { id?: string }): void {
|
||||
if (response.id) {
|
||||
span.setAttribute(SEMATTRS_AUTUMN_EVENT_ID, response.id);
|
||||
}
|
||||
}
|
||||
|
||||
function annotateCheckoutSpan(
|
||||
span: Span,
|
||||
params: {
|
||||
customer_id: string;
|
||||
product_id?: string;
|
||||
product_ids?: string[];
|
||||
entity_id?: string;
|
||||
force_checkout?: boolean;
|
||||
invoice?: boolean;
|
||||
},
|
||||
config?: InstrumentationConfig,
|
||||
): void {
|
||||
span.setAttributes({
|
||||
[SEMATTRS_BILLING_SYSTEM]: "autumn",
|
||||
[SEMATTRS_BILLING_OPERATION]: "checkout",
|
||||
[SEMATTRS_AUTUMN_RESOURCE]: "checkout",
|
||||
[SEMATTRS_AUTUMN_TARGET]: "checkout.create",
|
||||
});
|
||||
|
||||
span.setAttribute(SEMATTRS_AUTUMN_CUSTOMER_ID, params.customer_id);
|
||||
|
||||
if (params.product_id) {
|
||||
span.setAttribute(SEMATTRS_AUTUMN_PRODUCT_ID, params.product_id);
|
||||
}
|
||||
|
||||
if (params.product_ids && params.product_ids.length > 0) {
|
||||
span.setAttribute(SEMATTRS_AUTUMN_PRODUCT_IDS, params.product_ids.join(", "));
|
||||
}
|
||||
|
||||
if (params.entity_id) {
|
||||
span.setAttribute(SEMATTRS_AUTUMN_ENTITY_ID, params.entity_id);
|
||||
}
|
||||
|
||||
if (typeof params.force_checkout === "boolean") {
|
||||
span.setAttribute(SEMATTRS_AUTUMN_FORCE_CHECKOUT, params.force_checkout);
|
||||
}
|
||||
|
||||
if (typeof params.invoice === "boolean") {
|
||||
span.setAttribute(SEMATTRS_AUTUMN_INVOICE, params.invoice);
|
||||
}
|
||||
}
|
||||
|
||||
function annotateCheckoutResponse(
|
||||
span: Span,
|
||||
response: {
|
||||
url?: string;
|
||||
has_prorations?: boolean;
|
||||
total?: number;
|
||||
currency?: string;
|
||||
},
|
||||
): void {
|
||||
if (response.url) {
|
||||
span.setAttribute(SEMATTRS_AUTUMN_CHECKOUT_URL, response.url);
|
||||
}
|
||||
|
||||
if (typeof response.has_prorations === "boolean") {
|
||||
span.setAttribute(SEMATTRS_AUTUMN_HAS_PRORATIONS, response.has_prorations);
|
||||
}
|
||||
|
||||
if (typeof response.total === "number") {
|
||||
span.setAttribute(SEMATTRS_AUTUMN_TOTAL_AMOUNT, response.total);
|
||||
}
|
||||
|
||||
if (response.currency) {
|
||||
span.setAttribute(SEMATTRS_AUTUMN_CURRENCY, response.currency);
|
||||
}
|
||||
}
|
||||
|
||||
function annotateAttachSpan(
|
||||
span: Span,
|
||||
params: {
|
||||
customer_id: string;
|
||||
product_id: string;
|
||||
entity_id?: string;
|
||||
},
|
||||
): void {
|
||||
span.setAttributes({
|
||||
[SEMATTRS_BILLING_SYSTEM]: "autumn",
|
||||
[SEMATTRS_BILLING_OPERATION]: "attach",
|
||||
[SEMATTRS_AUTUMN_RESOURCE]: "products",
|
||||
[SEMATTRS_AUTUMN_TARGET]: "products.attach",
|
||||
});
|
||||
|
||||
span.setAttribute(SEMATTRS_AUTUMN_CUSTOMER_ID, params.customer_id);
|
||||
span.setAttribute(SEMATTRS_AUTUMN_PRODUCT_ID, params.product_id);
|
||||
|
||||
if (params.entity_id) {
|
||||
span.setAttribute(SEMATTRS_AUTUMN_ENTITY_ID, params.entity_id);
|
||||
}
|
||||
}
|
||||
|
||||
function annotateAttachResponse(
|
||||
span: Span,
|
||||
response: {
|
||||
success?: boolean;
|
||||
checkout_url?: string;
|
||||
},
|
||||
): void {
|
||||
if (typeof response.success === "boolean") {
|
||||
span.setAttribute(SEMATTRS_AUTUMN_SUCCESS, response.success);
|
||||
}
|
||||
|
||||
if (response.checkout_url) {
|
||||
span.setAttribute(SEMATTRS_AUTUMN_CHECKOUT_URL, response.checkout_url);
|
||||
}
|
||||
}
|
||||
|
||||
function annotateCancelSpan(
|
||||
span: Span,
|
||||
params: {
|
||||
customer_id: string;
|
||||
product_id: string;
|
||||
},
|
||||
): void {
|
||||
span.setAttributes({
|
||||
[SEMATTRS_BILLING_SYSTEM]: "autumn",
|
||||
[SEMATTRS_BILLING_OPERATION]: "cancel",
|
||||
[SEMATTRS_AUTUMN_RESOURCE]: "products",
|
||||
[SEMATTRS_AUTUMN_TARGET]: "products.cancel",
|
||||
});
|
||||
|
||||
span.setAttribute(SEMATTRS_AUTUMN_CUSTOMER_ID, params.customer_id);
|
||||
span.setAttribute(SEMATTRS_AUTUMN_PRODUCT_ID, params.product_id);
|
||||
}
|
||||
|
||||
function annotateCancelResponse(span: Span, response: { success?: boolean }): void {
|
||||
if (typeof response.success === "boolean") {
|
||||
span.setAttribute(SEMATTRS_AUTUMN_SUCCESS, response.success);
|
||||
}
|
||||
}
|
||||
|
||||
export function instrumentAutumn(
|
||||
client: Autumn,
|
||||
config?: InstrumentationConfig,
|
||||
): Autumn {
|
||||
// Check if already instrumented
|
||||
if ((client as InstrumentedAutumn)[INSTRUMENTED_FLAG]) {
|
||||
return client;
|
||||
}
|
||||
|
||||
const tracer = trace.getTracer(DEFAULT_TRACER_NAME);
|
||||
|
||||
// Instrument check method
|
||||
const originalCheck = client.check.bind(client);
|
||||
const instrumentedCheck = async function instrumentedCheck(
|
||||
params: Parameters<typeof originalCheck>[0],
|
||||
): Promise<ReturnType<typeof originalCheck>> {
|
||||
const span = tracer.startSpan("autumn.check", {
|
||||
kind: SpanKind.CLIENT,
|
||||
});
|
||||
|
||||
annotateCheckSpan(span, params as {
|
||||
customer_id: string;
|
||||
feature_id?: string;
|
||||
product_id?: string;
|
||||
entity_id?: string;
|
||||
required_balance?: number;
|
||||
});
|
||||
|
||||
const activeContext = trace.setSpan(context.active(), span);
|
||||
|
||||
try {
|
||||
const result = await context.with(activeContext, () =>
|
||||
originalCheck(params),
|
||||
);
|
||||
|
||||
if (result.data) {
|
||||
annotateCheckResponse(span, result.data as Record<string, unknown>);
|
||||
}
|
||||
|
||||
finalizeSpan(span);
|
||||
return result;
|
||||
} catch (error) {
|
||||
finalizeSpan(span, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Instrument track method
|
||||
const originalTrack = client.track.bind(client);
|
||||
const instrumentedTrack = async function instrumentedTrack(
|
||||
params: Parameters<typeof originalTrack>[0],
|
||||
): Promise<ReturnType<typeof originalTrack>> {
|
||||
const span = tracer.startSpan("autumn.track", {
|
||||
kind: SpanKind.CLIENT,
|
||||
});
|
||||
|
||||
annotateTrackSpan(span, params as {
|
||||
customer_id: string;
|
||||
feature_id?: string;
|
||||
event_name?: string;
|
||||
value?: number;
|
||||
entity_id?: string;
|
||||
idempotency_key?: string;
|
||||
});
|
||||
|
||||
const activeContext = trace.setSpan(context.active(), span);
|
||||
|
||||
try {
|
||||
const result = await context.with(activeContext, () =>
|
||||
originalTrack(params),
|
||||
);
|
||||
|
||||
if (result.data) {
|
||||
annotateTrackResponse(span, result.data);
|
||||
}
|
||||
|
||||
finalizeSpan(span);
|
||||
return result;
|
||||
} catch (error) {
|
||||
finalizeSpan(span, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Instrument checkout method
|
||||
const originalCheckout = client.checkout.bind(client);
|
||||
const instrumentedCheckout = async function instrumentedCheckout(
|
||||
params: Parameters<typeof originalCheckout>[0],
|
||||
): Promise<ReturnType<typeof originalCheckout>> {
|
||||
const span = tracer.startSpan("autumn.checkout", {
|
||||
kind: SpanKind.CLIENT,
|
||||
});
|
||||
|
||||
annotateCheckoutSpan(span, params as {
|
||||
customer_id: string;
|
||||
product_id?: string;
|
||||
product_ids?: string[];
|
||||
entity_id?: string;
|
||||
force_checkout?: boolean;
|
||||
invoice?: boolean;
|
||||
}, config);
|
||||
|
||||
const activeContext = trace.setSpan(context.active(), span);
|
||||
|
||||
try {
|
||||
const result = await context.with(activeContext, () =>
|
||||
originalCheckout(params),
|
||||
);
|
||||
|
||||
if (result.data) {
|
||||
annotateCheckoutResponse(span, result.data);
|
||||
}
|
||||
|
||||
finalizeSpan(span);
|
||||
return result;
|
||||
} catch (error) {
|
||||
finalizeSpan(span, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Instrument attach method
|
||||
const originalAttach = client.attach.bind(client);
|
||||
const instrumentedAttach = async function instrumentedAttach(
|
||||
params: Parameters<typeof originalAttach>[0],
|
||||
): Promise<ReturnType<typeof originalAttach>> {
|
||||
const span = tracer.startSpan("autumn.attach", {
|
||||
kind: SpanKind.CLIENT,
|
||||
});
|
||||
|
||||
annotateAttachSpan(span, params as {
|
||||
customer_id: string;
|
||||
product_id: string;
|
||||
entity_id?: string;
|
||||
});
|
||||
|
||||
const activeContext = trace.setSpan(context.active(), span);
|
||||
|
||||
try {
|
||||
const result = await context.with(activeContext, () =>
|
||||
originalAttach(params),
|
||||
);
|
||||
|
||||
if (result.data) {
|
||||
annotateAttachResponse(span, result.data);
|
||||
}
|
||||
|
||||
finalizeSpan(span);
|
||||
return result;
|
||||
} catch (error) {
|
||||
finalizeSpan(span, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Instrument cancel method
|
||||
const originalCancel = client.cancel.bind(client);
|
||||
const instrumentedCancel = async function instrumentedCancel(
|
||||
params: Parameters<typeof originalCancel>[0],
|
||||
): Promise<ReturnType<typeof originalCancel>> {
|
||||
const span = tracer.startSpan("autumn.cancel", {
|
||||
kind: SpanKind.CLIENT,
|
||||
});
|
||||
|
||||
annotateCancelSpan(span, params as {
|
||||
customer_id: string;
|
||||
product_id: string;
|
||||
});
|
||||
|
||||
const activeContext = trace.setSpan(context.active(), span);
|
||||
|
||||
try {
|
||||
const result = await context.with(activeContext, () =>
|
||||
originalCancel(params),
|
||||
);
|
||||
|
||||
if (result.data) {
|
||||
annotateCancelResponse(span, result.data);
|
||||
}
|
||||
|
||||
finalizeSpan(span);
|
||||
return result;
|
||||
} catch (error) {
|
||||
finalizeSpan(span, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Replace methods with instrumented versions
|
||||
client.check = instrumentedCheck;
|
||||
client.track = instrumentedTrack;
|
||||
client.checkout = instrumentedCheckout;
|
||||
client.attach = instrumentedAttach;
|
||||
client.cancel = instrumentedCancel;
|
||||
|
||||
// Mark as instrumented
|
||||
(client as InstrumentedAutumn)[INSTRUMENTED_FLAG] = true;
|
||||
|
||||
return client;
|
||||
}
|
||||
21
packages/otel-autumn/tsconfig.json
Normal file
21
packages/otel-autumn/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"outDir": "dist",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"strict": false,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declarationDir": "dist/types",
|
||||
"stripInternal": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user