promptmetrics-sample 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.
Files changed (55) hide show
  1. package/.env.example +12 -0
  2. package/README.md +235 -0
  3. package/dist/index.d.ts +7 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +29 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/tests/logs.test.d.ts +7 -0
  8. package/dist/tests/logs.test.d.ts.map +1 -0
  9. package/dist/tests/logs.test.js +163 -0
  10. package/dist/tests/logs.test.js.map +1 -0
  11. package/dist/tests/providers.test.d.ts +7 -0
  12. package/dist/tests/providers.test.d.ts.map +1 -0
  13. package/dist/tests/providers.test.js +99 -0
  14. package/dist/tests/providers.test.js.map +1 -0
  15. package/dist/tests/tags.test.d.ts +7 -0
  16. package/dist/tests/tags.test.d.ts.map +1 -0
  17. package/dist/tests/tags.test.js +220 -0
  18. package/dist/tests/tags.test.js.map +1 -0
  19. package/dist/tests/template-run-load.test.d.ts +11 -0
  20. package/dist/tests/template-run-load.test.d.ts.map +1 -0
  21. package/dist/tests/template-run-load.test.js +245 -0
  22. package/dist/tests/template-run-load.test.js.map +1 -0
  23. package/dist/tests/templates.test.d.ts +7 -0
  24. package/dist/tests/templates.test.d.ts.map +1 -0
  25. package/dist/tests/templates.test.js +152 -0
  26. package/dist/tests/templates.test.js.map +1 -0
  27. package/dist/tests/traceable.test.d.ts +7 -0
  28. package/dist/tests/traceable.test.d.ts.map +1 -0
  29. package/dist/tests/traceable.test.js +300 -0
  30. package/dist/tests/traceable.test.js.map +1 -0
  31. package/dist/tests/traces.test.d.ts +7 -0
  32. package/dist/tests/traces.test.d.ts.map +1 -0
  33. package/dist/tests/traces.test.js +264 -0
  34. package/dist/tests/traces.test.js.map +1 -0
  35. package/dist/tests/versions.test.d.ts +7 -0
  36. package/dist/tests/versions.test.d.ts.map +1 -0
  37. package/dist/tests/versions.test.js +145 -0
  38. package/dist/tests/versions.test.js.map +1 -0
  39. package/dist/utils/logger.d.ts +26 -0
  40. package/dist/utils/logger.d.ts.map +1 -0
  41. package/dist/utils/logger.js +79 -0
  42. package/dist/utils/logger.js.map +1 -0
  43. package/package.json +36 -0
  44. package/sample-project-plan.md +316 -0
  45. package/src/index.ts +36 -0
  46. package/src/tests/logs.test.ts +180 -0
  47. package/src/tests/providers.test.ts +99 -0
  48. package/src/tests/tags.test.ts +237 -0
  49. package/src/tests/template-run-load.test.ts +332 -0
  50. package/src/tests/templates.test.ts +154 -0
  51. package/src/tests/traceable.test.ts +290 -0
  52. package/src/tests/traces.test.ts +298 -0
  53. package/src/tests/versions.test.ts +155 -0
  54. package/src/utils/logger.ts +91 -0
  55. package/tsconfig.json +21 -0
@@ -0,0 +1,332 @@
1
+ #!/usr/bin/env ts-node
2
+ /**
3
+ * Template Run Load Test
4
+ * Configurable load testing for canary deployments
5
+ *
6
+ * Usage:
7
+ * npm run test:load -- --requests=1000 --concurrency=50
8
+ * npm run test:load -- --template=my-template --requests=500
9
+ */
10
+
11
+ import "dotenv/config";
12
+ import { PromptMetrics } from "@promptmetrics/sdk";
13
+ import { logger } from "../utils/logger";
14
+
15
+ // Configuration interface
16
+ interface LoadTestConfig {
17
+ templateIdentifier: string;
18
+ requestCount: number;
19
+ concurrency: number;
20
+ errorRate: number; // 0-1, percentage of requests that should fail
21
+ delayMs?: number; // Delay between batches
22
+ variables?: Record<string, any>;
23
+ }
24
+
25
+ // Parse command line arguments
26
+ function parseArgs(): Partial<LoadTestConfig> {
27
+ const args = process.argv.slice(2);
28
+ const config: Partial<LoadTestConfig> = {};
29
+
30
+ args.forEach((arg) => {
31
+ const [key, value] = arg.replace("--", "").split("=");
32
+ switch (key) {
33
+ case "template":
34
+ config.templateIdentifier = value;
35
+ break;
36
+ case "requests":
37
+ config.requestCount = parseInt(value);
38
+ break;
39
+ case "concurrency":
40
+ config.concurrency = parseInt(value);
41
+ break;
42
+ case "error-rate":
43
+ config.errorRate = parseFloat(value);
44
+ break;
45
+ case "delay":
46
+ config.delayMs = parseInt(value);
47
+ break;
48
+ }
49
+ });
50
+
51
+ return config;
52
+ }
53
+
54
+ // Default configuration
55
+ const defaultConfig: LoadTestConfig = {
56
+ templateIdentifier: process.env.TEST_TEMPLATE_NAME || "test-template",
57
+ requestCount: 100,
58
+ concurrency: 10,
59
+ errorRate: 0,
60
+ delayMs: 0,
61
+ variables: {},
62
+ };
63
+
64
+ // Merge with CLI args
65
+ const cliConfig = parseArgs();
66
+ const config: LoadTestConfig = { ...defaultConfig, ...cliConfig };
67
+
68
+ // Initialize SDK
69
+ const pm = new PromptMetrics({
70
+ apiKey: process.env.PROMPTMETRICS_API_KEY!,
71
+ });
72
+
73
+ // Metrics tracking
74
+ interface TestMetrics {
75
+ totalRequests: number;
76
+ successfulRequests: number;
77
+ failedRequests: number;
78
+ canaryRequests: number;
79
+ legacyRequests: number;
80
+ errors: Array<{ message: string; timestamp: Date }>;
81
+ responseTimes: number[];
82
+ startTime: Date;
83
+ endTime?: Date;
84
+ }
85
+
86
+ const metrics: TestMetrics = {
87
+ totalRequests: 0,
88
+ successfulRequests: 0,
89
+ failedRequests: 0,
90
+ canaryRequests: 0,
91
+ legacyRequests: 0,
92
+ errors: [],
93
+ responseTimes: [],
94
+ startTime: new Date(),
95
+ };
96
+
97
+ // Progress bar
98
+ function renderProgressBar(current: number, total: number): string {
99
+ const percentage = Math.floor((current / total) * 100);
100
+ const barLength = 20;
101
+ const filledLength = Math.floor((current / total) * barLength);
102
+ const bar = "█".repeat(filledLength) + "░".repeat(barLength - filledLength);
103
+ return `[${bar}] ${percentage}% (${current}/${total})`;
104
+ }
105
+
106
+ // Single request execution
107
+ async function executeRequest(
108
+ requestId: number
109
+ ): Promise<{ success: boolean; responseTime: number; isCanary?: boolean }> {
110
+ const startTime = Date.now();
111
+
112
+ try {
113
+ // Simulate error based on error rate
114
+ const shouldFail = Math.random() < config.errorRate;
115
+
116
+ if (shouldFail) {
117
+ throw new Error(`Simulated error for request ${requestId}`);
118
+ }
119
+
120
+ // Execute template run
121
+ const result = await pm.templates.run(config.templateIdentifier, {
122
+ variables: config.variables,
123
+ });
124
+
125
+ const responseTime = Date.now() - startTime;
126
+
127
+ // Try to detect if this was a canary request
128
+ // This would need to be exposed by the API or inferred from metadata
129
+ const isCanary = (result as any).metadata?.is_canary;
130
+
131
+ return {
132
+ success: true,
133
+ responseTime,
134
+ isCanary,
135
+ };
136
+ } catch (error: any) {
137
+ const responseTime = Date.now() - startTime;
138
+ metrics.errors.push({
139
+ message: error.message || "Unknown error",
140
+ timestamp: new Date(),
141
+ });
142
+ return {
143
+ success: false,
144
+ responseTime,
145
+ };
146
+ }
147
+ }
148
+
149
+ // Execute batch of requests concurrently
150
+ async function executeBatch(
151
+ batchSize: number,
152
+ startIndex: number
153
+ ): Promise<void> {
154
+ const promises = [];
155
+
156
+ for (let i = 0; i < batchSize; i++) {
157
+ const requestId = startIndex + i;
158
+ promises.push(executeRequest(requestId));
159
+ }
160
+
161
+ const results = await Promise.all(promises);
162
+
163
+ // Update metrics
164
+ results.forEach((result) => {
165
+ metrics.totalRequests++;
166
+ metrics.responseTimes.push(result.responseTime);
167
+
168
+ if (result.success) {
169
+ metrics.successfulRequests++;
170
+ if (result.isCanary === true) {
171
+ metrics.canaryRequests++;
172
+ } else if (result.isCanary === false) {
173
+ metrics.legacyRequests++;
174
+ }
175
+ } else {
176
+ metrics.failedRequests++;
177
+ }
178
+ });
179
+ }
180
+
181
+ // Main load test execution
182
+ async function runLoadTest(): Promise<void> {
183
+ console.log("\n" + "=".repeat(60));
184
+ console.log("🚀 Template Run Load Test");
185
+ console.log("=".repeat(60));
186
+ console.log(`Template: ${config.templateIdentifier}`);
187
+ console.log(`Requests: ${config.requestCount}`);
188
+ console.log(`Concurrency: ${config.concurrency}`);
189
+ console.log(`Error Rate: ${(config.errorRate * 100).toFixed(1)}%`);
190
+ console.log(`Delay: ${config.delayMs}ms`);
191
+ console.log("=".repeat(60) + "\n");
192
+
193
+ const batches = Math.ceil(config.requestCount / config.concurrency);
194
+
195
+ for (let i = 0; i < batches; i++) {
196
+ const startIndex = i * config.concurrency;
197
+ const remainingRequests = config.requestCount - startIndex;
198
+ const batchSize = Math.min(config.concurrency, remainingRequests);
199
+
200
+ // Execute batch
201
+ await executeBatch(batchSize, startIndex);
202
+
203
+ // Update progress
204
+ process.stdout.write(
205
+ `\rProgress: ${renderProgressBar(
206
+ metrics.totalRequests,
207
+ config.requestCount
208
+ )}`
209
+ );
210
+
211
+ // Delay between batches if configured
212
+ if (config.delayMs && i < batches - 1) {
213
+ await new Promise((resolve) => setTimeout(resolve, config.delayMs));
214
+ }
215
+ }
216
+
217
+ metrics.endTime = new Date();
218
+ console.log("\n");
219
+ }
220
+
221
+ // Calculate and display results
222
+ function displayResults(): void {
223
+ const duration =
224
+ (metrics.endTime!.getTime() - metrics.startTime.getTime()) / 1000;
225
+ const avgResponseTime =
226
+ metrics.responseTimes.reduce((a, b) => a + b, 0) /
227
+ metrics.responseTimes.length || 0;
228
+ const minResponseTime = Math.min(...metrics.responseTimes) || 0;
229
+ const maxResponseTime = Math.max(...metrics.responseTimes) || 0;
230
+ const requestsPerSecond = metrics.totalRequests / duration;
231
+
232
+ console.log("=".repeat(60));
233
+ console.log("📊 Test Results");
234
+ console.log("=".repeat(60));
235
+
236
+ // Request stats
237
+ console.log("\n📈 Request Statistics:");
238
+ console.log(` ✓ Total Requests: ${metrics.totalRequests}`);
239
+ console.log(
240
+ ` ✓ Successful: ${metrics.successfulRequests} (${(
241
+ (metrics.successfulRequests / metrics.totalRequests) *
242
+ 100
243
+ ).toFixed(1)}%)`
244
+ );
245
+ console.log(
246
+ ` ✗ Failed: ${metrics.failedRequests} (${(
247
+ (metrics.failedRequests / metrics.totalRequests) *
248
+ 100
249
+ ).toFixed(1)}%)`
250
+ );
251
+
252
+ // Canary distribution (if available)
253
+ if (metrics.canaryRequests > 0 || metrics.legacyRequests > 0) {
254
+ const totalVersioned = metrics.canaryRequests + metrics.legacyRequests;
255
+ console.log("\n🔀 Traffic Distribution:");
256
+ console.log(
257
+ ` 🐤 Canary Requests: ${metrics.canaryRequests} (${(
258
+ (metrics.canaryRequests / totalVersioned) *
259
+ 100
260
+ ).toFixed(1)}%)`
261
+ );
262
+ console.log(
263
+ ` 🏛️ Legacy Requests: ${metrics.legacyRequests} (${(
264
+ (metrics.legacyRequests / totalVersioned) *
265
+ 100
266
+ ).toFixed(1)}%)`
267
+ );
268
+ }
269
+
270
+ // Performance stats
271
+ console.log("\n⚡ Performance:");
272
+ console.log(` ⏱️ Duration: ${duration.toFixed(2)}s`);
273
+ console.log(` 📊 Requests/sec: ${requestsPerSecond.toFixed(2)}`);
274
+ console.log(` 📉 Avg Response Time: ${avgResponseTime.toFixed(0)}ms`);
275
+ console.log(` 📊 Min Response Time: ${minResponseTime.toFixed(0)}ms`);
276
+ console.log(` 📊 Max Response Time: ${maxResponseTime.toFixed(0)}ms`);
277
+
278
+ // Error details
279
+ if (metrics.errors.length > 0) {
280
+ console.log("\n❌ Errors:");
281
+ const errorCounts = new Map<string, number>();
282
+ metrics.errors.forEach((error) => {
283
+ const count = errorCounts.get(error.message) || 0;
284
+ errorCounts.set(error.message, count + 1);
285
+ });
286
+
287
+ errorCounts.forEach((count, message) => {
288
+ console.log(` • ${message}: ${count} occurrences`);
289
+ });
290
+
291
+ // Show last 3 errors with timestamps
292
+ console.log("\n Recent Errors:");
293
+ metrics.errors.slice(-3).forEach((error) => {
294
+ console.log(` ${error.timestamp.toISOString()}: ${error.message}`);
295
+ });
296
+ }
297
+
298
+ console.log("\n" + "=".repeat(60));
299
+
300
+ // Validation checks
301
+ console.log("\n✅ Validation:");
302
+
303
+ // Check counter accuracy (if we can query the backend)
304
+ console.log(` ℹ️ Note: Verify backend counters match these numbers`);
305
+ console.log(` Expected total_requests: ${metrics.totalRequests}`);
306
+ console.log(` Expected canary_requests: ${metrics.canaryRequests}`);
307
+ console.log(` Expected legacy_requests: ${metrics.legacyRequests}`);
308
+
309
+ console.log("\n" + "=".repeat(60) + "\n");
310
+ }
311
+
312
+ // Main execution
313
+ async function main() {
314
+ try {
315
+ await runLoadTest();
316
+ displayResults();
317
+
318
+ // Exit with appropriate code
319
+ if (metrics.failedRequests > 0 && config.errorRate === 0) {
320
+ console.log("⚠️ Test completed with unexpected errors");
321
+ process.exit(1);
322
+ } else {
323
+ console.log("✅ Test completed successfully");
324
+ process.exit(0);
325
+ }
326
+ } catch (error: any) {
327
+ console.error("❌ Load test failed:", error.message);
328
+ process.exit(1);
329
+ }
330
+ }
331
+
332
+ main();
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env ts-node
2
+ /**
3
+ * Templates Test Suite
4
+ * Tests all template-related SDK operations
5
+ */
6
+
7
+ import "dotenv/config";
8
+ import { PromptMetrics } from "@promptmetrics/sdk";
9
+ import { runTestSuite, logger } from "../utils/logger";
10
+
11
+ const pm = new PromptMetrics({
12
+ apiKey: process.env.PROMPTMETRICS_API_KEY!,
13
+ });
14
+
15
+ const TEST_TEMPLATE_NAME = process.env.TEST_TEMPLATE_NAME || "test-template";
16
+ const TEST_TEMPLATE_ID = process.env.TEST_TEMPLATE_ID;
17
+
18
+ const tests = [
19
+ {
20
+ name: "Get template by name",
21
+ fn: async () => {
22
+ const template = await pm.templates.get(TEST_TEMPLATE_NAME);
23
+ if (!template) throw new Error("Template not found");
24
+ logger.data("Template ID", template._id);
25
+ logger.data("Template Name", template.name);
26
+ logger.data("Versions Count", template.versions?.length || 0);
27
+ },
28
+ },
29
+
30
+ {
31
+ name: "Get template by ID",
32
+ fn: async () => {
33
+ if (!TEST_TEMPLATE_ID) {
34
+ logger.info("Skipped - TEST_TEMPLATE_ID not set");
35
+ return;
36
+ }
37
+ const template = await pm.templates.get(TEST_TEMPLATE_ID);
38
+ if (!template) throw new Error("Template not found");
39
+ logger.data("Template ID", template._id);
40
+ logger.data("Template Name", template.name);
41
+ },
42
+ },
43
+
44
+ {
45
+ name: "Get template with specific version number",
46
+ fn: async () => {
47
+ const template = await pm.templates.get(TEST_TEMPLATE_NAME, {
48
+ version: 1,
49
+ });
50
+ if (!template) throw new Error("Template not found");
51
+ logger.data("Template Name", template.name);
52
+ logger.data("Version", template.versions?.[0]?.version || "N/A");
53
+ },
54
+ },
55
+
56
+ {
57
+ name: "Get template with env_label (production)",
58
+ fn: async () => {
59
+ try {
60
+ const template = await pm.templates.get(TEST_TEMPLATE_NAME, {
61
+ env_label: "production",
62
+ });
63
+ if (!template) throw new Error("Template not found");
64
+ logger.data("Template Name", template.name);
65
+ logger.data("Env Label", template.versions?.[0]?.env_label || "N/A");
66
+ } catch (error: unknown) {
67
+ const err = error as { message?: string };
68
+ if (err.message?.includes("not found")) {
69
+ logger.info("No production version found - this is OK");
70
+ } else {
71
+ throw error;
72
+ }
73
+ }
74
+ },
75
+ },
76
+
77
+ {
78
+ name: "List templates with pagination",
79
+ fn: async () => {
80
+ const result = await pm.templates.list({
81
+ page: 1,
82
+ per_page: 10,
83
+ });
84
+ if (!result) throw new Error("Failed to list templates");
85
+ logger.data("Templates Count", result.prompts?.length || 0);
86
+ logger.data("Total", result.total || 0);
87
+ },
88
+ },
89
+
90
+ {
91
+ name: "List templates with search filter",
92
+ fn: async () => {
93
+ const result = await pm.templates.list({
94
+ search: TEST_TEMPLATE_NAME.substring(0, 4), // Search with partial name
95
+ page: 1,
96
+ per_page: 10,
97
+ });
98
+ if (!result) throw new Error("Failed to list templates");
99
+ logger.data("Search Results", result.prompts?.length || 0);
100
+ },
101
+ },
102
+
103
+ {
104
+ name: "Run template (canary-aware)",
105
+ fn: async () => {
106
+ const result = await pm.templates.run(TEST_TEMPLATE_NAME, {
107
+ variables: {
108
+ topic: "SDK Template Run Test",
109
+ },
110
+ });
111
+
112
+ if (!result) throw new Error("Run failed");
113
+ logger.data("Request ID", result.request_id);
114
+ logger.data("Status", result.status);
115
+ logger.data("Latency", `${result.latency}s`);
116
+ logger.data("Total Tokens", result.llm_total_tokens);
117
+ },
118
+ },
119
+
120
+ {
121
+ name: "Run template with pm_tags",
122
+ fn: async () => {
123
+ const result = await pm.templates.run(TEST_TEMPLATE_NAME, {
124
+ variables: {
125
+ topic: "Template Tags Test",
126
+ },
127
+ pm_tags: ["template-run-test", "sdk-sample"],
128
+ });
129
+
130
+ if (!result) throw new Error("Run failed");
131
+ logger.data("Request ID", result.request_id);
132
+ logger.data("Status", result.status);
133
+ logger.data("Tags", result.tags || "Not returned");
134
+ },
135
+ },
136
+
137
+ {
138
+ name: "Run template with specific version",
139
+ fn: async () => {
140
+ const result = await pm.templates.run(TEST_TEMPLATE_NAME, {
141
+ version: 1,
142
+ variables: {
143
+ topic: "Specific Version Test",
144
+ },
145
+ });
146
+
147
+ if (!result) throw new Error("Run failed");
148
+ logger.data("Request ID", result.request_id);
149
+ logger.data("Status", result.status);
150
+ },
151
+ },
152
+ ];
153
+
154
+ runTestSuite("Templates Test Suite", tests);