Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/core/ProtobufCommunicator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,8 +384,8 @@ export class ProtobufCommunicator {
submoduleName: span.submoduleName || "",
inputValue: toStruct(span.inputValue),
outputValue: toStruct(span.outputValue),
inputSchema: toStruct(span.inputSchema),
outputSchema: toStruct(span.outputSchema),
inputSchema: span.inputSchema,
outputSchema: span.outputSchema,
inputSchemaHash: span.inputSchemaHash || "",
outputSchemaHash: span.outputSchemaHash || "",
inputValueHash: span.inputValueHash || "",
Expand Down
244 changes: 127 additions & 117 deletions src/core/tracing/JsonSchemaHelper.test.ts

Large diffs are not rendered by default.

145 changes: 54 additions & 91 deletions src/core/tracing/JsonSchemaHelper.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
import * as crypto from "crypto";
import { logger } from "../utils/logger";

export enum JsonSchemaType {
NUMBER = "NUMBER",
STRING = "STRING",
BOOLEAN = "BOOLEAN",
NULL = "NULL",
UNDEFINED = "UNDEFINED",
OBJECT = "OBJECT",
ORDERED_LIST = "ORDERED_LIST",
UNORDERED_LIST = "UNORDERED_LIST",
FUNCTION = "FUNCTION",
}
import {
JsonSchemaType,
EncodingType,
DecodedType,
type JsonSchema,
} from "@use-tusk/drift-schemas/core/json_schema";

Check failure on line 8 in src/core/tracing/JsonSchemaHelper.ts

View workflow job for this annotation

GitHub Actions / Lint and Type Check

Cannot find module '@use-tusk/drift-schemas/core/json_schema' or its corresponding type declarations.

// Standardized schema type mapping
const jsToJsonSchemaTypeMapping = {
Expand Down Expand Up @@ -47,45 +41,8 @@
["Arguments"]: JsonSchemaType.ORDERED_LIST,
} as const;

export enum EncodingType {
BASE64 = "BASE64",
}

export enum DecodedType {
JSON = "JSON",
HTML = "HTML",
CSS = "CSS",
JAVASCRIPT = "JAVASCRIPT",
XML = "XML",
YAML = "YAML",
MARKDOWN = "MARKDOWN",
CSV = "CSV",
SQL = "SQL",
GRAPHQL = "GRAPHQL",
PLAIN_TEXT = "PLAIN_TEXT",
FORM_DATA = "FORM_DATA",
MULTIPART_FORM = "MULTIPART_FORM",
PDF = "PDF",
AUDIO = "AUDIO",
VIDEO = "VIDEO",
GZIP = "GZIP",
BINARY = "BINARY",
JPEG = "JPEG",
PNG = "PNG",
GIF = "GIF",
WEBP = "WEBP",
SVG = "SVG",
ZIP = "ZIP",
}

export interface JsonSchema {
type: JsonSchemaType;
properties?: Record<string, JsonSchema>;
items?: JsonSchema | null;
encoding?: EncodingType;
decodedType?: DecodedType;
matchImportance?: number; // Should be between 0 and 1, 0 being the lowest importance and 1 being the highest importance
}
// Re-export proto types for convenience
export { JsonSchemaType, EncodingType, DecodedType, type JsonSchema };

// The following types can be merged with the generated schema to provide additional information
// This is set in the instrumentation layer and merged with the generated schema
Expand Down Expand Up @@ -212,39 +169,49 @@

/**
* Generate schema from data object using standardized types
*
* Note: We properties always exists on JsonSchema because proto3 maps cannot be marked optional.
* The JSON data is a bit inefficient because of this, but the easiest way to handle this is to keep it for now.
*/
static generateSchema(data: any, schemaMerges?: SchemaMerges): JsonSchema {
if (data === null) {
return { type: jsToJsonSchemaTypeMapping["null"] };
return { type: jsToJsonSchemaTypeMapping["null"], properties: {} };
}

if (data === undefined) {
return { type: jsToJsonSchemaTypeMapping["undefined"] };
return { type: jsToJsonSchemaTypeMapping["undefined"], properties: {} };
}

const detailedType = JsonSchemaHelper.getDetailedType(data);

if (detailedType === JsonSchemaType.ORDERED_LIST) {
if (Array.isArray(data) && data.length === 0) {
return { type: JsonSchemaType.ORDERED_LIST, items: null };
return { type: JsonSchemaType.ORDERED_LIST, properties: {} };
}
const items = Array.isArray(data) && data.length > 0 ? JsonSchemaHelper.generateSchema(data[0]) : undefined;
if (items !== undefined) {
return {
type: JsonSchemaType.ORDERED_LIST,
items,
properties: {},
};
}
return {
type: JsonSchemaType.ORDERED_LIST,
items:
Array.isArray(data) && data.length > 0 ? JsonSchemaHelper.generateSchema(data[0]) : null,
};
return { type: JsonSchemaType.ORDERED_LIST, properties: {} };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Schema Generation Fails for Arguments Objects

The generateSchema method incorrectly omits the items field for Arguments objects when generating ORDERED_LIST schemas. The Array.isArray(data) check prevents Arguments objects from having their first item analyzed or the items: null fallback applied, changing the schema structure and potentially losing information.

Fix in Cursor Fix in Web

}

if (detailedType === JsonSchemaType.UNORDERED_LIST) {
// Handle Set objects
if (data instanceof Set) {
const firstItem = data.size > 0 ? data.values().next().value : null;
return {
type: JsonSchemaType.UNORDERED_LIST,
items: firstItem !== null ? JsonSchemaHelper.generateSchema(firstItem) : null,
};
if (firstItem !== null) {
return {
type: JsonSchemaType.UNORDERED_LIST,
items: JsonSchemaHelper.generateSchema(firstItem),
properties: {},
};
}
}
return { type: JsonSchemaType.UNORDERED_LIST, items: null };
return { type: JsonSchemaType.UNORDERED_LIST, properties: {} };
}

if (detailedType === JsonSchemaType.OBJECT) {
Expand All @@ -253,44 +220,40 @@
// Handle Map objects
if (data instanceof Map) {
data.forEach((value, key) => {
if (schema.properties) {
const keyString = String(key);
const generatedSchema = JsonSchemaHelper.generateSchema(value);

// Check for schema override for this key
if (schemaMerges && schemaMerges[keyString]) {
schema.properties[keyString] = JsonSchemaHelper.mergeSchemaWithMerges(
generatedSchema,
schemaMerges[keyString],
);
} else {
schema.properties[keyString] = generatedSchema;
}
const keyString = String(key);
const generatedSchema = JsonSchemaHelper.generateSchema(value);

// Check for schema override for this key
if (schemaMerges && schemaMerges[keyString]) {
schema.properties[keyString] = JsonSchemaHelper.mergeSchemaWithMerges(
generatedSchema,
schemaMerges[keyString],
);
} else {
schema.properties[keyString] = generatedSchema;
}
});
} else if (typeof data === "object") {
Object.keys(data).forEach((key) => {
if (schema.properties) {
const generatedSchema = JsonSchemaHelper.generateSchema(data[key]);

// Check for schema override for this key
if (schemaMerges && schemaMerges[key]) {
schema.properties[key] = JsonSchemaHelper.mergeSchemaWithMerges(
generatedSchema,
schemaMerges[key],
);
} else {
schema.properties[key] = generatedSchema;
}
const generatedSchema = JsonSchemaHelper.generateSchema(data[key]);

// Check for schema override for this key
if (schemaMerges && schemaMerges[key]) {
schema.properties[key] = JsonSchemaHelper.mergeSchemaWithMerges(
generatedSchema,
schemaMerges[key],
);
} else {
schema.properties[key] = generatedSchema;
}
});
}

return schema;
}

// For primitive types, return the standardized type
return { type: detailedType };
// For primitive types, return just the type
return { type: detailedType, properties: {} };
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/core/tracing/SpanTransformer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ReadableSpan } from "@opentelemetry/sdk-trace-base";
import { SpanKind as OtSpanKind } from "@opentelemetry/api";
import { JsonSchemaHelper, JsonSchema, JsonSchemaType } from "./JsonSchemaHelper";
import { JsonSchemaHelper, JsonSchemaType, JsonSchema } from "./JsonSchemaHelper";
import { CleanSpanData, MetadataObject, TdSpanAttributes } from "../types";
import { PackageType, StatusCode } from "@use-tusk/drift-schemas/core/span";
import { logger, OriginalGlobalUtils } from "../utils";
Expand Down
4 changes: 2 additions & 2 deletions src/core/tracing/adapters/ApiSpanAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ export class ApiSpanAdapter implements SpanExportAdapter {
packageType: cleanSpan.packageType || PackageType.UNSPECIFIED,
inputValue: toStruct(cleanSpan.inputValue),
outputValue: toStruct(cleanSpan.outputValue),
inputSchema: toStruct(cleanSpan.inputSchema),
outputSchema: toStruct(cleanSpan.outputSchema),
inputSchema: cleanSpan.inputSchema,
outputSchema: cleanSpan.outputSchema,
inputSchemaHash: cleanSpan.inputSchemaHash || "",
outputSchemaHash: cleanSpan.outputSchemaHash || "",
inputValueHash: cleanSpan.inputValueHash || "",
Expand Down
8 changes: 5 additions & 3 deletions src/core/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createContextKey } from "@opentelemetry/api";
import { StatusCode, PackageType } from "@use-tusk/drift-schemas/core/span";
import { SpanKind } from "@opentelemetry/api";
import type { JsonSchema } from "@use-tusk/drift-schemas/core/json_schema";

Check failure on line 4 in src/core/types.ts

View workflow job for this annotation

GitHub Actions / Lint and Type Check

Cannot find module '@use-tusk/drift-schemas/core/json_schema' or its corresponding type declarations.

export const REPLAY_TRACE_ID_CONTEXT_KEY = createContextKey("td.replayTraceId");
export const SPAN_KIND_CONTEXT_KEY = createContextKey("td.spanKind");
Expand Down Expand Up @@ -60,11 +61,12 @@

packageType?: PackageType;

// keep these as plain JSON for readability
// Values are kept as plain JSON for readability
inputValue: unknown;
outputValue: unknown;
inputSchema: unknown;
outputSchema: unknown;
// Schemas use the proto JsonSchema type
inputSchema: JsonSchema;
outputSchema?: JsonSchema; // optional bc replay outbound spans don't have output schemas

inputSchemaHash: string;
outputSchemaHash: string;
Expand Down
16 changes: 10 additions & 6 deletions src/instrumentation/libraries/http/HttpTransformEngine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
spanId: "login_456",
kind: SpanKind.SERVER,
protocol: "http",
inputValue: {

Check failure on line 73 in src/instrumentation/libraries/http/HttpTransformEngine.test.ts

View workflow job for this annotation

GitHub Actions / Lint and Type Check

Conversion of type '{ method: string; url: string; target: string; headers: { "content-type": string; "user-agent": string; }; httpVersion: string; body: { username: string; password: string; }; bodySize: number; }' to type 'HttpServerInputValue' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
method: "POST",
url: "http://localhost:3000/api/auth/login",
target: "/api/auth/login",
Expand Down Expand Up @@ -131,6 +131,11 @@
headers: {
"content-type": "application/json",
},
httpVersion: "1.1",
httpVersionMajor: 1,
httpVersionMinor: 1,
complete: true,
readable: false,
body: Buffer.from(JSON.stringify({
id: "ch_123456",
amount: 1000,
Expand All @@ -145,7 +150,7 @@
spanId: "multi_999",
kind: SpanKind.SERVER,
protocol: "http",
inputValue: {

Check failure on line 153 in src/instrumentation/libraries/http/HttpTransformEngine.test.ts

View workflow job for this annotation

GitHub Actions / Lint and Type Check

Conversion of type '{ method: string; url: string; target: string; headers: { Authorization: string; "X-API-Key": string; }; httpVersion: string; body: { user: { password: string; email: string; }; }; bodySize: number; }' to type 'HttpServerInputValue' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
method: "GET",
url: "http://localhost:3000/api/user/lookup?ssn=123-45-6789&email=user@example.com",
target: "/api/user/lookup?ssn=123-45-6789&email=user@example.com",
Expand Down Expand Up @@ -201,7 +206,7 @@
t.truthy(transformed);
const span = transformed as HttpSpanData;

t.regex((span.inputValue as HttpServerInputValue).body.password, /^PWD_[0-9a-f]{12}\.\.\.$/);

Check failure on line 209 in src/instrumentation/libraries/http/HttpTransformEngine.test.ts

View workflow job for this annotation

GitHub Actions / Lint and Type Check

Property 'password' does not exist on type 'string'.
t.is(span.transformMetadata?.actions.length, 1);
t.is(span.transformMetadata?.actions[0].type, "redact");
t.is(span.transformMetadata?.actions[0].field, "jsonPath:$.password");
Expand Down Expand Up @@ -244,7 +249,7 @@
const inputValue = span.inputValue as HttpServerInputValue;
t.is(inputValue.url, "http://localhost:3000/api/user/lookup?ssn=XXXXXXXXXXX&email=user%40example.com");
t.is(inputValue.target, "/api/user/lookup?ssn=XXXXXXXXXXX&email=user%40example.com");
t.is(inputValue.body.user.password, "HIDDEN");

Check failure on line 252 in src/instrumentation/libraries/http/HttpTransformEngine.test.ts

View workflow job for this annotation

GitHub Actions / Lint and Type Check

Property 'user' does not exist on type 'string'.
t.is(span.transformMetadata?.actions.length, 2);
const actionTypes = span.transformMetadata!.actions.map((a) => ({ type: a.type, field: a.field }));
t.true(actionTypes.some((a) => a.type === "mask" && a.field === "queryParam:ssn"));
Expand All @@ -269,7 +274,7 @@
const engine = new HttpTransformEngine(inboundDropConfig);

t.true(
engine.shouldDropInboundRequest("POST", "http://localhost:3000/api/auth/login", "localhost", {
engine.shouldDropInboundRequest("POST", "http://localhost:3000/api/auth/login", {
"content-type": "application/json",
}),
);
Expand All @@ -278,13 +283,12 @@
engine.shouldDropInboundRequest(
"POST",
"http://localhost:3000/api/other/endpoint",
"localhost",
{ "content-type": "application/json" },
),
);

t.false(
engine.shouldDropInboundRequest("GET", "http://localhost:3000/api/auth/login", "localhost", {
engine.shouldDropInboundRequest("GET", "http://localhost:3000/api/auth/login", {
"content-type": "application/json",
}),
);
Expand Down Expand Up @@ -406,7 +410,7 @@
const spanWithoutBody = cloneSpan(exampleServerSpanData);
spanWithoutBody.inputValue = {
...(spanWithoutBody.inputValue as HttpServerInputValue),
body: undefined,

Check failure on line 413 in src/instrumentation/libraries/http/HttpTransformEngine.test.ts

View workflow job for this annotation

GitHub Actions / Lint and Type Check

Type 'undefined' is not assignable to type 'string'.
};

const result = engine.applyTransforms(spanWithoutBody as any);
Expand All @@ -430,7 +434,7 @@
const spanWithoutHeaders = cloneSpan(exampleServerSpanData);
spanWithoutHeaders.inputValue = {
...(spanWithoutHeaders.inputValue as HttpServerInputValue),
headers: undefined,
headers: {} as any,
};

const result = engine.applyTransforms(spanWithoutHeaders as any);
Expand All @@ -454,7 +458,7 @@
const engine = new HttpTransformEngine(maskConfig);
const result = engine.applyTransforms(cloneSpan(exampleServerSpanData));

t.is((result.inputValue as HttpServerInputValue).body.password, "#################");

Check failure on line 461 in src/instrumentation/libraries/http/HttpTransformEngine.test.ts

View workflow job for this annotation

GitHub Actions / Lint and Type Check

Property 'password' does not exist on type 'string'.
});

test("applies redact transform with custom hash prefix", (t) => {
Expand Down Expand Up @@ -674,7 +678,7 @@

const inputValue = result.inputValue as HttpServerInputValue;
t.regex(inputValue.url, /http:\/\/localhost:3000\/api\/test\?token=REDACTED_[0-9a-f]{12}\.\.\.&other=value/);
t.regex(inputValue.target, /\/api\/test\?token=REDACTED_[0-9a-f]{12}\.\.\.&other=value/);
t.regex(inputValue.target!, /\/api\/test\?token=REDACTED_[0-9a-f]{12}\.\.\.&other=value/);
});

test("transforms query parameter in client path with existing params", (t) => {
Expand All @@ -700,7 +704,7 @@
const result = engine.applyTransforms(spanWithQuery);

const inputValue = result.inputValue as HttpClientInputValue;
t.regex(inputValue.path, /\/v1\/charges\?api_key=REDACTED_[0-9a-f]{12}\.\.\.&other=value/);
t.regex(inputValue.path!, /\/v1\/charges\?api_key=REDACTED_[0-9a-f]{12}\.\.\.&other=value/);
});

test("handles query param that doesn't exist", (t) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,11 @@ test("should drop outbound span data but keep span present when calling dropped

// Should have transformMetadata indicating it was dropped
t.truthy(droppedSpan!.transformMetadata, "Expected transformMetadata to be defined");
const hasDropAction = droppedSpan!.transformMetadata.actions?.some(
const hasDropAction = droppedSpan!.transformMetadata?.actions?.some(
(action) => action.type === "drop" && action.field === "entire_span",
);
t.truthy(
hasDropAction,
`Expected drop action in transformMetadata, got ${JSON.stringify(droppedSpan!.transformMetadata.actions)}`,
`Expected drop action in transformMetadata, got ${JSON.stringify(droppedSpan!.transformMetadata?.actions)}`,
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,15 @@ const express = require("express");
const serviceAApp = express();
serviceAApp.use(express.json());

serviceAApp.post("/api/sensitive", (req, res) => {
serviceAApp.post("/api/sensitive", (req: any, res: any) => {
res.json({
status: "success",
sensitiveData: "TOP_SECRET_123",
userId: req.body.userId,
});
});

serviceAApp.get("/api/data", (req, res) => {
serviceAApp.get("/api/data", (req: any, res: any) => {
res.json({ data: "public data from service A" });
});

Expand All @@ -71,8 +71,8 @@ test.after.always(() => {

test("should be able to reach the server", async (t) => {
await new Promise<void>((resolve) => {
http.get(`http://127.0.0.1:${port}/api/data`, (res) => {
res.on("data", function (chunk) {
http.get(`http://127.0.0.1:${port}/api/data`, (res: any) => {
res.on("data", function (chunk: any) {
console.log("BODY: " + chunk);
});

Expand Down
2 changes: 0 additions & 2 deletions src/instrumentation/libraries/http/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type {
ServerResponse,
IncomingHttpHeaders,
} from "http";
import { HttpBodyType } from "./utils";

export type HttpProtocol = "http" | "https";

Expand All @@ -21,7 +20,6 @@ export interface HttpClientInputValue {
timeout?: number;
body?: unknown;
bodySize?: number;
bodyType?: HttpBodyType;
hasBodyParsingError?: boolean;
[key: string]: unknown;
}
Expand Down
Loading
Loading