11 KiB
@kubiks/otel-polar
OpenTelemetry instrumentation for the 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.
Installation
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
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.*:
benefitGrantscustomerMeterscustomerscustomerSessiondownloadableslicenseKeysordersorganizationsseatssubscriptionswallets
Webhook Validation
webhooks.validate- Validate webhook signatures
Configuration
The instrumentation can be customized with configuration options:
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
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
// 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
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:
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
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:
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
- Instrument Early: Call
instrumentPolar()once when initializing your Polar client - Reuse Clients: Instrument a single Polar client instance and reuse it throughout your app
- Context Propagation: The instrumentation automatically propagates context for distributed tracing
- Error Tracking: Errors are automatically captured - no need for manual exception recording
- Resource IDs: Keep
captureResourceIdsenabled to track specific resources in your spans
Framework Integration
This instrumentation works with any Node.js framework:
Express
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
// 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
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;
});
Troubleshooting
No spans appearing
Ensure you have:
- Initialized OpenTelemetry SDK before instrumenting Polar
- Called
instrumentPolar()after creating the Polar client - Configured a span exporter (Console, OTLP, etc.)
Spans not linking to parent traces
The instrumentation uses context.active() to link spans. Ensure your HTTP framework supports OpenTelemetry context propagation.
Double instrumentation
The instrumentation is idempotent. Calling instrumentPolar() multiple times on the same client is safe and will only instrument once.
TypeScript Support
This package includes full TypeScript definitions. The instrumentation preserves all type information from the Polar SDK:
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