Skip to content

Commit af64692

Browse files
committed
feat: Adds development tools to simplify the console.log story for non-cloud implementations
1 parent 92d8e00 commit af64692

File tree

11 files changed

+218
-163
lines changed

11 files changed

+218
-163
lines changed

.changeset/clever-monkeys-dream.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@taskless/loader": patch
3+
---
4+
5+
Creates the packcheck utility for validating Lua scripting packs. This makes it possible to check your own Taskless packs without having to upload them to the Taskless server. The packcheck commands takes a pack or config (in yaml), and stands up a mock service worker to emulate the full Taskless lifecycle. It then returns the response as a consolidated JSON object you can assert on.

examples/programatic.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ rules:
3737
taskless.capture("status", response.getStatus())
3838
`;
3939

40-
const t = await taskless(process.env.TASKLESS_API_KEY);
40+
const t = taskless(process.env.TASKLESS_API_KEY);
4141
t.add(pack);
4242
await t.load();
4343

package.json

+6
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"codegen": "tsx scripts/generate.ts",
1212
"husky": "husky",
1313
"lint-staged": "lint-staged",
14+
"packcheck": "tsx scripts/packcheck.ts",
1415
"prepare": "husky",
1516
"prettier": "prettier",
1617
"syncpack": "syncpack",
@@ -29,6 +30,11 @@
2930
"types": "./dist/core.d.ts",
3031
"import": "./dist/core.js",
3132
"require": "./dist/core.cjs"
33+
},
34+
"./dev/packcheck": {
35+
"types": "./dist/dev/packcheck.d.ts",
36+
"import": "./dist/dev/packcheck.js",
37+
"require": "./dist/dev/packcheck.cjs"
3238
}
3339
},
3440
"files": [

src/dev/packcheck.ts

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/* eslint-disable n/no-process-env */
2+
import process from "node:process";
3+
import { type ConsolePayload, taskless } from "@~/core.js";
4+
import { http } from "msw";
5+
import { setupServer } from "msw/node";
6+
7+
type Fixture = {
8+
request: Request;
9+
response: Response;
10+
};
11+
12+
/**
13+
* Packcheck - simple tool for checking your Taskless packs, easily integrated
14+
* with your existing unit testing framework
15+
*/
16+
export async function packcheck(configOrPack: string, fixture: Fixture) {
17+
if (process.env.NODE_ENV === "production") {
18+
throw new Error(
19+
"Taskless `dev` actions cannot be used in production node environments"
20+
);
21+
}
22+
23+
const msw = setupServer(
24+
http.all("*", async (info) => {
25+
fixture.response.clone();
26+
})
27+
);
28+
msw.listen();
29+
30+
const logs: ConsolePayload[] = [];
31+
32+
const t = taskless(undefined, {
33+
network: false,
34+
logging: true,
35+
flushInterval: 0,
36+
logLevel: "error",
37+
__experimental: {
38+
msw,
39+
},
40+
log: {
41+
data(message) {
42+
logs.push(JSON.parse(message) as ConsolePayload);
43+
},
44+
},
45+
});
46+
47+
t.add(configOrPack);
48+
await t.load();
49+
50+
await fetch(fixture.request);
51+
await t.flush();
52+
53+
msw.close();
54+
55+
// merge any logs that share a request id into a single output
56+
const grouped = new Map<string, ConsolePayload>();
57+
58+
for (const log of logs) {
59+
if (grouped.has(log.requestId)) {
60+
const existingLog = grouped.get(log.requestId)!;
61+
existingLog.sequenceIds.push(...log.sequenceIds);
62+
existingLog.dimensions.push(...log.dimensions);
63+
grouped.set(log.requestId, existingLog);
64+
} else {
65+
grouped.set(log.requestId, { ...log });
66+
}
67+
}
68+
69+
const result = Array.from(grouped.values())[0] ?? {};
70+
return result;
71+
}

src/lib/taskless.ts

+40-13
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ import {
1111
type Pack,
1212
type InitOptions,
1313
type NetworkPayload,
14-
type ConsolePayload,
1514
type CaptureCallback,
1615
type CaptureItem,
1716
type TasklessAPI,
1817
type Logger,
1918
type Config,
19+
isConfig,
20+
isPack,
21+
type ConsolePayload,
2022
} from "@~/types.js";
2123
import { createClient, type NormalizeOAS } from "fets";
2224
import yaml from "js-yaml";
@@ -193,15 +195,28 @@ export const taskless = (
193195

194196
/** Sends a set of entries to the registered logging function */
195197
const logEntries = (entries: CaptureItem[]) => {
198+
// group all entries by their request id
199+
const grouped = new Map<string, ConsolePayload>();
200+
201+
// convert entries to the grouped structure
196202
for (const entry of entries) {
197-
logger.data(
198-
JSON.stringify({
199-
req: entry.requestId,
200-
seq: entry.sequenceId,
201-
dim: entry.dimension,
202-
val: entry.value,
203-
} satisfies ConsolePayload)
204-
);
203+
const group = grouped.get(entry.requestId) ?? {
204+
requestId: entry.requestId,
205+
sequenceIds: [],
206+
dimensions: [],
207+
};
208+
209+
group.sequenceIds.push(entry.sequenceId);
210+
group.dimensions.push({
211+
name: entry.dimension,
212+
value: entry.value,
213+
});
214+
215+
grouped.set(entry.requestId, group);
216+
}
217+
218+
for (const [_id, line] of grouped.entries()) {
219+
logger.data(JSON.stringify(line));
205220
}
206221
};
207222

@@ -425,14 +440,26 @@ export const taskless = (
425440
}
426441

427442
const api = {
428-
/** add additional local packs programatically */
429-
add(pack: string) {
443+
/** add additional local packs programatically, or an entire configuration */
444+
add(packOrConfig: string) {
430445
if (initialized) {
431446
throw new Error("A pack was added after Taskless was initialized");
432447
}
433448

434-
const data = typeof pack === "string" ? (yaml.load(pack) as Pack) : pack;
435-
packs.push(data);
449+
const data =
450+
typeof packOrConfig === "string"
451+
? yaml.load(packOrConfig)
452+
: packOrConfig;
453+
454+
const loadedConfig: Config = isConfig(data)
455+
? data
456+
: {
457+
schema: 1,
458+
organizationId: "none",
459+
packs: [...(isPack(data) ? [data] : [])],
460+
};
461+
462+
packs.push(...loadedConfig.packs);
436463
},
437464

438465
/** get the current logger */

src/types.ts

+45-8
Original file line numberDiff line numberDiff line change
@@ -86,21 +86,58 @@ export type HookName = keyof NonNullable<Pack["hooks"]>;
8686
/** Pack sends collection */
8787
export type Permissions = Pack["permissions"];
8888

89+
export function isConfig(value?: unknown): value is Config {
90+
if (!value || typeof value !== "object" || value === null) {
91+
return false;
92+
}
93+
94+
const check = value as Config;
95+
96+
if (
97+
check.organizationId !== undefined &&
98+
Array.isArray(check.packs) &&
99+
check.schema !== undefined
100+
) {
101+
return true;
102+
}
103+
104+
return false;
105+
}
106+
107+
export function isPack(value?: unknown): value is Pack {
108+
if (!value || typeof value !== "object" || value === null) {
109+
return false;
110+
}
111+
112+
const check = value as Pack;
113+
114+
if (
115+
check.name !== undefined &&
116+
check.schema !== undefined &&
117+
check.version !== undefined
118+
) {
119+
return true;
120+
}
121+
122+
return false;
123+
}
124+
89125
/** Network payload intended for Taskless */
90126
export type NetworkPayload = NonNullable<
91127
OASInput<NormalizeOAS<typeof openapi>, "/{version}/events", "post", "json">
92128
>;
93129

94130
/** Console payload intended for stdout */
95131
export type ConsolePayload = {
96-
/** The request ID */
97-
req: string;
98-
/** The sequenceID */
99-
seq: string;
100-
/** The dimension name */
101-
dim: string;
102-
/** The dimension's value */
103-
val: string;
132+
/** The request ID. Can be used on the backend to merge related logs from a request */
133+
requestId: string;
134+
/** The sequenceIDs connected to this log entry */
135+
sequenceIds: string[];
136+
/** The dimension name & value that are recorded */
137+
dimensions: Array<{
138+
name: string;
139+
value: string;
140+
}>;
104141
};
105142

106143
/** The object generated during a telemetry capture */

test/env.spec.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import { dirname, resolve } from "node:path";
22
import { fileURLToPath } from "node:url";
33
import { execa } from "execa";
44
import { vi, test as vitest, afterEach, describe, beforeAll } from "vitest";
5+
import { packcheck } from "../dist/dev/packcheck.js";
56
import { taskless } from "../src/core.js";
7+
import sampleYaml from "./fixtures/sample.yaml?raw";
68
import { defaultConfig, withHono } from "./helpers/server.js";
79

810
const test = withHono(vitest);

test/helpers/find.ts

-114
This file was deleted.

0 commit comments

Comments
 (0)