diff --git a/.changeset/custom-view-helpers.md b/.changeset/custom-view-helpers.md new file mode 100644 index 000000000..efc14939f --- /dev/null +++ b/.changeset/custom-view-helpers.md @@ -0,0 +1,5 @@ +--- +"braintrust": minor +--- + +feat: Add utilities for custom views (push + preview) diff --git a/js/package.json b/js/package.json index b85271cb8..6a3261f60 100644 --- a/js/package.json +++ b/js/package.json @@ -37,6 +37,13 @@ "require": "./dist/index.js", "default": "./dist/index.mjs" }, + "./custom-views": { + "types": "./dist/custom-views.d.ts", + "import": "./dist/custom-views.mjs", + "module": "./dist/custom-views.mjs", + "require": "./dist/custom-views.js", + "default": "./dist/custom-views.mjs" + }, "./workerd": { "import": "./dist/workerd.mjs", "require": "./dist/workerd.js", @@ -209,6 +216,7 @@ "dependencies": { "@apm-js-collab/code-transformer": "^0.12.0", "@next/env": "^14.2.3", + "@types/react": "^18.3.1", "@vercel/functions": "^1.0.2", "ajv": "^8.20.0", "argparse": "^2.0.1", diff --git a/js/src/custom-views/exports.ts b/js/src/custom-views/exports.ts new file mode 100644 index 000000000..44f3b357e --- /dev/null +++ b/js/src/custom-views/exports.ts @@ -0,0 +1 @@ +export { customDatasetView, customTraceView } from "./registry"; diff --git a/js/src/custom-views/registry.ts b/js/src/custom-views/registry.ts new file mode 100644 index 000000000..15b44008a --- /dev/null +++ b/js/src/custom-views/registry.ts @@ -0,0 +1,177 @@ +import type { ReactNode } from "react"; + +type SpanFieldName = "input" | "output" | "expected" | "metadata"; + +type SpanFields< + TInput = unknown, + TOutput = unknown, + TExpected = unknown, + TMetadata extends Record = Record, +> = { + input?: TInput; + output?: TOutput; + expected?: TExpected; + metadata?: TMetadata; +}; + +type TraceViewSpan< + TInput = unknown, + TOutput = unknown, + TExpected = unknown, + TMetadata extends Record = Record, +> = { + id: string; + span_id: string; + root_span_id: string; + parent_span_id?: string | null; + span_parents?: string[]; + data: { + input?: TInput; + output?: TOutput; + expected?: TExpected; + metadata?: TMetadata; + scores?: Record; + metrics?: Record; + error?: string; + tags?: string[]; + span_attributes?: Record; + [key: string]: unknown; + }; + children: string[]; +}; + +type CustomViewUpdateTarget = "selected" | "root" | { spanId: string }; + +type CustomViewUpdateResult = { + transactionId: string | null; +}; + +type CustomViewSpanUpdate< + TMetadata extends Record = Record, +> = { + target?: CustomViewUpdateTarget; + metadata?: Partial & Record; + scores?: Record; + tags?: string[] | null; +}; + +type TraceViewTrace< + TInput = unknown, + TOutput = unknown, + TExpected = unknown, + TMetadata extends Record = Record, +> = { + rootSpanId: string; + selectedSpanId: string; + spanOrder: string[]; + spans: Record>; + fetchSpanFields: ( + spanIds: string | string[], + fields?: SpanFieldName[], + ) => Promise< + Record> + >; + update: ( + update: CustomViewSpanUpdate, + ) => Promise; +}; + +type TraceViewProps< + TInput = unknown, + TOutput = unknown, + TExpected = unknown, + TMetadata extends Record = Record, +> = { + trace: TraceViewTrace; + span: TraceViewSpan; + selectSpan?: (spanId: string) => void; + update?: (field: string, value: unknown) => void; +}; + +type DatasetViewProps< + TInput = unknown, + TExpected = unknown, + TMetadata extends Record = Record, +> = { + id: string; + input?: TInput; + expected?: TExpected; + metadata?: TMetadata; + tags?: string[]; + update: (update: { + input?: TInput; + expected?: TExpected; + metadata?: Partial & Record; + tags?: string[] | null; + }) => Promise; +}; + +type Component = (props: Props) => ReactNode; + +type ProjectRef = string | { id: string } | { name: string }; +type DatasetRef = { id: string } | { name: string }; + +type CustomTraceViewDefinition = { + name: string; + slug: string; + project?: ProjectRef; +}; + +type CustomTraceView< + TInput = unknown, + TOutput = unknown, + TExpected = unknown, + TMetadata extends Record = Record, +> = CustomTraceViewDefinition & { + kind: "trace"; + component: Component>; +}; + +type CustomDatasetViewDefinition = { + name: string; + slug: string; + dataset: DatasetRef; + project?: ProjectRef; +}; + +type CustomDatasetView< + TInput = unknown, + TExpected = unknown, + TMetadata extends Record = Record, +> = CustomDatasetViewDefinition & { + kind: "dataset"; + component: Component>; +}; + +/** + * Defines a trace custom view for discovery by the `bt views` CLI. + * + * @experimental This API is not yet stabilized and may change across non-major versions. + */ +export function customTraceView< + TInput = unknown, + TOutput = unknown, + TExpected = unknown, + TMetadata extends Record = Record, +>( + definition: CustomTraceViewDefinition, + component: Component>, +): CustomTraceView { + return { ...definition, component, kind: "trace" }; +} + +/** + * Defines a dataset custom view for discovery by the `bt views` CLI. + * + * @experimental This API is not yet stabilized and may change across non-major versions. + */ +export function customDatasetView< + TInput = unknown, + TExpected = unknown, + TMetadata extends Record = Record, +>( + definition: CustomDatasetViewDefinition, + component: Component>, +): CustomDatasetView { + return { ...definition, component, kind: "dataset" }; +} diff --git a/js/tsup.config.ts b/js/tsup.config.ts index 9ee12a9d0..2559747de 100644 --- a/js/tsup.config.ts +++ b/js/tsup.config.ts @@ -5,6 +5,7 @@ export default defineConfig([ { entry: { index: "src/node/index.ts", + "custom-views": "src/custom-views/exports.ts", "apply-auto-instrumentation": "src/node/apply-auto-instrumentation-entry.ts", "vitest-evals-reporter": "src/wrappers/vitest-evals/reporter.ts", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6892f97c5..eea3642d9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -338,6 +338,9 @@ importers: '@next/env': specifier: ^14.2.3 version: 14.2.3 + '@types/react': + specifier: ^18.3.1 + version: 18.3.31 '@vercel/functions': specifier: ^1.0.2 version: 1.0.2 @@ -1799,12 +1802,18 @@ packages: '@types/pluralize@0.0.30': resolution: {integrity: sha512-kVww6xZrW/db5BR9OqiT71J9huRdQ+z/r+LbDuT7/EK50mCmj5FoaIARnVv0rvjUS/YpDox0cDU9lpQT011VBA==} + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + '@types/qs@6.9.18': resolution: {integrity: sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==} '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + '@types/react@18.3.31': + resolution: {integrity: sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==} + '@types/retry@0.12.0': resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} @@ -2305,6 +2314,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + dataloader@1.4.0: resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} @@ -5499,10 +5511,17 @@ snapshots: '@types/pluralize@0.0.30': {} + '@types/prop-types@15.7.15': {} + '@types/qs@6.9.18': {} '@types/range-parser@1.2.7': {} + '@types/react@18.3.31': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + '@types/retry@0.12.0': {} '@types/send@0.17.4': @@ -6063,6 +6082,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + csstype@3.2.3: {} + dataloader@1.4.0: {} dc-browser@1.0.4: {}