mirror of
https://github.com/zoriya/drizzle-otel.git
synced 2025-12-06 00:46:09 +00:00
404 lines
11 KiB
Markdown
404 lines
11 KiB
Markdown
# @kubiks/otel-polar
|
|
|
|
OpenTelemetry instrumentation for the [Polar.sh](https://polar.sh) Node.js SDK.
|
|
Capture spans for every Polar API call, enrich them with operation metadata,
|
|
and keep an eye on your billing, subscriptions, and customer operations from your traces.
|
|
|
|

|
|
|
|
_Visualize your Polar operations with detailed span information including resource IDs, organization IDs, and operation metadata._
|
|
|
|
## Installation
|
|
|
|
```bash
|
|
npm install @kubiks/otel-polar
|
|
# or
|
|
pnpm add @kubiks/otel-polar
|
|
# or
|
|
yarn add @kubiks/otel-polar
|
|
# or
|
|
bun add @kubiks/otel-polar
|
|
```
|
|
|
|
**Peer Dependencies:** `@opentelemetry/api` >= 1.9.0, `@polar-sh/sdk` >= 0.11.0
|
|
|
|
## Quick Start
|
|
|
|
```ts
|
|
import { Polar } from "@polar-sh/sdk";
|
|
import { instrumentPolar } from "@kubiks/otel-polar";
|
|
|
|
const polar = new Polar({
|
|
accessToken: process.env.POLAR_ACCESS_TOKEN!,
|
|
});
|
|
|
|
// Instrument the client - all operations are now traced!
|
|
instrumentPolar(polar);
|
|
|
|
// Use the SDK normally - traces are automatically created
|
|
await polar.benefits.list({ organizationId: "org_123" });
|
|
await polar.customers.create({
|
|
email: "customer@example.com",
|
|
organizationId: "org_123",
|
|
});
|
|
```
|
|
|
|
`instrumentPolar` wraps the Polar SDK instance you already use — no configuration changes needed. Every SDK call creates a client span with useful attributes.
|
|
|
|
## What Gets Traced
|
|
|
|
This instrumentation automatically wraps **all** Polar SDK methods across all resources, including:
|
|
|
|
### Core Resources
|
|
- **Benefits** - `list`, `create`, `get`, `update`, `delete`
|
|
- **Benefit Grants** - `list`, `create`, `get`, `update`
|
|
- **Checkouts** - `list`, `create`, `get`, `update`
|
|
- **Checkout Links** - `list`, `create`, `get`, `update`, `delete`
|
|
- **Customers** - `list`, `create`, `get`, `update`, `delete`
|
|
- **Customer Meters** - Track usage metrics
|
|
- **Customer Seats** - Manage seat assignments
|
|
- **Customer Sessions** - Session management
|
|
- **Discounts** - `list`, `create`, `get`, `update`, `delete`
|
|
- **Events** - `list`, `ingest`
|
|
- **Files** - `list`, `create`, `upload`
|
|
- **License Keys** - `list`, `get`, `update`, `validate`, `activate`, `deactivate`
|
|
- **Organizations** - `list`, `create`, `get`, `update`
|
|
- **Orders** - `list`, `get`
|
|
- **Products** - `list`, `create`, `get`, `update`, `delete`
|
|
- **Subscriptions** - `list`, `create`, `get`, `update`, `export`
|
|
- **Wallets** - Access wallet information
|
|
- **Custom Fields** - Define custom data fields
|
|
- **Metrics** - Access analytics and metrics
|
|
- **OAuth2** - `authorize`, `token`, `revoke`, `introspect`
|
|
|
|
### Customer Portal Resources
|
|
All customer-facing operations under `polar.customerPortal.*`:
|
|
- `benefitGrants`
|
|
- `customerMeters`
|
|
- `customers`
|
|
- `customerSession`
|
|
- `downloadables`
|
|
- `licenseKeys`
|
|
- `orders`
|
|
- `organizations`
|
|
- `seats`
|
|
- `subscriptions`
|
|
- `wallets`
|
|
|
|
### Webhook Validation
|
|
- `webhooks.validate` - Validate webhook signatures
|
|
|
|
## Configuration
|
|
|
|
The instrumentation can be customized with configuration options:
|
|
|
|
```ts
|
|
import { instrumentPolar } from "@kubiks/otel-polar";
|
|
|
|
instrumentPolar(polar, {
|
|
// Custom tracer name (default: "@kubiks/otel-polar")
|
|
tracerName: "my-custom-tracer",
|
|
|
|
// Capture resource IDs from requests and responses (default: true)
|
|
captureResourceIds: true,
|
|
|
|
// Capture organization IDs from requests (default: true)
|
|
captureOrganizationIds: true,
|
|
|
|
// Instrument customer portal operations (default: true)
|
|
instrumentCustomerPortal: true,
|
|
});
|
|
```
|
|
|
|
## Span Attributes
|
|
|
|
Each span includes comprehensive attributes to help with debugging and monitoring:
|
|
|
|
### Common Attributes
|
|
|
|
| Attribute | Description | Example |
|
|
|-----------|-------------|---------|
|
|
| `polar.operation` | Full operation name | `benefits.list`, `customers.create` |
|
|
| `polar.resource` | Resource type being accessed | `benefits`, `customers`, `checkouts` |
|
|
| `polar.resource_id` | ID of the specific resource (when available) | `benefit_123`, `cust_456` |
|
|
|
|
### Resource-Specific Attributes
|
|
|
|
Depending on the operation, additional attributes are captured:
|
|
|
|
| Attribute | Description | Example |
|
|
|-----------|-------------|---------|
|
|
| `polar.organization_id` | Organization ID from request | `org_123` |
|
|
| `polar.customer_id` | Customer ID | `cust_456` |
|
|
| `polar.product_id` | Product ID | `prod_789` |
|
|
| `polar.subscription_id` | Subscription ID | `sub_abc` |
|
|
| `polar.checkout_id` | Checkout session ID | `checkout_xyz` |
|
|
| `polar.order_id` | Order ID | `order_123` |
|
|
| `polar.benefit_id` | Benefit ID | `benefit_456` |
|
|
| `polar.license_key_id` | License key ID | `lic_789` |
|
|
| `polar.file_id` | File ID | `file_abc` |
|
|
| `polar.event_id` | Event ID | `evt_xyz` |
|
|
| `polar.discount_id` | Discount code ID | `disc_123` |
|
|
|
|
### Webhook Attributes
|
|
|
|
For webhook validation operations:
|
|
|
|
| Attribute | Description | Example |
|
|
|-----------|-------------|---------|
|
|
| `polar.webhook.event_type` | Type of webhook event | `checkout.created`, `subscription.updated` |
|
|
| `polar.webhook.valid` | Whether validation succeeded | `true`, `false` |
|
|
|
|
## Usage Examples
|
|
|
|
### Basic Operations
|
|
|
|
```ts
|
|
import { Polar } from "@polar-sh/sdk";
|
|
import { instrumentPolar } from "@kubiks/otel-polar";
|
|
|
|
const polar = new Polar({
|
|
accessToken: process.env.POLAR_ACCESS_TOKEN!,
|
|
});
|
|
|
|
instrumentPolar(polar);
|
|
|
|
// Create a benefit
|
|
const benefit = await polar.benefits.create({
|
|
organizationId: "org_123",
|
|
type: "custom",
|
|
description: "Premium Support",
|
|
});
|
|
|
|
// List customers
|
|
const customers = await polar.customers.list({
|
|
organizationId: "org_123",
|
|
});
|
|
|
|
// Create a checkout session
|
|
const checkout = await polar.checkouts.create({
|
|
productId: "prod_456",
|
|
successUrl: "https://example.com/success",
|
|
});
|
|
```
|
|
|
|
### Customer Portal Operations
|
|
|
|
```ts
|
|
// Customer portal operations are automatically instrumented
|
|
const subscriptions = await polar.customerPortal.subscriptions.list({
|
|
customerId: "cust_789",
|
|
});
|
|
|
|
const licenseKey = await polar.customerPortal.licenseKeys.validate({
|
|
key: "lic_key_value",
|
|
});
|
|
```
|
|
|
|
### Webhook Handling
|
|
|
|
```ts
|
|
import { instrumentPolar } from "@kubiks/otel-polar";
|
|
|
|
const polar = new Polar({
|
|
accessToken: process.env.POLAR_ACCESS_TOKEN!,
|
|
});
|
|
|
|
instrumentPolar(polar);
|
|
|
|
// Webhook validation is automatically traced
|
|
app.post("/webhooks/polar", async (req, res) => {
|
|
try {
|
|
const event = await polar.webhooks.validate({
|
|
body: req.body,
|
|
signature: req.headers["polar-signature"],
|
|
secret: process.env.POLAR_WEBHOOK_SECRET!,
|
|
});
|
|
|
|
// Handle the event
|
|
console.log("Received event:", event.type);
|
|
|
|
res.json({ received: true });
|
|
} catch (error) {
|
|
res.status(400).json({ error: "Invalid signature" });
|
|
}
|
|
});
|
|
```
|
|
|
|
### Error Handling
|
|
|
|
Errors are automatically captured in spans with full exception details:
|
|
|
|
```ts
|
|
try {
|
|
await polar.customers.get("invalid_id");
|
|
} catch (error) {
|
|
// Span will be marked as failed with exception details
|
|
console.error("Failed to get customer:", error);
|
|
}
|
|
```
|
|
|
|
### Advanced Configuration
|
|
|
|
```ts
|
|
import { instrumentPolar } from "@kubiks/otel-polar";
|
|
import { trace } from "@opentelemetry/api";
|
|
|
|
const polar = new Polar({
|
|
accessToken: process.env.POLAR_ACCESS_TOKEN!,
|
|
});
|
|
|
|
// Use custom configuration for fine-grained control
|
|
instrumentPolar(polar, {
|
|
tracerName: "my-app-polar-tracer",
|
|
captureResourceIds: true,
|
|
captureOrganizationIds: true,
|
|
instrumentCustomerPortal: true,
|
|
});
|
|
|
|
// Create a custom span around multiple operations
|
|
const tracer = trace.getTracer("my-app");
|
|
const span = tracer.startSpan("create-subscription-flow");
|
|
|
|
await trace.setSpan(context.active(), span).with(async () => {
|
|
const customer = await polar.customers.create({
|
|
email: "user@example.com",
|
|
organizationId: "org_123",
|
|
});
|
|
|
|
const subscription = await polar.subscriptions.create({
|
|
customerId: customer.data.id,
|
|
productId: "prod_456",
|
|
});
|
|
|
|
span.end();
|
|
});
|
|
```
|
|
|
|
## Integration with OpenTelemetry
|
|
|
|
This instrumentation integrates seamlessly with your existing OpenTelemetry setup:
|
|
|
|
```ts
|
|
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
import { ConsoleSpanExporter } from "@opentelemetry/sdk-trace-node";
|
|
import { Resource } from "@opentelemetry/resources";
|
|
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
|
|
import { Polar } from "@polar-sh/sdk";
|
|
import { instrumentPolar } from "@kubiks/otel-polar";
|
|
|
|
// Initialize OpenTelemetry
|
|
const sdk = new NodeSDK({
|
|
resource: new Resource({
|
|
[ATTR_SERVICE_NAME]: "my-app",
|
|
}),
|
|
traceExporter: new ConsoleSpanExporter(),
|
|
});
|
|
|
|
sdk.start();
|
|
|
|
// Instrument Polar client
|
|
const polar = new Polar({
|
|
accessToken: process.env.POLAR_ACCESS_TOKEN!,
|
|
});
|
|
|
|
instrumentPolar(polar);
|
|
|
|
// All Polar operations now appear in your traces!
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
1. **Instrument Early**: Call `instrumentPolar()` once when initializing your Polar client
|
|
2. **Reuse Clients**: Instrument a single Polar client instance and reuse it throughout your app
|
|
3. **Context Propagation**: The instrumentation automatically propagates context for distributed tracing
|
|
4. **Error Tracking**: Errors are automatically captured - no need for manual exception recording
|
|
5. **Resource IDs**: Keep `captureResourceIds` enabled to track specific resources in your spans
|
|
|
|
## Framework Integration
|
|
|
|
This instrumentation works with any Node.js framework:
|
|
|
|
### Express
|
|
|
|
```ts
|
|
import express from "express";
|
|
import { Polar } from "@polar-sh/sdk";
|
|
import { instrumentPolar } from "@kubiks/otel-polar";
|
|
|
|
const app = express();
|
|
const polar = instrumentPolar(new Polar({ accessToken: process.env.POLAR_ACCESS_TOKEN! }));
|
|
|
|
app.get("/benefits", async (req, res) => {
|
|
const benefits = await polar.benefits.list({
|
|
organizationId: req.query.orgId,
|
|
});
|
|
res.json(benefits);
|
|
});
|
|
```
|
|
|
|
### Next.js
|
|
|
|
```ts
|
|
// lib/polar.ts
|
|
import { Polar } from "@polar-sh/sdk";
|
|
import { instrumentPolar } from "@kubiks/otel-polar";
|
|
|
|
export const polar = instrumentPolar(
|
|
new Polar({
|
|
accessToken: process.env.POLAR_ACCESS_TOKEN!,
|
|
})
|
|
);
|
|
|
|
// app/api/benefits/route.ts
|
|
import { polar } from "@/lib/polar";
|
|
|
|
export async function GET(request: Request) {
|
|
const benefits = await polar.benefits.list({
|
|
organizationId: "org_123",
|
|
});
|
|
return Response.json(benefits);
|
|
}
|
|
```
|
|
|
|
### Fastify
|
|
|
|
```ts
|
|
import Fastify from "fastify";
|
|
import { Polar } from "@polar-sh/sdk";
|
|
import { instrumentPolar } from "@kubiks/otel-polar";
|
|
|
|
const fastify = Fastify();
|
|
const polar = instrumentPolar(new Polar({ accessToken: process.env.POLAR_ACCESS_TOKEN! }));
|
|
|
|
fastify.get("/customers", async (request, reply) => {
|
|
const customers = await polar.customers.list({});
|
|
return customers;
|
|
});
|
|
```
|
|
|
|
## TypeScript Support
|
|
|
|
This package includes full TypeScript definitions. The instrumentation preserves all type information from the Polar SDK:
|
|
|
|
```ts
|
|
import { Polar } from "@polar-sh/sdk";
|
|
import { instrumentPolar } from "@kubiks/otel-polar";
|
|
|
|
const polar = instrumentPolar(new Polar({ accessToken: "..." }));
|
|
|
|
// Full type safety is preserved
|
|
const benefit = await polar.benefits.create({
|
|
organizationId: "org_123",
|
|
type: "custom", // TypeScript knows the valid types
|
|
description: "Premium Support",
|
|
});
|
|
|
|
// TypeScript error: Property 'invalidMethod' does not exist
|
|
// polar.benefits.invalidMethod();
|
|
```
|
|
|
|
## License
|
|
|
|
MIT
|