Skip to content

Commit 74ae809

Browse files
chore(js): enforce safe type assertions (#3566)
Co-authored-by: Mikyo King <mikeldking@gmail.com>
1 parent 6689a74 commit 74ae809

36 files changed

Lines changed: 753 additions & 530 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"@arizeai/openinference-core": patch
3+
"@arizeai/openinference-genai": patch
4+
"@arizeai/openinference-instrumentation-anthropic": patch
5+
"@arizeai/openinference-instrumentation-bedrock-agent-runtime": patch
6+
"@arizeai/openinference-instrumentation-bedrock": patch
7+
"@arizeai/openinference-instrumentation-beeai": patch
8+
"@arizeai/openinference-instrumentation-claude-agent-sdk": patch
9+
"@arizeai/openinference-instrumentation-langchain-v0": patch
10+
"@arizeai/openinference-instrumentation-langchain": patch
11+
"@arizeai/openinference-instrumentation-mcp": patch
12+
"@arizeai/openinference-instrumentation-openai-agents": patch
13+
"@arizeai/openinference-instrumentation-openai": patch
14+
"@arizeai/openinference-tanstack-ai": patch
15+
"@arizeai/openinference-vercel": patch
16+
---
17+
18+
Replace unsafe type assertions with runtime type guards across packages (enforce `typescript/no-unsafe-type-assertion`)

js/.oxlintrc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
}
2525
],
2626
"typescript/consistent-return": "warn",
27-
"typescript/no-unsafe-type-assertion": "warn",
27+
"typescript/no-unsafe-type-assertion": "error",
2828
"typescript/prefer-reduce-type-parameter": "warn",
2929
"typescript/no-floating-promises": "warn",
3030
"typescript/unbound-method": "warn",

js/packages/openinference-core/src/helpers/decorators.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,14 @@ export function observe<Fn extends AnyFn>(options: SpanTraceOptions = {}) {
4848
// Create a wrapper that preserves 'this' context for class methods
4949
const wrappedMethod = function (this: unknown, ...args: Parameters<Fn>) {
5050
// Bind the original method to the current 'this' context
51-
const boundMethod = originalMethod.bind(this) as Fn;
51+
const boundMethod = originalMethod.bind(this);
5252

5353
// Use withSpan to wrap the bound method, ensuring consistent tracing behavior
5454
const tracedMethod = withSpan(boundMethod, traceOptions);
5555

5656
return tracedMethod(...args);
57-
} as Fn;
57+
};
5858

59-
return wrappedMethod;
59+
return Object.assign(wrappedMethod, originalMethod);
6060
};
6161
}

js/packages/openinference-core/src/helpers/withSpan.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { Exception } from "@opentelemetry/api";
12
import { SpanKind, SpanStatusCode } from "@opentelemetry/api";
23

34
import {
@@ -13,6 +14,25 @@ import type { AnyFn, InputToAttributesFn, OutputToAttributesFn, SpanTraceOptions
1314

1415
const { OPENINFERENCE_SPAN_KIND } = SemanticConventions;
1516

17+
/**
18+
* True when the thrown value is a valid OpenTelemetry {@link Exception} — a string or an
19+
* object carrying at least one of message, name, or code — so it can be recorded on the
20+
* span without losing its structured error details.
21+
*/
22+
function isException(error: unknown): error is Exception {
23+
if (typeof error === "string") {
24+
return true;
25+
}
26+
if (typeof error !== "object" || error == null) {
27+
return false;
28+
}
29+
return (
30+
("message" in error && typeof error.message === "string") ||
31+
("name" in error && typeof error.name === "string") ||
32+
("code" in error && (typeof error.code === "string" || typeof error.code === "number"))
33+
);
34+
}
35+
1636
/**
1737
* Wraps a function with openinference tracing capabilities, creating spans for execution monitoring.
1838
*
@@ -94,7 +114,7 @@ export function withSpan<Fn extends AnyFn = AnyFn>(fn: Fn, options?: SpanTraceOp
94114
return String(error);
95115
};
96116
// TODO: infer the name from the target
97-
const wrappedFn: Fn = function (this: ThisParameterType<Fn>, ...args: Parameters<Fn>) {
117+
const wrappedFn = function (this: ThisParameterType<Fn>, ...args: Parameters<Fn>) {
98118
const tracer = configuredTracer ?? getTracer();
99119
return tracer.startActiveSpan(
100120
spanName,
@@ -108,16 +128,16 @@ export function withSpan<Fn extends AnyFn = AnyFn>(fn: Fn, options?: SpanTraceOp
108128
},
109129
(span) => {
110130
const recordError = (error: unknown) => {
111-
span.recordException(error as Error);
131+
span.recordException(isException(error) ? error : String(error));
112132
span.setStatus({
113133
code: SpanStatusCode.ERROR,
114134
message: getErrorMessage(error),
115135
});
116136
};
117137

118138
try {
119-
const result = fn.apply(this, args) as ReturnType<Fn>;
120-
if (isPromise(result)) {
139+
const result = fn.apply(this, args);
140+
if (isPromise<Awaited<ReturnType<Fn>>>(result)) {
121141
// Execute the promise and return the promise chain
122142
return result
123143
.then((value: Awaited<ReturnType<Fn>>) => {
@@ -152,6 +172,6 @@ export function withSpan<Fn extends AnyFn = AnyFn>(fn: Fn, options?: SpanTraceOp
152172
}
153173
},
154174
);
155-
} as Fn;
156-
return wrappedFn;
175+
};
176+
return Object.assign(wrappedFn, fn);
157177
}

js/packages/openinference-core/src/trace/trace-config/OITracer.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ function formatStartActiveSpanParams<F extends OpenInferenceActiveSpanCallback>(
1919
) {
2020
let opts: SpanOptions | undefined;
2121
let ctx: Context | undefined;
22-
let fn: F;
22+
let fn: F | undefined;
2323

2424
if (typeof arg2 === "function") {
2525
fn = arg2;
@@ -29,9 +29,11 @@ function formatStartActiveSpanParams<F extends OpenInferenceActiveSpanCallback>(
2929
} else {
3030
opts = arg2;
3131
ctx = arg3;
32-
fn = arg4 as F;
32+
fn = arg4;
3333
}
3434

35+
if (fn == null) return;
36+
3537
opts = opts ?? {};
3638
ctx = ctx ?? apiContext.active();
3739

js/packages/openinference-core/src/trace/trace-config/constants.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export const REDACTED_VALUE = "__REDACTED__";
4646
* The default, environment, and type information for each value on the TraceConfig
4747
* Used to generate a full TraceConfig object with the correct types and default values
4848
*/
49-
export const traceConfigMetadata: Readonly<Record<TraceConfigKey, TraceConfigFlag>> = {
49+
export const traceConfigMetadata = {
5050
hideLLMTools: {
5151
default: DEFAULT_HIDE_LLM_TOOLS,
5252
envKey: OPENINFERENCE_HIDE_LLM_TOOLS,
@@ -102,7 +102,7 @@ export const traceConfigMetadata: Readonly<Record<TraceConfigKey, TraceConfigFla
102102
envKey: OPENINFERENCE_HIDE_PROMPTS,
103103
type: "boolean",
104104
},
105-
};
105+
} satisfies Readonly<Record<TraceConfigKey, TraceConfigFlag>>;
106106

107107
export const DefaultTraceConfig: TraceConfig = {
108108
hideLLMTools: DEFAULT_HIDE_LLM_TOOLS,

js/packages/openinference-core/src/trace/trace-config/traceConfig.ts

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import { assertUnreachable, withSafety } from "../../utils";
22
import { DefaultTraceConfig, traceConfigMetadata } from "./constants";
3-
import type { TraceConfig, TraceConfigKey, TraceConfigOptions } from "./types";
3+
import type {
4+
BooleanTraceConfigFlag,
5+
NumericTraceConfigFlag,
6+
TraceConfig,
7+
TraceConfigOptions,
8+
} from "./types";
49

510
const safelyParseInt = withSafety({ fn: parseInt });
611

7-
type TraceConfigOptionMetadata = (typeof traceConfigMetadata)[TraceConfigKey];
12+
type TraceConfigOptionMetadata = BooleanTraceConfigFlag | NumericTraceConfigFlag;
813

914
/**
1015
* Parses an option based on its type
@@ -13,6 +18,20 @@ type TraceConfigOptionMetadata = (typeof traceConfigMetadata)[TraceConfigKey];
1318
* @param optionMetadata - The {@link TraceConfigOptionMetadata} for the option which includes its type, default value, and environment variable key.
1419
*
1520
*/
21+
function parseOption({
22+
optionValue,
23+
optionMetadata,
24+
}: {
25+
optionValue?: boolean;
26+
optionMetadata: BooleanTraceConfigFlag;
27+
}): boolean;
28+
function parseOption({
29+
optionValue,
30+
optionMetadata,
31+
}: {
32+
optionValue?: number;
33+
optionMetadata: NumericTraceConfigFlag;
34+
}): number;
1635
function parseOption({
1736
optionValue,
1837
optionMetadata,
@@ -52,14 +71,50 @@ export function generateTraceConfig(options?: TraceConfigOptions): TraceConfig {
5271
if (options == null) {
5372
return DefaultTraceConfig;
5473
}
55-
return Object.entries(traceConfigMetadata).reduce((config, [key, optionMetadata]) => {
56-
const TraceConfigKey = key as TraceConfigKey;
57-
return {
58-
...config,
59-
[TraceConfigKey]: parseOption({
60-
optionValue: options[TraceConfigKey],
61-
optionMetadata,
62-
}),
63-
};
64-
}, {} as TraceConfig);
74+
return {
75+
hideLLMTools: parseOption({
76+
optionValue: options.hideLLMTools,
77+
optionMetadata: traceConfigMetadata.hideLLMTools,
78+
}),
79+
hideInputs: parseOption({
80+
optionValue: options.hideInputs,
81+
optionMetadata: traceConfigMetadata.hideInputs,
82+
}),
83+
hideOutputs: parseOption({
84+
optionValue: options.hideOutputs,
85+
optionMetadata: traceConfigMetadata.hideOutputs,
86+
}),
87+
hideInputMessages: parseOption({
88+
optionValue: options.hideInputMessages,
89+
optionMetadata: traceConfigMetadata.hideInputMessages,
90+
}),
91+
hideOutputMessages: parseOption({
92+
optionValue: options.hideOutputMessages,
93+
optionMetadata: traceConfigMetadata.hideOutputMessages,
94+
}),
95+
hideInputImages: parseOption({
96+
optionValue: options.hideInputImages,
97+
optionMetadata: traceConfigMetadata.hideInputImages,
98+
}),
99+
hideInputText: parseOption({
100+
optionValue: options.hideInputText,
101+
optionMetadata: traceConfigMetadata.hideInputText,
102+
}),
103+
hideOutputText: parseOption({
104+
optionValue: options.hideOutputText,
105+
optionMetadata: traceConfigMetadata.hideOutputText,
106+
}),
107+
hideEmbeddingVectors: parseOption({
108+
optionValue: options.hideEmbeddingVectors,
109+
optionMetadata: traceConfigMetadata.hideEmbeddingVectors,
110+
}),
111+
base64ImageMaxLength: parseOption({
112+
optionValue: options.base64ImageMaxLength,
113+
optionMetadata: traceConfigMetadata.base64ImageMaxLength,
114+
}),
115+
hidePrompts: parseOption({
116+
optionValue: options.hidePrompts,
117+
optionMetadata: traceConfigMetadata.hidePrompts,
118+
}),
119+
};
65120
}

js/packages/openinference-core/src/utils/typeUtils.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,7 @@ export function isObjectWithStringKeys(value: unknown): value is Record<string,
3434
* @returns true if it is a Promise
3535
*/
3636
export function isPromise<T = unknown>(value: unknown): value is Promise<T> {
37-
return (
38-
!!value &&
39-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
40-
typeof (value as any)?.then === "function" &&
41-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
42-
typeof (value as any)?.catch === "function"
43-
);
37+
return isObject(value) && typeof value.then === "function" && typeof value.catch === "function";
4438
}
4539

4640
/**

js/packages/openinference-genai/src/attributes.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -155,16 +155,15 @@ const isGenAIChatMessage = (value: unknown): value is ChatMessage => {
155155
* @param toolDefinition - The tool definition to normalize
156156
* @returns The normalized tool definition, or the original value when it cannot be normalized
157157
*/
158+
const isRecord = (value: unknown): value is Record<string, unknown> =>
159+
typeof value === "object" && value !== null && !Array.isArray(value);
160+
158161
const normalizeToolDefinition = (toolDefinition: unknown): unknown => {
159-
if (
160-
typeof toolDefinition !== "object" ||
161-
toolDefinition === null ||
162-
Array.isArray(toolDefinition)
163-
) {
162+
if (!isRecord(toolDefinition)) {
164163
return toolDefinition;
165164
}
166165

167-
const definition = toolDefinition as Record<string, unknown>;
166+
const definition = toolDefinition;
168167
if (typeof definition.function === "object" && definition.function !== null) {
169168
return definition;
170169
}

js/packages/openinference-instrumentation-anthropic/src/instrumentation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ export class AnthropicInstrumentation extends InstrumentationBase<typeof Anthrop
341341
* True when create() returned an APIPromise we can transform with _thenUnwrap.
342342
*/
343343
function hasThenUnwrap<T>(promise: PromiseLike<T>): promise is APIPromise<T> {
344-
return typeof (promise as Partial<APIPromise<T>>)._thenUnwrap === "function";
344+
return "_thenUnwrap" in promise && typeof promise._thenUnwrap === "function";
345345
}
346346

347347
/**

0 commit comments

Comments
 (0)