Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 2 additions & 1 deletion backend/rest/routers/deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import httpx
from fastapi import Depends, Header, HTTPException, status
from shared.enums import MemberRole

# Configuration for internal API (Python → Next.js)
TRACEROOT_UI_URL = os.getenv("TRACEROOT_UI_URL", "http://localhost:3000")
Expand Down Expand Up @@ -76,7 +77,7 @@ async def get_project_access(
return ProjectAccessInfo(
project_id=project_id,
user_id=x_user_id,
role=data.get("role", "VIEWER"),
role=data.get("role", MemberRole.VIEWER),
)


Expand Down
Empty file added backend/shared/__init__.py
Empty file.
24 changes: 24 additions & 0 deletions backend/shared/enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from enum import StrEnum


class SpanKind(StrEnum):
LLM = "LLM"
AGENT = "AGENT"
TOOL = "TOOL"
SPAN = "SPAN"


class SpanStatus(StrEnum):
OK = "OK"
ERROR = "ERROR"


class TraceStatus(StrEnum):
OK = "ok"
ERROR = "error"


class MemberRole(StrEnum):
VIEWER = "VIEWER"
MEMBER = "MEMBER"
ADMIN = "ADMIN"
26 changes: 14 additions & 12 deletions backend/worker/transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
from datetime import UTC, datetime
from typing import Any

from shared.enums import SpanKind, SpanStatus

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -147,29 +149,29 @@ def get_span_kind(attrs: dict[str, Any], otel_kind: int | str | None) -> str:
"""
# Check explicit type attribute (handle None values)
explicit_type = (attrs.get("traceroot.span.type") or "").upper()
if explicit_type in ("LLM", "SPAN", "AGENT", "TOOL"):
if explicit_type in (SpanKind.LLM, SpanKind.SPAN, SpanKind.AGENT, SpanKind.TOOL):
return explicit_type

# Check OpenInference semantic conventions (handle None values)
openinference_type = (attrs.get("openinference.span.kind") or "").upper()
if openinference_type == "LLM":
return "LLM"
elif openinference_type == "AGENT":
return "AGENT"
elif openinference_type == "TOOL":
return "TOOL"
if openinference_type == SpanKind.LLM:
return SpanKind.LLM
elif openinference_type == SpanKind.AGENT:
return SpanKind.AGENT
elif openinference_type == SpanKind.TOOL:
return SpanKind.TOOL
elif openinference_type == "CHAIN":
return "SPAN"
return SpanKind.SPAN

# Default based on presence of LLM-related attributes
if (
attrs.get("gen_ai.system")
or attrs.get("llm.model_name")
or attrs.get("traceroot.llm.model")
):
return "LLM"
return SpanKind.LLM

return "SPAN"
return SpanKind.SPAN


def _extract_user_id(attrs: dict[str, Any]) -> str | None:
Expand Down Expand Up @@ -266,7 +268,7 @@ def transform_otel_to_clickhouse(
"span_end_time": end_time,
"name": span_name,
"span_kind": span_kind,
"status": "OK",
"status": SpanStatus.OK,
"environment": environment,
}

Expand Down Expand Up @@ -370,7 +372,7 @@ def transform_otel_to_clickhouse(
status_code = status.get("code", 0)
# Handle both int (0, 1, 2) and string ("STATUS_CODE_ERROR") formats
if status_code == 2 or status_code == "STATUS_CODE_ERROR":
span_record["status"] = "ERROR"
span_record["status"] = SpanStatus.ERROR
span_record["status_message"] = status.get("message")

spans.append(span_record)
Expand Down
3 changes: 2 additions & 1 deletion frontend/packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"clean": "rm -rf dist"
},
"dependencies": {
"@prisma/client": "^5.22.0"
"@prisma/client": "^5.22.0",
"zod": "^4.3.6"
},
"devDependencies": {
"@types/node": "^22.19.9",
Expand Down
22 changes: 22 additions & 0 deletions frontend/packages/core/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 14 additions & 8 deletions frontend/packages/core/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ datasource db {
url = env("DATABASE_URL")
}

enum MemberRole {
VIEWER
MEMBER
ADMIN
}

// Access Keys - API authentication for SDK/trace ingestion
model AccessKey {
id String @id @db.VarChar
Expand All @@ -25,10 +31,10 @@ model AccessKey {

// Invites - Pending workspace invitations
model Invite {
id String @id @db.VarChar
email String @db.VarChar
workspaceId String @map("workspace_id") @db.VarChar
role String @db.VarChar
id String @id @db.VarChar
email String @db.VarChar
workspaceId String @map("workspace_id") @db.VarChar
role MemberRole
invitedByUserId String? @map("invited_by_user_id") @db.VarChar
createTime DateTime @default(now()) @map("create_time") @db.Timestamp(6)
updateTime DateTime @default(now()) @map("update_time") @db.Timestamp(6)
Expand All @@ -42,10 +48,10 @@ model Invite {

// Workspace Members - User membership in workspaces
model WorkspaceMember {
id String @id @db.VarChar
workspaceId String @map("workspace_id") @db.VarChar
userId String @map("user_id") @db.VarChar
role String @db.VarChar
id String @id @db.VarChar
workspaceId String @map("workspace_id") @db.VarChar
userId String @map("user_id") @db.VarChar
role MemberRole
createTime DateTime @default(now()) @map("create_time") @db.Timestamp(6)
updateTime DateTime @default(now()) @map("update_time") @db.Timestamp(6)
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade, onUpdate: NoAction)
Expand Down
19 changes: 19 additions & 0 deletions frontend/packages/core/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Role — re-exported from Prisma (runtime object + type)
export { MemberRole as Role } from "@prisma/client";

// SpanKind — ClickHouse values
export const SpanKind = {
LLM: "LLM",
AGENT: "AGENT",
TOOL: "TOOL",
SPAN: "SPAN",
} as const;
export type SpanKind = (typeof SpanKind)[keyof typeof SpanKind];

// SpanStatus — ClickHouse values
export const SpanStatus = { OK: "OK", ERROR: "ERROR" } as const;
export type SpanStatus = (typeof SpanStatus)[keyof typeof SpanStatus];

// TraceStatus — lowercase, computed at query time
export const TraceStatus = { OK: "ok", ERROR: "error" } as const;
export type TraceStatus = (typeof TraceStatus)[keyof typeof TraceStatus];
4 changes: 4 additions & 0 deletions frontend/packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,9 @@ export type {
Account,
} from "@prisma/client";

// Constants & Zod schemas
export * from "./constants.js";
export * from "./schemas.js";

// Shared types
export * from "./types/index.js";
7 changes: 7 additions & 0 deletions frontend/packages/core/src/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { z } from "zod";
import { Role, SpanKind, SpanStatus, TraceStatus } from "./constants.js";

export const RoleSchema = z.enum(Role);
export const SpanKindSchema = z.enum(SpanKind);
export const SpanStatusSchema = z.enum(SpanStatus);
export const TraceStatusSchema = z.enum(TraceStatus);
12 changes: 7 additions & 5 deletions frontend/packages/core/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
// Role Types
export const ROLES = ["VIEWER", "MEMBER", "ADMIN"] as const;
export type Role = (typeof ROLES)[number];
import { Role } from "../constants.js";

export type { Role };

const ROLE_ORDER = [Role.VIEWER, Role.MEMBER, Role.ADMIN] as const;

/**
* Check if a role meets the minimum required role.
*/
export function hasMinRole(userRole: Role, minRole: Role): boolean {
const userIndex = ROLES.indexOf(userRole);
const minIndex = ROLES.indexOf(minRole);
const userIndex = ROLE_ORDER.indexOf(userRole);
const minIndex = ROLE_ORDER.indexOf(minRole);
return userIndex >= minIndex;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { prisma } from "@traceroot/core";
import { prisma, Role } from "@traceroot/core";
import {
requireAuth,
requireProjectAccess,
Expand All @@ -22,7 +22,7 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
if (authResult.error) return authResult.error;
const { user } = authResult;

const accessResult = await requireProjectAccess(user.id, projectId, "MEMBER");
const accessResult = await requireProjectAccess(user.id, projectId, Role.MEMBER);
if (accessResult.error) return accessResult.error;

// Check access key exists and belongs to this project
Expand Down Expand Up @@ -76,7 +76,7 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) {
if (authResult.error) return authResult.error;
const { user } = authResult;

const accessResult = await requireProjectAccess(user.id, projectId, "ADMIN");
const accessResult = await requireProjectAccess(user.id, projectId, Role.ADMIN);
if (accessResult.error) return accessResult.error;

// Check access key exists and belongs to this project
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { prisma } from "@traceroot/core";
import { prisma, Role } from "@traceroot/core";
import {
requireAuth,
requireProjectAccess,
Expand Down Expand Up @@ -60,7 +60,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
if (authResult.error) return authResult.error;
const { user } = authResult;

const accessResult = await requireProjectAccess(user.id, projectId, "MEMBER");
const accessResult = await requireProjectAccess(user.id, projectId, Role.MEMBER);
if (accessResult.error) return accessResult.error;

let body: unknown;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@traceroot/core";
import { prisma, Role } from "@traceroot/core";
import { requireAuth, requireWorkspaceMembership, errorResponse } from "@/lib/auth-helpers";

type RouteParams = { params: Promise<{ workspaceId: string; inviteId: string }> };
Expand All @@ -12,7 +12,7 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) {
if (authResult.error) return authResult.error;
const { user } = authResult;

const membershipResult = await requireWorkspaceMembership(user.id, workspaceId, "ADMIN");
const membershipResult = await requireWorkspaceMembership(user.id, workspaceId, Role.ADMIN);
if (membershipResult.error) return membershipResult.error;

// Check invite exists and belongs to this workspace
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { prisma } from "@traceroot/core";
import { prisma, Role, RoleSchema } from "@traceroot/core";
import {
requireAuth,
requireWorkspaceMembership,
Expand All @@ -11,7 +11,7 @@ import { sendInviteEmail } from "@/lib/email";

const createInviteSchema = z.object({
email: z.string().email("Invalid email"),
role: z.enum(["VIEWER", "MEMBER", "ADMIN"]),
role: RoleSchema,
});

type RouteParams = { params: Promise<{ workspaceId: string }> };
Expand All @@ -24,7 +24,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
if (authResult.error) return authResult.error;
const { user } = authResult;

const membershipResult = await requireWorkspaceMembership(user.id, workspaceId, "ADMIN");
const membershipResult = await requireWorkspaceMembership(user.id, workspaceId, Role.ADMIN);
if (membershipResult.error) return membershipResult.error;

const invites = await prisma.invite.findMany({
Expand Down Expand Up @@ -62,7 +62,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
if (authResult.error) return authResult.error;
const { user } = authResult;

const membershipResult = await requireWorkspaceMembership(user.id, workspaceId, "ADMIN");
const membershipResult = await requireWorkspaceMembership(user.id, workspaceId, Role.ADMIN);
if (membershipResult.error) return membershipResult.error;

let body: unknown;
Expand Down
Loading
Loading