@rely-net/sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,198 @@
1
+ # @rely-net/sdk
2
+
3
+ Official SDK for [rely.net](https://rely.net) — monitor your application from the inside.
4
+
5
+ ## What it does
6
+
7
+ - Sends health check results from inside your app to rely.net
8
+ - Tracks custom metrics alongside vendor status
9
+ - Marks deployments on your monitoring charts
10
+ - Captures request telemetry (error rate, response times)
11
+ - Sends runtime stats (memory usage, uptime)
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @rely-net/sdk
17
+ ```
18
+
19
+ ## Quick start (Next.js)
20
+
21
+ ### 1. Get your API key
22
+
23
+ Go to [rely.net/settings/api-keys](https://rely.net/settings/api-keys) and create a new key.
24
+
25
+ ### 2. Add to your environment
26
+
27
+ ```bash
28
+ # .env.local
29
+ RELY_API_KEY=rely_live_...
30
+ ```
31
+
32
+ ### 3. Create `instrumentation.ts`
33
+
34
+ Create a file called `instrumentation.ts` in your Next.js project root:
35
+
36
+ ```ts
37
+ import { Rely } from '@rely-net/sdk'
38
+
39
+ export const rely = new Rely({
40
+ apiKey: process.env.RELY_API_KEY!,
41
+ })
42
+
43
+ // Add health checks for your services
44
+ rely.healthCheck('database', async () => {
45
+ const { supabase } = await import('@/lib/supabase/client')
46
+ const { error } = await supabase.from('_health').select('1')
47
+ if (error) throw error
48
+ })
49
+ ```
50
+
51
+ That's it. The SDK will start sending data to rely.net within 60 seconds.
52
+
53
+ ## Health check examples
54
+
55
+ Copy-paste snippets for common services:
56
+
57
+ ### Supabase
58
+
59
+ ```ts
60
+ rely.healthCheck('supabase', async () => {
61
+ const { error } = await supabase.from('_health').select('1')
62
+ if (error) throw error
63
+ })
64
+ ```
65
+
66
+ ### Stripe
67
+
68
+ ```ts
69
+ rely.healthCheck('stripe', async () => {
70
+ await stripe.balance.retrieve()
71
+ })
72
+ ```
73
+
74
+ ### Resend
75
+
76
+ ```ts
77
+ rely.healthCheck('resend', async () => {
78
+ const res = await fetch('https://api.resend.com/domains', {
79
+ headers: { Authorization: `Bearer ${process.env.RESEND_API_KEY}` }
80
+ })
81
+ if (!res.ok) throw new Error(`Resend returned ${res.status}`)
82
+ })
83
+ ```
84
+
85
+ ### Redis / Upstash
86
+
87
+ ```ts
88
+ rely.healthCheck('redis', async () => {
89
+ await redis.ping()
90
+ })
91
+ ```
92
+
93
+ ### PlanetScale / MySQL
94
+
95
+ ```ts
96
+ rely.healthCheck('database', async () => {
97
+ await db.execute('SELECT 1')
98
+ })
99
+ ```
100
+
101
+ ### OpenAI
102
+
103
+ ```ts
104
+ rely.healthCheck('openai', async () => {
105
+ await openai.models.list()
106
+ })
107
+ ```
108
+
109
+ ### Anthropic
110
+
111
+ ```ts
112
+ rely.healthCheck('anthropic', async () => {
113
+ const res = await fetch('https://api.anthropic.com/v1/models', {
114
+ headers: { 'x-api-key': process.env.ANTHROPIC_API_KEY! }
115
+ })
116
+ if (!res.ok) throw new Error(`Anthropic returned ${res.status}`)
117
+ })
118
+ ```
119
+
120
+ ## Custom metrics
121
+
122
+ Send any numeric value to chart it in rely.net:
123
+
124
+ ```ts
125
+ rely.metric('checkout.conversion_rate', 0.034)
126
+ rely.metric('queue.depth', 142, { queue: 'email' })
127
+ rely.metric('api.active_connections', 58)
128
+ ```
129
+
130
+ Metrics are sent on the next flush (default: every 60 seconds).
131
+
132
+ ## Request telemetry (Next.js middleware)
133
+
134
+ Automatically capture error rates and response times:
135
+
136
+ ```ts
137
+ // middleware.ts (in your Next.js project root)
138
+ import { withRelyMiddleware } from '@rely-net/sdk'
139
+ import { rely } from './instrumentation'
140
+
141
+ export default withRelyMiddleware(rely)
142
+
143
+ export const config = {
144
+ matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
145
+ }
146
+ ```
147
+
148
+ ## Deployment markers
149
+
150
+ Deployment markers are sent automatically when the SDK initializes. They appear as vertical lines on your charts in rely.net, making it easy to correlate issues with deploys.
151
+
152
+ To send a manual deployment marker with custom metadata:
153
+
154
+ ```ts
155
+ rely.deployment({
156
+ version: process.env.MY_APP_VERSION,
157
+ metadata: { team: 'backend', feature_flags: 'new-checkout' }
158
+ })
159
+ ```
160
+
161
+ ## Configuration options
162
+
163
+ ```ts
164
+ new Rely({
165
+ apiKey: string, // required
166
+ baseUrl?: string, // default: 'https://rely.net'
167
+ environment?: string, // default: process.env.NODE_ENV
168
+ flushInterval?: number, // default: 60000 (ms)
169
+ sanitizeErrors?: boolean, // default: true (recommended)
170
+ debug?: boolean, // default: false
171
+ })
172
+ ```
173
+
174
+ ## Security
175
+
176
+ By default, the SDK automatically redacts common secret patterns from error messages before sending them to rely.net. This protects API keys, tokens, and passwords from being accidentally transmitted.
177
+
178
+ Patterns redacted include:
179
+
180
+ - Stripe API keys (`sk_live_`, `pk_live_`, etc.)
181
+ - Bearer tokens
182
+ - Passwords and secrets in error strings
183
+ - AWS access key patterns
184
+ - rely.net API keys
185
+
186
+ Set `sanitizeErrors: false` only if you have verified your error messages never contain sensitive data.
187
+
188
+ ## TypeScript
189
+
190
+ The SDK is written in TypeScript and ships with full type definitions. No `@types` package needed.
191
+
192
+ ```ts
193
+ import type { HealthCheckFn, RelyClientOptions } from '@rely-net/sdk'
194
+ ```
195
+
196
+ ## License
197
+
198
+ MIT
@@ -0,0 +1,133 @@
1
+ declare const SDK_VERSION = "1.0.0";
2
+ type Environment = "production" | "staging" | "development";
3
+ type HealthCheckStatus = "passing" | "failing";
4
+ type HealthCheckFn = () => Promise<void>;
5
+ interface HealthCheckResult {
6
+ name: string;
7
+ status: HealthCheckStatus;
8
+ duration_ms: number;
9
+ error_message?: string;
10
+ stack_trace?: string;
11
+ }
12
+ interface DeploymentInfo {
13
+ version?: string;
14
+ environment?: string;
15
+ branch?: string;
16
+ commit_message?: string;
17
+ metadata?: Record<string, string>;
18
+ }
19
+ interface MetricDatapoint {
20
+ name: string;
21
+ value: number;
22
+ tags?: Record<string, string>;
23
+ unit?: string;
24
+ }
25
+ interface RuntimeStats {
26
+ memory_heap_used_mb: number;
27
+ memory_heap_total_mb: number;
28
+ memory_rss_mb: number;
29
+ cpu_usage_percent: number;
30
+ process_uptime_secs: number;
31
+ node_version: string;
32
+ region: string;
33
+ }
34
+ interface RouteStats {
35
+ route: string;
36
+ p95_ms: number;
37
+ count: number;
38
+ }
39
+ interface RequestWindowData {
40
+ window_start: string;
41
+ window_end: string;
42
+ total_requests: number;
43
+ status_2xx: number;
44
+ status_3xx: number;
45
+ status_4xx: number;
46
+ status_5xx: number;
47
+ p50_ms: number;
48
+ p95_ms: number;
49
+ p99_ms: number;
50
+ slowest_routes: RouteStats[];
51
+ }
52
+ interface DeploymentPayload {
53
+ version: string;
54
+ environment: string;
55
+ branch: string;
56
+ commit_message: string;
57
+ framework: string;
58
+ framework_version: string;
59
+ node_version: string;
60
+ region: string;
61
+ deployment_url: string;
62
+ metadata: Record<string, unknown>;
63
+ }
64
+ interface IngestPayload {
65
+ version: string;
66
+ timestamp: string;
67
+ deployment?: DeploymentPayload;
68
+ health_checks?: HealthCheckResult[];
69
+ metrics?: MetricDatapoint[];
70
+ runtime?: RuntimeStats;
71
+ request_telemetry?: RequestWindowData;
72
+ }
73
+ interface IngestResponse {
74
+ received: boolean;
75
+ processed: {
76
+ deployment: boolean;
77
+ health_checks: number;
78
+ metrics: number;
79
+ runtime: boolean;
80
+ request_telemetry: boolean;
81
+ };
82
+ warnings: string[];
83
+ timestamp: string;
84
+ }
85
+ interface RelyClientOptions {
86
+ apiKey: string;
87
+ baseUrl?: string;
88
+ environment?: Environment;
89
+ flushInterval?: number;
90
+ sanitizeErrors?: boolean;
91
+ debug?: boolean;
92
+ }
93
+
94
+ declare class RelyClient {
95
+ private readonly apiKey;
96
+ private readonly baseUrl;
97
+ private readonly environment;
98
+ private readonly flushInterval;
99
+ private readonly sanitizeErrors;
100
+ private readonly debug;
101
+ private healthChecks;
102
+ private pendingMetrics;
103
+ private requestBuffer;
104
+ private flushTimer;
105
+ private deploymentSent;
106
+ private isDestroyed;
107
+ constructor(options: RelyClientOptions);
108
+ healthCheck(name: string, fn: HealthCheckFn): this;
109
+ metric(name: string, value: number, tags?: Record<string, string>): this;
110
+ deployment(info: DeploymentInfo): void;
111
+ recordRequest(route: string, statusCode: number, durationMs: number): void;
112
+ flush(): Promise<void>;
113
+ destroy(): void;
114
+ private initialize;
115
+ private runHealthChecks;
116
+ private collectRuntimeStats;
117
+ private sendDeploymentMarker;
118
+ private sendPayload;
119
+ private sanitize;
120
+ private sanitizeTags;
121
+ private detectFrameworkVersion;
122
+ private log;
123
+ }
124
+
125
+ type NextMiddlewareResult = Response | undefined | null;
126
+ type NextMiddleware = (request: Request, event: unknown) => NextMiddlewareResult | Promise<NextMiddlewareResult>;
127
+ type WrappedMiddleware = (request: Request, event: unknown) => Promise<NextMiddlewareResult>;
128
+ declare function withRelyMiddleware(rely: RelyClient, middleware?: NextMiddleware): WrappedMiddleware;
129
+
130
+ declare function createRelyClient(options: RelyClientOptions): RelyClient;
131
+ declare function getRelyClient(): RelyClient;
132
+
133
+ export { type DeploymentInfo, type Environment, type HealthCheckFn, type HealthCheckResult, type IngestPayload, type IngestResponse, type MetricDatapoint, RelyClient as Rely, RelyClient, type RelyClientOptions, type RequestWindowData, type RouteStats, type RuntimeStats, SDK_VERSION, createRelyClient, getRelyClient, withRelyMiddleware };
@@ -0,0 +1,133 @@
1
+ declare const SDK_VERSION = "1.0.0";
2
+ type Environment = "production" | "staging" | "development";
3
+ type HealthCheckStatus = "passing" | "failing";
4
+ type HealthCheckFn = () => Promise<void>;
5
+ interface HealthCheckResult {
6
+ name: string;
7
+ status: HealthCheckStatus;
8
+ duration_ms: number;
9
+ error_message?: string;
10
+ stack_trace?: string;
11
+ }
12
+ interface DeploymentInfo {
13
+ version?: string;
14
+ environment?: string;
15
+ branch?: string;
16
+ commit_message?: string;
17
+ metadata?: Record<string, string>;
18
+ }
19
+ interface MetricDatapoint {
20
+ name: string;
21
+ value: number;
22
+ tags?: Record<string, string>;
23
+ unit?: string;
24
+ }
25
+ interface RuntimeStats {
26
+ memory_heap_used_mb: number;
27
+ memory_heap_total_mb: number;
28
+ memory_rss_mb: number;
29
+ cpu_usage_percent: number;
30
+ process_uptime_secs: number;
31
+ node_version: string;
32
+ region: string;
33
+ }
34
+ interface RouteStats {
35
+ route: string;
36
+ p95_ms: number;
37
+ count: number;
38
+ }
39
+ interface RequestWindowData {
40
+ window_start: string;
41
+ window_end: string;
42
+ total_requests: number;
43
+ status_2xx: number;
44
+ status_3xx: number;
45
+ status_4xx: number;
46
+ status_5xx: number;
47
+ p50_ms: number;
48
+ p95_ms: number;
49
+ p99_ms: number;
50
+ slowest_routes: RouteStats[];
51
+ }
52
+ interface DeploymentPayload {
53
+ version: string;
54
+ environment: string;
55
+ branch: string;
56
+ commit_message: string;
57
+ framework: string;
58
+ framework_version: string;
59
+ node_version: string;
60
+ region: string;
61
+ deployment_url: string;
62
+ metadata: Record<string, unknown>;
63
+ }
64
+ interface IngestPayload {
65
+ version: string;
66
+ timestamp: string;
67
+ deployment?: DeploymentPayload;
68
+ health_checks?: HealthCheckResult[];
69
+ metrics?: MetricDatapoint[];
70
+ runtime?: RuntimeStats;
71
+ request_telemetry?: RequestWindowData;
72
+ }
73
+ interface IngestResponse {
74
+ received: boolean;
75
+ processed: {
76
+ deployment: boolean;
77
+ health_checks: number;
78
+ metrics: number;
79
+ runtime: boolean;
80
+ request_telemetry: boolean;
81
+ };
82
+ warnings: string[];
83
+ timestamp: string;
84
+ }
85
+ interface RelyClientOptions {
86
+ apiKey: string;
87
+ baseUrl?: string;
88
+ environment?: Environment;
89
+ flushInterval?: number;
90
+ sanitizeErrors?: boolean;
91
+ debug?: boolean;
92
+ }
93
+
94
+ declare class RelyClient {
95
+ private readonly apiKey;
96
+ private readonly baseUrl;
97
+ private readonly environment;
98
+ private readonly flushInterval;
99
+ private readonly sanitizeErrors;
100
+ private readonly debug;
101
+ private healthChecks;
102
+ private pendingMetrics;
103
+ private requestBuffer;
104
+ private flushTimer;
105
+ private deploymentSent;
106
+ private isDestroyed;
107
+ constructor(options: RelyClientOptions);
108
+ healthCheck(name: string, fn: HealthCheckFn): this;
109
+ metric(name: string, value: number, tags?: Record<string, string>): this;
110
+ deployment(info: DeploymentInfo): void;
111
+ recordRequest(route: string, statusCode: number, durationMs: number): void;
112
+ flush(): Promise<void>;
113
+ destroy(): void;
114
+ private initialize;
115
+ private runHealthChecks;
116
+ private collectRuntimeStats;
117
+ private sendDeploymentMarker;
118
+ private sendPayload;
119
+ private sanitize;
120
+ private sanitizeTags;
121
+ private detectFrameworkVersion;
122
+ private log;
123
+ }
124
+
125
+ type NextMiddlewareResult = Response | undefined | null;
126
+ type NextMiddleware = (request: Request, event: unknown) => NextMiddlewareResult | Promise<NextMiddlewareResult>;
127
+ type WrappedMiddleware = (request: Request, event: unknown) => Promise<NextMiddlewareResult>;
128
+ declare function withRelyMiddleware(rely: RelyClient, middleware?: NextMiddleware): WrappedMiddleware;
129
+
130
+ declare function createRelyClient(options: RelyClientOptions): RelyClient;
131
+ declare function getRelyClient(): RelyClient;
132
+
133
+ export { type DeploymentInfo, type Environment, type HealthCheckFn, type HealthCheckResult, type IngestPayload, type IngestResponse, type MetricDatapoint, RelyClient as Rely, RelyClient, type RelyClientOptions, type RequestWindowData, type RouteStats, type RuntimeStats, SDK_VERSION, createRelyClient, getRelyClient, withRelyMiddleware };
package/dist/index.js ADDED
@@ -0,0 +1,457 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ Rely: () => RelyClient,
34
+ RelyClient: () => RelyClient,
35
+ SDK_VERSION: () => SDK_VERSION2,
36
+ createRelyClient: () => createRelyClient,
37
+ getRelyClient: () => getRelyClient,
38
+ withRelyMiddleware: () => withRelyMiddleware
39
+ });
40
+ module.exports = __toCommonJS(index_exports);
41
+
42
+ // src/collectors/requests.ts
43
+ function percentile(arr, p) {
44
+ if (arr.length === 0) return 0;
45
+ const sorted = [...arr].sort((a, b) => a - b);
46
+ const index = Math.ceil(p / 100 * sorted.length) - 1;
47
+ return sorted[Math.max(0, index)];
48
+ }
49
+ var RequestBuffer = class {
50
+ constructor() {
51
+ this.routes = /* @__PURE__ */ new Map();
52
+ this.windowStart = /* @__PURE__ */ new Date();
53
+ }
54
+ record(route, statusCode, durationMs) {
55
+ const normalized = this.normalizeRoute(route);
56
+ if (!this.routes.has(normalized)) {
57
+ this.routes.set(normalized, { durations: [], statusCodes: [] });
58
+ }
59
+ const r = this.routes.get(normalized);
60
+ r.durations.push(Math.max(0, durationMs));
61
+ r.statusCodes.push(statusCode);
62
+ }
63
+ flush() {
64
+ const allDurations = [];
65
+ const allStatusCodes = [];
66
+ const routeStats = [];
67
+ for (const [route, data] of Array.from(this.routes.entries())) {
68
+ allDurations.push(...data.durations);
69
+ allStatusCodes.push(...data.statusCodes);
70
+ routeStats.push({
71
+ route,
72
+ p95_ms: percentile(data.durations, 95),
73
+ count: data.durations.length
74
+ });
75
+ }
76
+ const result = {
77
+ window_start: this.windowStart.toISOString(),
78
+ window_end: (/* @__PURE__ */ new Date()).toISOString(),
79
+ total_requests: allDurations.length,
80
+ status_2xx: allStatusCodes.filter((s) => s >= 200 && s < 300).length,
81
+ status_3xx: allStatusCodes.filter((s) => s >= 300 && s < 400).length,
82
+ status_4xx: allStatusCodes.filter((s) => s >= 400 && s < 500).length,
83
+ status_5xx: allStatusCodes.filter((s) => s >= 500).length,
84
+ p50_ms: percentile(allDurations, 50),
85
+ p95_ms: percentile(allDurations, 95),
86
+ p99_ms: percentile(allDurations, 99),
87
+ slowest_routes: routeStats.sort((a, b) => b.p95_ms - a.p95_ms).slice(0, 10)
88
+ };
89
+ this.routes = /* @__PURE__ */ new Map();
90
+ this.windowStart = /* @__PURE__ */ new Date();
91
+ return result;
92
+ }
93
+ get totalRequests() {
94
+ let total = 0;
95
+ for (const data of Array.from(this.routes.values())) {
96
+ total += data.durations.length;
97
+ }
98
+ return total;
99
+ }
100
+ // Replace dynamic path segments with placeholders to avoid
101
+ // high-cardinality route keys in the database.
102
+ // /users/123 → /users/[id]
103
+ // /posts/abc-def-123 → /posts/[id]
104
+ // /api/v1/orders/99 → /api/v1/orders/[id]
105
+ normalizeRoute(path) {
106
+ return path.replace(
107
+ /\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi,
108
+ "/[id]"
109
+ ).replace(/\/\d+/g, "/[id]").replace(/\/[a-zA-Z0-9]{20,}/g, "/[id]");
110
+ }
111
+ };
112
+
113
+ // src/client.ts
114
+ var SDK_VERSION = "1.0.0";
115
+ var SECRET_PATTERNS = [
116
+ /sk_live_[a-zA-Z0-9_]+/g,
117
+ /sk_test_[a-zA-Z0-9_]+/g,
118
+ /pk_live_[a-zA-Z0-9_]+/g,
119
+ /pk_test_[a-zA-Z0-9_]+/g,
120
+ /rely_live_[a-zA-Z0-9_]+/g,
121
+ /rely_test_[a-zA-Z0-9_]+/g,
122
+ /Bearer\s+[a-zA-Z0-9._\-]+/g,
123
+ /password\s*[=:]\s*["']?[^\s"',}\]]{4,}/gi,
124
+ /secret\s*[=:]\s*["']?[^\s"',}\]]{4,}/gi,
125
+ /api[_\-]?key\s*[=:]\s*["']?[^\s"',}\]]{4,}/gi,
126
+ /token\s*[=:]\s*["']?[^\s"',}\]]{4,}/gi,
127
+ /AKIA[A-Z0-9]{16}/g,
128
+ /[a-z0-9]{32,}:[a-z0-9]{32,}/g
129
+ // generic key:secret format
130
+ ];
131
+ var MIN_FLUSH_INTERVAL = 1e4;
132
+ var DEFAULT_FLUSH_INTERVAL = 6e4;
133
+ var INGEST_TIMEOUT_MS = 1e4;
134
+ var MAX_ERROR_MESSAGE_LENGTH = 2e3;
135
+ var MAX_STACK_TRACE_LENGTH = 1e4;
136
+ var MAX_METRIC_NAME_LENGTH = 100;
137
+ var MAX_HEALTH_CHECK_NAME_LENGTH = 100;
138
+ var RelyClient = class {
139
+ constructor(options) {
140
+ if (!options.apiKey || typeof options.apiKey !== "string") {
141
+ throw new Error(
142
+ "[Rely] apiKey is required. Get your API key at rely.net/settings/api-keys"
143
+ );
144
+ }
145
+ if (!options.apiKey.startsWith("rely_")) {
146
+ console.warn(
147
+ "[Rely] Warning: API key format looks incorrect. Keys should start with rely_live_ or rely_test_"
148
+ );
149
+ }
150
+ this.apiKey = options.apiKey;
151
+ this.baseUrl = (options.baseUrl ?? "https://rely.net").replace(/\/$/, "");
152
+ this.environment = options.environment ?? process.env.NODE_ENV ?? "production";
153
+ this.flushInterval = Math.max(
154
+ MIN_FLUSH_INTERVAL,
155
+ options.flushInterval ?? DEFAULT_FLUSH_INTERVAL
156
+ );
157
+ this.sanitizeErrors = options.sanitizeErrors ?? true;
158
+ this.debug = options.debug ?? false;
159
+ this.healthChecks = /* @__PURE__ */ new Map();
160
+ this.pendingMetrics = [];
161
+ this.requestBuffer = new RequestBuffer();
162
+ this.flushTimer = null;
163
+ this.deploymentSent = false;
164
+ this.isDestroyed = false;
165
+ this.log(`SDK initialized`);
166
+ this.log(`Environment: ${this.environment}`);
167
+ this.log(`Flush interval: ${this.flushInterval / 1e3}s`);
168
+ this.log(`Base URL: ${this.baseUrl}`);
169
+ this.initialize();
170
+ }
171
+ // Register a health check function.
172
+ // The function should throw if the check fails.
173
+ // Returns `this` for chaining.
174
+ healthCheck(name, fn) {
175
+ if (this.isDestroyed) {
176
+ this.log("Warning: SDK has been destroyed, ignoring healthCheck()");
177
+ return this;
178
+ }
179
+ const trimmedName = name?.trim();
180
+ if (!trimmedName) {
181
+ throw new Error("[Rely] Health check name must be a non-empty string");
182
+ }
183
+ if (trimmedName.length > MAX_HEALTH_CHECK_NAME_LENGTH) {
184
+ throw new Error(
185
+ `[Rely] Health check name must be under ${MAX_HEALTH_CHECK_NAME_LENGTH} characters`
186
+ );
187
+ }
188
+ if (typeof fn !== "function") {
189
+ throw new Error(
190
+ "[Rely] Health check must be a function that returns a Promise"
191
+ );
192
+ }
193
+ this.healthChecks.set(trimmedName, fn);
194
+ this.log(`Registered health check: "${trimmedName}"`);
195
+ return this;
196
+ }
197
+ // Send a custom metric value.
198
+ // Returns `this` for chaining.
199
+ metric(name, value, tags) {
200
+ if (this.isDestroyed) return this;
201
+ if (typeof value !== "number" || !isFinite(value)) {
202
+ this.log(`Warning: invalid metric value for "${name}": ${value}`);
203
+ return this;
204
+ }
205
+ const trimmedName = name?.trim();
206
+ if (!trimmedName || trimmedName.length > MAX_METRIC_NAME_LENGTH) {
207
+ this.log(`Warning: invalid metric name: "${name}"`);
208
+ return this;
209
+ }
210
+ this.pendingMetrics.push({
211
+ name: trimmedName,
212
+ value,
213
+ tags: tags ? this.sanitizeTags(tags) : void 0
214
+ });
215
+ return this;
216
+ }
217
+ // Manually send a deployment marker.
218
+ // Called automatically on initialization in production.
219
+ // Use this to send additional metadata with your deployment.
220
+ deployment(info) {
221
+ if (this.isDestroyed) return;
222
+ this.sendDeploymentMarker(info);
223
+ }
224
+ // Called by withRelyMiddleware on each HTTP request.
225
+ // Not intended to be called directly in most cases.
226
+ recordRequest(route, statusCode, durationMs) {
227
+ if (this.isDestroyed) return;
228
+ this.requestBuffer.record(route, statusCode, durationMs);
229
+ }
230
+ // Manually trigger a flush of all pending data.
231
+ // The SDK flushes automatically on the flush interval.
232
+ // Use this for graceful shutdown scenarios.
233
+ async flush() {
234
+ if (this.isDestroyed) return;
235
+ const payload = {
236
+ version: SDK_VERSION,
237
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
238
+ };
239
+ if (this.healthChecks.size > 0) {
240
+ payload.health_checks = await this.runHealthChecks();
241
+ }
242
+ if (this.pendingMetrics.length > 0) {
243
+ payload.metrics = [...this.pendingMetrics];
244
+ this.pendingMetrics = [];
245
+ }
246
+ payload.runtime = this.collectRuntimeStats();
247
+ if (this.requestBuffer.totalRequests > 0) {
248
+ payload.request_telemetry = this.requestBuffer.flush();
249
+ }
250
+ await this.sendPayload(payload);
251
+ }
252
+ // Destroy the client and stop all background activity.
253
+ // Call this during graceful shutdown if needed.
254
+ destroy() {
255
+ if (this.isDestroyed) return;
256
+ this.isDestroyed = true;
257
+ if (this.flushTimer) {
258
+ clearInterval(this.flushTimer);
259
+ this.flushTimer = null;
260
+ }
261
+ this.log("SDK destroyed");
262
+ }
263
+ initialize() {
264
+ if (this.environment === "production" || this.environment === "staging") {
265
+ this.sendDeploymentMarker();
266
+ }
267
+ this.flushTimer = setInterval(() => {
268
+ this.flush().catch((err) => {
269
+ this.log(`Flush error: ${err instanceof Error ? err.message : err}`);
270
+ });
271
+ }, this.flushInterval);
272
+ if (typeof this.flushTimer === "object" && this.flushTimer !== null) {
273
+ const timer = this.flushTimer;
274
+ if (typeof timer.unref === "function") {
275
+ timer.unref();
276
+ }
277
+ }
278
+ }
279
+ async runHealthChecks() {
280
+ const results = await Promise.allSettled(
281
+ Array.from(this.healthChecks.entries()).map(
282
+ async ([name, fn]) => {
283
+ const start = Date.now();
284
+ try {
285
+ await fn();
286
+ const duration = Date.now() - start;
287
+ this.log(`\u2713 Health check "${name}" passed (${duration}ms)`);
288
+ return {
289
+ name,
290
+ status: "passing",
291
+ duration_ms: duration
292
+ };
293
+ } catch (err) {
294
+ const duration = Date.now() - start;
295
+ const error = err instanceof Error ? err : new Error(String(err));
296
+ this.log(`\u2717 Health check "${name}" failed: ${error.message}`);
297
+ return {
298
+ name,
299
+ status: "failing",
300
+ duration_ms: duration,
301
+ error_message: this.sanitizeErrors ? this.sanitize(
302
+ error.message.slice(0, MAX_ERROR_MESSAGE_LENGTH)
303
+ ) : error.message.slice(0, MAX_ERROR_MESSAGE_LENGTH),
304
+ stack_trace: error.stack ? this.sanitizeErrors ? this.sanitize(
305
+ error.stack.slice(0, MAX_STACK_TRACE_LENGTH)
306
+ ) : error.stack.slice(0, MAX_STACK_TRACE_LENGTH) : void 0
307
+ };
308
+ }
309
+ }
310
+ )
311
+ );
312
+ return results.filter(
313
+ (r) => r.status === "fulfilled"
314
+ ).map((r) => r.value);
315
+ }
316
+ collectRuntimeStats() {
317
+ const mem = process.memoryUsage();
318
+ return {
319
+ memory_heap_used_mb: Math.round(mem.heapUsed / 1024 / 1024 * 100) / 100,
320
+ memory_heap_total_mb: Math.round(mem.heapTotal / 1024 / 1024 * 100) / 100,
321
+ memory_rss_mb: Math.round(mem.rss / 1024 / 1024 * 100) / 100,
322
+ cpu_usage_percent: 0,
323
+ process_uptime_secs: Math.floor(process.uptime()),
324
+ node_version: process.version,
325
+ region: process.env.VERCEL_REGION ?? process.env.AWS_REGION ?? process.env.FLY_REGION ?? process.env.RAILWAY_REGION ?? "unknown"
326
+ };
327
+ }
328
+ sendDeploymentMarker(info) {
329
+ if (this.deploymentSent && !info) return;
330
+ this.deploymentSent = true;
331
+ const payload = {
332
+ version: SDK_VERSION,
333
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
334
+ deployment: {
335
+ version: info?.version ?? process.env.VERCEL_GIT_COMMIT_SHA ?? process.env.RAILWAY_GIT_COMMIT_SHA ?? process.env.FLY_APP_VERSION ?? process.env.RENDER_GIT_COMMIT ?? "unknown",
336
+ environment: info?.environment ?? this.environment,
337
+ branch: info?.branch ?? process.env.VERCEL_GIT_COMMIT_REF ?? process.env.RAILWAY_GIT_BRANCH ?? process.env.RENDER_GIT_BRANCH ?? "unknown",
338
+ commit_message: info?.commit_message ?? process.env.VERCEL_GIT_COMMIT_MESSAGE ?? "",
339
+ framework: "next.js",
340
+ framework_version: this.detectFrameworkVersion(),
341
+ node_version: process.version,
342
+ region: process.env.VERCEL_REGION ?? "unknown",
343
+ deployment_url: process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : "",
344
+ metadata: info?.metadata ?? {}
345
+ }
346
+ };
347
+ this.sendPayload(payload).catch(() => {
348
+ });
349
+ }
350
+ async sendPayload(payload) {
351
+ try {
352
+ this.log(`Sending payload (${JSON.stringify(payload).length} bytes)`);
353
+ const response = await fetch(`${this.baseUrl}/api/sdk/ingest`, {
354
+ method: "POST",
355
+ headers: {
356
+ "Content-Type": "application/json",
357
+ Authorization: `Bearer ${this.apiKey}`,
358
+ "User-Agent": `@rely-net/sdk/${SDK_VERSION}`
359
+ },
360
+ body: JSON.stringify(payload),
361
+ signal: AbortSignal.timeout(INGEST_TIMEOUT_MS)
362
+ });
363
+ if (this.debug) {
364
+ if (response.ok) {
365
+ const body = await response.json();
366
+ this.log(`Flush successful`);
367
+ if (body.warnings?.length > 0) {
368
+ body.warnings.forEach((w) => this.log(`Warning: ${w}`));
369
+ }
370
+ } else {
371
+ this.log(`Ingest returned HTTP ${response.status}`);
372
+ }
373
+ }
374
+ } catch (err) {
375
+ this.log(
376
+ `Send failed: ${err instanceof Error ? err.message : String(err)}`
377
+ );
378
+ }
379
+ }
380
+ sanitize(str) {
381
+ let result = str;
382
+ for (const pattern of SECRET_PATTERNS) {
383
+ pattern.lastIndex = 0;
384
+ result = result.replace(pattern, "[REDACTED]");
385
+ }
386
+ return result;
387
+ }
388
+ sanitizeTags(tags) {
389
+ const sanitized = {};
390
+ for (const [key, value] of Object.entries(tags)) {
391
+ sanitized[key] = this.sanitize(String(value).slice(0, 200));
392
+ }
393
+ return sanitized;
394
+ }
395
+ detectFrameworkVersion() {
396
+ try {
397
+ const pkg = require("next/package.json");
398
+ return pkg.version;
399
+ } catch {
400
+ return "unknown";
401
+ }
402
+ }
403
+ log(message) {
404
+ if (this.debug) {
405
+ console.log(`[Rely] ${message}`);
406
+ }
407
+ }
408
+ };
409
+
410
+ // src/middleware.ts
411
+ function withRelyMiddleware(rely, middleware) {
412
+ return async (request, event) => {
413
+ const start = Date.now();
414
+ const url = new URL(request.url);
415
+ let response;
416
+ if (middleware) {
417
+ response = await middleware(request, event);
418
+ } else {
419
+ const { NextResponse } = await import("next/server");
420
+ response = NextResponse.next();
421
+ }
422
+ const duration = Date.now() - start;
423
+ const status = response?.status ?? 200;
424
+ const path = url.pathname;
425
+ if (!path.startsWith("/_next/") && !path.startsWith("/favicon") && !path.startsWith("/robots") && !path.startsWith("/sitemap")) {
426
+ rely.recordRequest(path, status, duration);
427
+ }
428
+ return response;
429
+ };
430
+ }
431
+
432
+ // src/types.ts
433
+ var SDK_VERSION2 = "1.0.0";
434
+
435
+ // src/index.ts
436
+ var _instance = null;
437
+ function createRelyClient(options) {
438
+ _instance = new RelyClient(options);
439
+ return _instance;
440
+ }
441
+ function getRelyClient() {
442
+ if (!_instance) {
443
+ throw new Error(
444
+ "[Rely] SDK not initialized.\nCall createRelyClient() in your instrumentation.ts file first.\nSee https://rely.net/docs/sdk for setup instructions."
445
+ );
446
+ }
447
+ return _instance;
448
+ }
449
+ // Annotate the CommonJS export names for ESM import in node:
450
+ 0 && (module.exports = {
451
+ Rely,
452
+ RelyClient,
453
+ SDK_VERSION,
454
+ createRelyClient,
455
+ getRelyClient,
456
+ withRelyMiddleware
457
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,422 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/collectors/requests.ts
9
+ function percentile(arr, p) {
10
+ if (arr.length === 0) return 0;
11
+ const sorted = [...arr].sort((a, b) => a - b);
12
+ const index = Math.ceil(p / 100 * sorted.length) - 1;
13
+ return sorted[Math.max(0, index)];
14
+ }
15
+ var RequestBuffer = class {
16
+ constructor() {
17
+ this.routes = /* @__PURE__ */ new Map();
18
+ this.windowStart = /* @__PURE__ */ new Date();
19
+ }
20
+ record(route, statusCode, durationMs) {
21
+ const normalized = this.normalizeRoute(route);
22
+ if (!this.routes.has(normalized)) {
23
+ this.routes.set(normalized, { durations: [], statusCodes: [] });
24
+ }
25
+ const r = this.routes.get(normalized);
26
+ r.durations.push(Math.max(0, durationMs));
27
+ r.statusCodes.push(statusCode);
28
+ }
29
+ flush() {
30
+ const allDurations = [];
31
+ const allStatusCodes = [];
32
+ const routeStats = [];
33
+ for (const [route, data] of Array.from(this.routes.entries())) {
34
+ allDurations.push(...data.durations);
35
+ allStatusCodes.push(...data.statusCodes);
36
+ routeStats.push({
37
+ route,
38
+ p95_ms: percentile(data.durations, 95),
39
+ count: data.durations.length
40
+ });
41
+ }
42
+ const result = {
43
+ window_start: this.windowStart.toISOString(),
44
+ window_end: (/* @__PURE__ */ new Date()).toISOString(),
45
+ total_requests: allDurations.length,
46
+ status_2xx: allStatusCodes.filter((s) => s >= 200 && s < 300).length,
47
+ status_3xx: allStatusCodes.filter((s) => s >= 300 && s < 400).length,
48
+ status_4xx: allStatusCodes.filter((s) => s >= 400 && s < 500).length,
49
+ status_5xx: allStatusCodes.filter((s) => s >= 500).length,
50
+ p50_ms: percentile(allDurations, 50),
51
+ p95_ms: percentile(allDurations, 95),
52
+ p99_ms: percentile(allDurations, 99),
53
+ slowest_routes: routeStats.sort((a, b) => b.p95_ms - a.p95_ms).slice(0, 10)
54
+ };
55
+ this.routes = /* @__PURE__ */ new Map();
56
+ this.windowStart = /* @__PURE__ */ new Date();
57
+ return result;
58
+ }
59
+ get totalRequests() {
60
+ let total = 0;
61
+ for (const data of Array.from(this.routes.values())) {
62
+ total += data.durations.length;
63
+ }
64
+ return total;
65
+ }
66
+ // Replace dynamic path segments with placeholders to avoid
67
+ // high-cardinality route keys in the database.
68
+ // /users/123 → /users/[id]
69
+ // /posts/abc-def-123 → /posts/[id]
70
+ // /api/v1/orders/99 → /api/v1/orders/[id]
71
+ normalizeRoute(path) {
72
+ return path.replace(
73
+ /\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi,
74
+ "/[id]"
75
+ ).replace(/\/\d+/g, "/[id]").replace(/\/[a-zA-Z0-9]{20,}/g, "/[id]");
76
+ }
77
+ };
78
+
79
+ // src/client.ts
80
+ var SDK_VERSION = "1.0.0";
81
+ var SECRET_PATTERNS = [
82
+ /sk_live_[a-zA-Z0-9_]+/g,
83
+ /sk_test_[a-zA-Z0-9_]+/g,
84
+ /pk_live_[a-zA-Z0-9_]+/g,
85
+ /pk_test_[a-zA-Z0-9_]+/g,
86
+ /rely_live_[a-zA-Z0-9_]+/g,
87
+ /rely_test_[a-zA-Z0-9_]+/g,
88
+ /Bearer\s+[a-zA-Z0-9._\-]+/g,
89
+ /password\s*[=:]\s*["']?[^\s"',}\]]{4,}/gi,
90
+ /secret\s*[=:]\s*["']?[^\s"',}\]]{4,}/gi,
91
+ /api[_\-]?key\s*[=:]\s*["']?[^\s"',}\]]{4,}/gi,
92
+ /token\s*[=:]\s*["']?[^\s"',}\]]{4,}/gi,
93
+ /AKIA[A-Z0-9]{16}/g,
94
+ /[a-z0-9]{32,}:[a-z0-9]{32,}/g
95
+ // generic key:secret format
96
+ ];
97
+ var MIN_FLUSH_INTERVAL = 1e4;
98
+ var DEFAULT_FLUSH_INTERVAL = 6e4;
99
+ var INGEST_TIMEOUT_MS = 1e4;
100
+ var MAX_ERROR_MESSAGE_LENGTH = 2e3;
101
+ var MAX_STACK_TRACE_LENGTH = 1e4;
102
+ var MAX_METRIC_NAME_LENGTH = 100;
103
+ var MAX_HEALTH_CHECK_NAME_LENGTH = 100;
104
+ var RelyClient = class {
105
+ constructor(options) {
106
+ if (!options.apiKey || typeof options.apiKey !== "string") {
107
+ throw new Error(
108
+ "[Rely] apiKey is required. Get your API key at rely.net/settings/api-keys"
109
+ );
110
+ }
111
+ if (!options.apiKey.startsWith("rely_")) {
112
+ console.warn(
113
+ "[Rely] Warning: API key format looks incorrect. Keys should start with rely_live_ or rely_test_"
114
+ );
115
+ }
116
+ this.apiKey = options.apiKey;
117
+ this.baseUrl = (options.baseUrl ?? "https://rely.net").replace(/\/$/, "");
118
+ this.environment = options.environment ?? process.env.NODE_ENV ?? "production";
119
+ this.flushInterval = Math.max(
120
+ MIN_FLUSH_INTERVAL,
121
+ options.flushInterval ?? DEFAULT_FLUSH_INTERVAL
122
+ );
123
+ this.sanitizeErrors = options.sanitizeErrors ?? true;
124
+ this.debug = options.debug ?? false;
125
+ this.healthChecks = /* @__PURE__ */ new Map();
126
+ this.pendingMetrics = [];
127
+ this.requestBuffer = new RequestBuffer();
128
+ this.flushTimer = null;
129
+ this.deploymentSent = false;
130
+ this.isDestroyed = false;
131
+ this.log(`SDK initialized`);
132
+ this.log(`Environment: ${this.environment}`);
133
+ this.log(`Flush interval: ${this.flushInterval / 1e3}s`);
134
+ this.log(`Base URL: ${this.baseUrl}`);
135
+ this.initialize();
136
+ }
137
+ // Register a health check function.
138
+ // The function should throw if the check fails.
139
+ // Returns `this` for chaining.
140
+ healthCheck(name, fn) {
141
+ if (this.isDestroyed) {
142
+ this.log("Warning: SDK has been destroyed, ignoring healthCheck()");
143
+ return this;
144
+ }
145
+ const trimmedName = name?.trim();
146
+ if (!trimmedName) {
147
+ throw new Error("[Rely] Health check name must be a non-empty string");
148
+ }
149
+ if (trimmedName.length > MAX_HEALTH_CHECK_NAME_LENGTH) {
150
+ throw new Error(
151
+ `[Rely] Health check name must be under ${MAX_HEALTH_CHECK_NAME_LENGTH} characters`
152
+ );
153
+ }
154
+ if (typeof fn !== "function") {
155
+ throw new Error(
156
+ "[Rely] Health check must be a function that returns a Promise"
157
+ );
158
+ }
159
+ this.healthChecks.set(trimmedName, fn);
160
+ this.log(`Registered health check: "${trimmedName}"`);
161
+ return this;
162
+ }
163
+ // Send a custom metric value.
164
+ // Returns `this` for chaining.
165
+ metric(name, value, tags) {
166
+ if (this.isDestroyed) return this;
167
+ if (typeof value !== "number" || !isFinite(value)) {
168
+ this.log(`Warning: invalid metric value for "${name}": ${value}`);
169
+ return this;
170
+ }
171
+ const trimmedName = name?.trim();
172
+ if (!trimmedName || trimmedName.length > MAX_METRIC_NAME_LENGTH) {
173
+ this.log(`Warning: invalid metric name: "${name}"`);
174
+ return this;
175
+ }
176
+ this.pendingMetrics.push({
177
+ name: trimmedName,
178
+ value,
179
+ tags: tags ? this.sanitizeTags(tags) : void 0
180
+ });
181
+ return this;
182
+ }
183
+ // Manually send a deployment marker.
184
+ // Called automatically on initialization in production.
185
+ // Use this to send additional metadata with your deployment.
186
+ deployment(info) {
187
+ if (this.isDestroyed) return;
188
+ this.sendDeploymentMarker(info);
189
+ }
190
+ // Called by withRelyMiddleware on each HTTP request.
191
+ // Not intended to be called directly in most cases.
192
+ recordRequest(route, statusCode, durationMs) {
193
+ if (this.isDestroyed) return;
194
+ this.requestBuffer.record(route, statusCode, durationMs);
195
+ }
196
+ // Manually trigger a flush of all pending data.
197
+ // The SDK flushes automatically on the flush interval.
198
+ // Use this for graceful shutdown scenarios.
199
+ async flush() {
200
+ if (this.isDestroyed) return;
201
+ const payload = {
202
+ version: SDK_VERSION,
203
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
204
+ };
205
+ if (this.healthChecks.size > 0) {
206
+ payload.health_checks = await this.runHealthChecks();
207
+ }
208
+ if (this.pendingMetrics.length > 0) {
209
+ payload.metrics = [...this.pendingMetrics];
210
+ this.pendingMetrics = [];
211
+ }
212
+ payload.runtime = this.collectRuntimeStats();
213
+ if (this.requestBuffer.totalRequests > 0) {
214
+ payload.request_telemetry = this.requestBuffer.flush();
215
+ }
216
+ await this.sendPayload(payload);
217
+ }
218
+ // Destroy the client and stop all background activity.
219
+ // Call this during graceful shutdown if needed.
220
+ destroy() {
221
+ if (this.isDestroyed) return;
222
+ this.isDestroyed = true;
223
+ if (this.flushTimer) {
224
+ clearInterval(this.flushTimer);
225
+ this.flushTimer = null;
226
+ }
227
+ this.log("SDK destroyed");
228
+ }
229
+ initialize() {
230
+ if (this.environment === "production" || this.environment === "staging") {
231
+ this.sendDeploymentMarker();
232
+ }
233
+ this.flushTimer = setInterval(() => {
234
+ this.flush().catch((err) => {
235
+ this.log(`Flush error: ${err instanceof Error ? err.message : err}`);
236
+ });
237
+ }, this.flushInterval);
238
+ if (typeof this.flushTimer === "object" && this.flushTimer !== null) {
239
+ const timer = this.flushTimer;
240
+ if (typeof timer.unref === "function") {
241
+ timer.unref();
242
+ }
243
+ }
244
+ }
245
+ async runHealthChecks() {
246
+ const results = await Promise.allSettled(
247
+ Array.from(this.healthChecks.entries()).map(
248
+ async ([name, fn]) => {
249
+ const start = Date.now();
250
+ try {
251
+ await fn();
252
+ const duration = Date.now() - start;
253
+ this.log(`\u2713 Health check "${name}" passed (${duration}ms)`);
254
+ return {
255
+ name,
256
+ status: "passing",
257
+ duration_ms: duration
258
+ };
259
+ } catch (err) {
260
+ const duration = Date.now() - start;
261
+ const error = err instanceof Error ? err : new Error(String(err));
262
+ this.log(`\u2717 Health check "${name}" failed: ${error.message}`);
263
+ return {
264
+ name,
265
+ status: "failing",
266
+ duration_ms: duration,
267
+ error_message: this.sanitizeErrors ? this.sanitize(
268
+ error.message.slice(0, MAX_ERROR_MESSAGE_LENGTH)
269
+ ) : error.message.slice(0, MAX_ERROR_MESSAGE_LENGTH),
270
+ stack_trace: error.stack ? this.sanitizeErrors ? this.sanitize(
271
+ error.stack.slice(0, MAX_STACK_TRACE_LENGTH)
272
+ ) : error.stack.slice(0, MAX_STACK_TRACE_LENGTH) : void 0
273
+ };
274
+ }
275
+ }
276
+ )
277
+ );
278
+ return results.filter(
279
+ (r) => r.status === "fulfilled"
280
+ ).map((r) => r.value);
281
+ }
282
+ collectRuntimeStats() {
283
+ const mem = process.memoryUsage();
284
+ return {
285
+ memory_heap_used_mb: Math.round(mem.heapUsed / 1024 / 1024 * 100) / 100,
286
+ memory_heap_total_mb: Math.round(mem.heapTotal / 1024 / 1024 * 100) / 100,
287
+ memory_rss_mb: Math.round(mem.rss / 1024 / 1024 * 100) / 100,
288
+ cpu_usage_percent: 0,
289
+ process_uptime_secs: Math.floor(process.uptime()),
290
+ node_version: process.version,
291
+ region: process.env.VERCEL_REGION ?? process.env.AWS_REGION ?? process.env.FLY_REGION ?? process.env.RAILWAY_REGION ?? "unknown"
292
+ };
293
+ }
294
+ sendDeploymentMarker(info) {
295
+ if (this.deploymentSent && !info) return;
296
+ this.deploymentSent = true;
297
+ const payload = {
298
+ version: SDK_VERSION,
299
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
300
+ deployment: {
301
+ version: info?.version ?? process.env.VERCEL_GIT_COMMIT_SHA ?? process.env.RAILWAY_GIT_COMMIT_SHA ?? process.env.FLY_APP_VERSION ?? process.env.RENDER_GIT_COMMIT ?? "unknown",
302
+ environment: info?.environment ?? this.environment,
303
+ branch: info?.branch ?? process.env.VERCEL_GIT_COMMIT_REF ?? process.env.RAILWAY_GIT_BRANCH ?? process.env.RENDER_GIT_BRANCH ?? "unknown",
304
+ commit_message: info?.commit_message ?? process.env.VERCEL_GIT_COMMIT_MESSAGE ?? "",
305
+ framework: "next.js",
306
+ framework_version: this.detectFrameworkVersion(),
307
+ node_version: process.version,
308
+ region: process.env.VERCEL_REGION ?? "unknown",
309
+ deployment_url: process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : "",
310
+ metadata: info?.metadata ?? {}
311
+ }
312
+ };
313
+ this.sendPayload(payload).catch(() => {
314
+ });
315
+ }
316
+ async sendPayload(payload) {
317
+ try {
318
+ this.log(`Sending payload (${JSON.stringify(payload).length} bytes)`);
319
+ const response = await fetch(`${this.baseUrl}/api/sdk/ingest`, {
320
+ method: "POST",
321
+ headers: {
322
+ "Content-Type": "application/json",
323
+ Authorization: `Bearer ${this.apiKey}`,
324
+ "User-Agent": `@rely-net/sdk/${SDK_VERSION}`
325
+ },
326
+ body: JSON.stringify(payload),
327
+ signal: AbortSignal.timeout(INGEST_TIMEOUT_MS)
328
+ });
329
+ if (this.debug) {
330
+ if (response.ok) {
331
+ const body = await response.json();
332
+ this.log(`Flush successful`);
333
+ if (body.warnings?.length > 0) {
334
+ body.warnings.forEach((w) => this.log(`Warning: ${w}`));
335
+ }
336
+ } else {
337
+ this.log(`Ingest returned HTTP ${response.status}`);
338
+ }
339
+ }
340
+ } catch (err) {
341
+ this.log(
342
+ `Send failed: ${err instanceof Error ? err.message : String(err)}`
343
+ );
344
+ }
345
+ }
346
+ sanitize(str) {
347
+ let result = str;
348
+ for (const pattern of SECRET_PATTERNS) {
349
+ pattern.lastIndex = 0;
350
+ result = result.replace(pattern, "[REDACTED]");
351
+ }
352
+ return result;
353
+ }
354
+ sanitizeTags(tags) {
355
+ const sanitized = {};
356
+ for (const [key, value] of Object.entries(tags)) {
357
+ sanitized[key] = this.sanitize(String(value).slice(0, 200));
358
+ }
359
+ return sanitized;
360
+ }
361
+ detectFrameworkVersion() {
362
+ try {
363
+ const pkg = __require("next/package.json");
364
+ return pkg.version;
365
+ } catch {
366
+ return "unknown";
367
+ }
368
+ }
369
+ log(message) {
370
+ if (this.debug) {
371
+ console.log(`[Rely] ${message}`);
372
+ }
373
+ }
374
+ };
375
+
376
+ // src/middleware.ts
377
+ function withRelyMiddleware(rely, middleware) {
378
+ return async (request, event) => {
379
+ const start = Date.now();
380
+ const url = new URL(request.url);
381
+ let response;
382
+ if (middleware) {
383
+ response = await middleware(request, event);
384
+ } else {
385
+ const { NextResponse } = await import("next/server");
386
+ response = NextResponse.next();
387
+ }
388
+ const duration = Date.now() - start;
389
+ const status = response?.status ?? 200;
390
+ const path = url.pathname;
391
+ if (!path.startsWith("/_next/") && !path.startsWith("/favicon") && !path.startsWith("/robots") && !path.startsWith("/sitemap")) {
392
+ rely.recordRequest(path, status, duration);
393
+ }
394
+ return response;
395
+ };
396
+ }
397
+
398
+ // src/types.ts
399
+ var SDK_VERSION2 = "1.0.0";
400
+
401
+ // src/index.ts
402
+ var _instance = null;
403
+ function createRelyClient(options) {
404
+ _instance = new RelyClient(options);
405
+ return _instance;
406
+ }
407
+ function getRelyClient() {
408
+ if (!_instance) {
409
+ throw new Error(
410
+ "[Rely] SDK not initialized.\nCall createRelyClient() in your instrumentation.ts file first.\nSee https://rely.net/docs/sdk for setup instructions."
411
+ );
412
+ }
413
+ return _instance;
414
+ }
415
+ export {
416
+ RelyClient as Rely,
417
+ RelyClient,
418
+ SDK_VERSION2 as SDK_VERSION,
419
+ createRelyClient,
420
+ getRelyClient,
421
+ withRelyMiddleware
422
+ };
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@rely-net/sdk",
3
+ "version": "1.0.0",
4
+ "description": "Official SDK for rely.net — monitor your app from the inside",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
21
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
22
+ "prepublishOnly": "npm run build"
23
+ },
24
+ "keywords": [
25
+ "monitoring",
26
+ "observability",
27
+ "health-checks",
28
+ "rely",
29
+ "uptime",
30
+ "apm",
31
+ "status-page",
32
+ "nextjs"
33
+ ],
34
+ "author": "Rely <hello@rely.net>",
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/rely-net/sdk"
39
+ },
40
+ "homepage": "https://rely.net/docs/sdk",
41
+ "bugs": {
42
+ "url": "https://github.com/rely-net/sdk/issues"
43
+ },
44
+ "peerDependencies": {
45
+ "next": ">=13.0.0"
46
+ },
47
+ "peerDependenciesMeta": {
48
+ "next": {
49
+ "optional": true
50
+ }
51
+ },
52
+ "devDependencies": {
53
+ "tsup": "^8.0.0",
54
+ "typescript": "^5.0.0",
55
+ "@types/node": "^20.0.0"
56
+ },
57
+ "engines": {
58
+ "node": ">=18.0.0"
59
+ }
60
+ }