Compare commits

...
20 Commits
Author SHA1 Message Date
Marc Klingen 3c35e5e9c5 chore: release v2.44.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-05-21 12:32:58 +02:00
Marc KlingenandGitHub fe75e4c5f1 refactor: add time to first token to observations_view (#2092) 2024-05-21 12:30:27 +02:00
6435ce1a79 fix(docker-compose): Add healthcheck for db service (#2113)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-05-21 10:55:15 +02:00
marliessophieandGitHub d270143f0e fix: rename careers file to match hackernews post (#2115) 2024-05-21 09:54:21 +02:00
Marc Klingen 4041550d5e fix: link of help link of eval log 2024-05-21 00:35:46 +02:00
Max DeichmannandGitHub fc5ff52d1e docs: careers file extension (#2105) 2024-05-20 19:19:17 +00:00
ClemoandGitHub 6f6bef722b docs: create careers 2024-05-20 12:11:25 -07:00
Marc KlingenandGitHub 496ac68008 docs: Update .env.prod.example (#2103) 2024-05-20 17:12:36 +02:00
Marc KlingenandGitHub 595f9f1e5d fix(ui): parse chatml messages where content is an array (#2100) 2024-05-20 12:53:00 +00:00
Marc Klingen 70bca50ad0 fix(ui): spacing of user menu 2024-05-17 23:56:15 +02:00
Marc Klingen 99791458b4 fix(ui): padding of second-level menu 2024-05-17 23:54:33 +02:00
Marc KlingenandGitHub 60dc78b498 feat(ui): less padding in main menu (#2095) 2024-05-17 21:36:00 +00:00
Marc KlingenandGitHub 43e5887361 fix(api): merge tags when upserting a trace (#2091) 2024-05-17 18:43:10 +02:00
marliessophieandGitHub c009d9602d revert: fix(cloud) remove custom cookie management" (#2090)
Revert "fix(cloud): remove custom cookie management (#2087)"

This reverts commit 976f743841.
2024-05-17 14:32:54 +00:00
Max DeichmannandGitHub 830ebf6fff fix: set cache control explicitly (#2088) 2024-05-17 16:08:19 +02:00
Marc KlingenandGitHub 976f743841 fix(cloud): remove custom cookie management (#2087) 2024-05-17 14:12:11 +02:00
Max DeichmannandGitHub 89c1e46c55 chore: adjust sentry (#2086) 2024-05-17 10:21:51 +00:00
Marc Klingen 832e5d85ff fix(ee): capture exceptions when loading dynamic sso providers 2024-05-17 12:14:14 +02:00
Marc Klingen 9b3a1f3af3 feat(cloud): include data region in project invite emails 2024-05-17 12:08:40 +02:00
Marc Klingen c2515f7e6a feat(auth): improve auth error messages and logging 2024-05-17 11:37:45 +02:00
31 changed files with 207 additions and 71 deletions
+1 -1
View File
@@ -96,7 +96,7 @@ LANGFUSE_CSP_ENFORCE_HTTPS="true"
### START Langfuse Cloud Config
# Used for Langfuse Cloud deployments
# Not recommended for self-hosted deployments as these are NOT COVERED BY SEMVER
# Not recommended for self-hosted deployments as these are NOT COVERED BY SEMANTIC VERSIONING
# NEXT_PUBLIC_LANGFUSE_CLOUD_REGION="US"
# NEXTAUTH_COOKIE_DOMAIN=".langfuse.com"
+14
View File
@@ -0,0 +1,14 @@
_We are Hiring_
Join us in scaling Langfuse in Berlin, Germany. We are an open source company, we hire in person, we are only hiring technical talent.
_Open Roles_
- Backend Engineer, 70-110k EUR, 0.25-0.75% Equity, https://www.ycombinator.com/companies/langfuse/jobs/mnrdwla-backend-engineer
- Product Engineer, 70-110k EUR, 0.25-0.75% Equity, https://www.ycombinator.com/companies/langfuse/jobs/aAvmoFB-product-engineer
- Developer Advocate, 60-100k EUR, 0.25-0.5% Equity, https://www.ycombinator.com/companies/langfuse/jobs/uHysbKH-developer-advocate-devrel
_More Info_
- https://langfuse.com/careers
- https://langfuse.com/docs
- https://langfuse.com/changelog
+7 -1
View File
@@ -4,7 +4,8 @@ services:
langfuse-server:
image: ghcr.io/langfuse/langfuse:latest
depends_on:
- db
db:
condition: service_healthy
ports:
- "3000:3000"
environment:
@@ -18,6 +19,11 @@ services:
db:
image: postgres
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 3s
timeout: 3s
retries: 10
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "langfuse",
"version": "2.43.2",
"version": "2.44.0",
"author": "engineering@langfuse.com",
"license": "MIT",
"private": true,
@@ -293,6 +293,7 @@ export type ObservationView = {
calculated_output_cost: string | null;
calculated_total_cost: string | null;
latency: number | null;
time_to_first_token: number | null;
};
export type PosthogIntegration = {
project_id: string;
@@ -0,0 +1,61 @@
CREATE OR REPLACE VIEW "observations_view" AS
SELECT
o.*,
m.id AS "model_id",
m.start_date AS "model_start_date",
m.input_price,
m.output_price,
m.total_price,
m.tokenizer_config AS "tokenizer_config",
CASE
WHEN o.input_cost IS NULL AND o.output_cost IS NULL AND o.total_cost IS NULL THEN
o.prompt_tokens::decimal * m.input_price
ELSE
o.input_cost
END AS "calculated_input_cost",
CASE
WHEN o.input_cost IS NULL AND o.output_cost IS NULL AND o.total_cost IS NULL THEN
o.completion_tokens::decimal * m.output_price
ELSE
o.output_cost
END AS "calculated_output_cost",
CASE
WHEN o.input_cost IS NULL AND o.output_cost IS NULL AND o.total_cost IS NULL THEN
CASE
WHEN m.total_price IS NOT NULL AND o.total_tokens IS NOT NULL THEN
m.total_price * o.total_tokens
ELSE
o.prompt_tokens::decimal * m.input_price +
o.completion_tokens::decimal * m.output_price
END
ELSE
o.total_cost
END AS "calculated_total_cost",
CASE WHEN o.end_time IS NULL THEN NULL ELSE (EXTRACT(EPOCH FROM o."end_time") - EXTRACT(EPOCH FROM o."start_time"))::double precision END AS "latency",
CASE WHEN o.completion_start_time IS NOT NULL AND o.start_time IS NOT NULL THEN EXTRACT(EPOCH FROM (completion_start_time - start_time))::double precision ELSE NULL END as "time_to_first_token"
FROM
observations o
LEFT JOIN LATERAL (
SELECT
models.*
FROM
models
WHERE (models.project_id = o.project_id OR models.project_id IS NULL)
AND models.model_name = o.internal_model
AND (models.start_date < o.start_time OR models.start_date IS NULL)
AND o.unit::TEXT = models.unit
ORDER BY
models.project_id ASC, -- in postgres, NULLs are sorted last when ordering ASC
models.start_date DESC NULLS LAST -- now, NULLs are sorted last when ordering DESC as well
LIMIT 1
) m ON TRUE
-- requirements:
-- 1. The view should return all columns from the observations table
-- 2. The view should match with only one model for each observation if:
-- a. The model has the same project_id as the observation, otherwise the model without project_id.
-- b. The model has the same model_name as the observation
-- c. The model has a start_date that is less than the observation start_time, otherwise the model without start_date
-- d. The model has the same unit as the observation
+1
View File
@@ -373,6 +373,7 @@ view ObservationView {
calculatedOutputCost Decimal? @map("calculated_output_cost")
calculatedTotalCost Decimal? @map("calculated_total_cost")
latency Float? @map("latency")
timeToFirstToken Float? @map("time_to_first_token")
@@map("observations_view")
}
+11 -2
View File
@@ -528,9 +528,9 @@ function createObjects(
const spanTsStart = new Date(
traceTs.getTime() + Math.floor(Math.random() * 30)
);
// random duration of upto 30ms
// random duration of upto 5000ms
const spanTsEnd = new Date(
spanTsStart.getTime() + Math.floor(Math.random() * 30)
spanTsStart.getTime() + Math.floor(Math.random() * 5000)
);
const span = {
@@ -574,6 +574,13 @@ function createObjects(
(spanTsEnd.getTime() - generationTsStart.getTime())
)
);
// somewhere in the middle
const generationTsCompletionStart = new Date(
generationTsStart.getTime() +
Math.floor(
(generationTsEnd.getTime() - generationTsStart.getTime()) / 3
)
);
const promptTokens = Math.floor(Math.random() * 1000) + 300;
const completionTokens = Math.floor(Math.random() * 500) + 100;
@@ -602,6 +609,8 @@ function createObjects(
id: `generation-${v4()}`,
startTime: generationTsStart,
endTime: generationTsEnd,
completionStartTime:
Math.random() > 0.5 ? generationTsCompletionStart : undefined,
name: `generation-${i}-${j}-${k}`,
projectId: trace.projectId,
promptId: promptId,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "2.43.2",
"version": "2.44.0",
"private": true,
"license": "MIT",
"engines": {
+9
View File
@@ -20,6 +20,15 @@ if (process.env.NEXT_PUBLIC_SENTRY_DSN)
) {
return 0.3;
}
if (
samplingContext.request &&
samplingContext.request.url &&
samplingContext.request.url.includes("api/auth") &&
samplingContext.transactionContext.status !== "ok" &&
samplingContext.transactionContext.status !== "unauthenticated"
) {
return 1;
}
return 0.1;
},
@@ -727,6 +727,7 @@ describe("/api/public/ingestion API Endpoint", () => {
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
tags: ["tag-1", "tag-2"],
},
},
],
@@ -743,6 +744,7 @@ describe("/api/public/ingestion API Endpoint", () => {
id: traceId,
name: "trace-name",
userId: "user-2",
tags: ["tag-3", "tag-4"],
},
},
],
@@ -763,6 +765,7 @@ describe("/api/public/ingestion API Endpoint", () => {
expect(dbTrace[0]?.externalId).toBeNull();
expect(dbTrace[0]?.version).toBe("2.0.0");
expect(dbTrace[0]?.projectId).toBe("7a88fb47-b4e2-43b8-a06c-a5ce950dc53a");
expect(dbTrace[0]?.tags).toEqual(["tag-1", "tag-2", "tag-3", "tag-4"]);
});
it("should fail for wrong event formats", async () => {
+24 -24
View File
@@ -253,7 +253,7 @@ export default function Layout(props: PropsWithChildren) {
>
<span className="sr-only">Close sidebar</span>
<XMarkIcon
className="h-6 w-6 text-white"
className="h-5 w-5 text-white"
aria-hidden="true"
/>
</button>
@@ -273,7 +273,7 @@ export default function Layout(props: PropsWithChildren) {
<MainNavigation nav={navigation} />
</ul>
<div className="mb-2 flex flex-row place-content-between items-center">
<div className="text-xs font-semibold leading-6 text-gray-400">
<div className="text-xs font-semibold text-gray-400">
Project
</div>
<NewProjectButton size="xs" />
@@ -291,7 +291,7 @@ export default function Layout(props: PropsWithChildren) {
</Transition.Root>
{/* Static sidebar for desktop */}
<div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-60 lg:flex-col">
<div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-56 lg:flex-col">
{/* Sidebar component, swap this element with another sidebar if you like */}
<div className="flex h-screen grow flex-col border-r border-gray-200 bg-white pt-7">
<LangfuseLogo
@@ -312,16 +312,16 @@ export default function Layout(props: PropsWithChildren) {
description="What do you think about this project? What can be improved?"
type="feedback"
>
<li className="group -mx-2 my-1 flex cursor-pointer gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-gray-700 hover:bg-gray-50 hover:text-indigo-600">
<li className="group -mx-2 my-1 flex cursor-pointer gap-x-3 rounded-md p-1.5 text-sm font-semibold text-gray-700 hover:bg-gray-50 hover:text-indigo-600">
<MessageSquarePlus
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
className="h-5 w-5 shrink-0 text-gray-400 group-hover:text-indigo-600"
aria-hidden="true"
/>
Feedback
</li>
</FeedbackButtonWrapper>
<div className="mb-2 flex flex-row place-content-between items-center">
<div className="text-xs font-semibold leading-6 text-gray-400">
<div className="text-xs font-semibold text-gray-400">
Project
</div>
<NewProjectButton size="xs" />
@@ -334,9 +334,9 @@ export default function Layout(props: PropsWithChildren) {
</nav>
<Menu as="div" className="relative">
<Menu.Button className="flex w-full items-center gap-x-4 overflow-hidden p-1.5 py-3 pl-6 pr-10 text-sm font-semibold leading-6 text-gray-900 hover:bg-gray-50">
<Menu.Button className="flex w-full items-center gap-x-2 overflow-hidden p-1.5 py-3 pl-6 pr-8 text-sm font-semibold text-gray-900 hover:bg-gray-50">
<span className="sr-only">Open user menu</span>
<Avatar className="h-8 w-8">
<Avatar className="h-7 w-7">
<AvatarImage src={session.data?.user?.image ?? undefined} />
<AvatarFallback>
{session.data?.user?.name
@@ -348,7 +348,7 @@ export default function Layout(props: PropsWithChildren) {
: null}
</AvatarFallback>
</Avatar>
<span className="flex-shrink truncate text-sm font-semibold leading-6 text-gray-900">
<span className="flex-shrink truncate text-sm font-semibold text-gray-900">
{session.data?.user?.name}
</span>
<div className="flex-1" />
@@ -367,7 +367,7 @@ export default function Layout(props: PropsWithChildren) {
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute -top-full right-0 z-10 mt-2.5 rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
<span className="mb-1 block border-b px-3 pb-2 text-sm leading-6 text-gray-500">
<span className="mb-1 block border-b px-3 pb-2 text-sm text-gray-500">
{session.data?.user?.email}
</span>
{userNavigation.map((item) => (
@@ -377,7 +377,7 @@ export default function Layout(props: PropsWithChildren) {
onClick={() => void item.onClick()}
className={cn(
active ? "bg-gray-50" : "",
"block cursor-pointer px-3 py-1 text-sm leading-6 text-gray-900",
"block cursor-pointer px-3 py-1 text-sm text-gray-900",
)}
>
{item.name}
@@ -398,7 +398,7 @@ export default function Layout(props: PropsWithChildren) {
onClick={() => setSidebarOpen(true)}
>
<span className="sr-only">Open sidebar</span>
<Bars3Icon className="h-6 w-6" aria-hidden="true" />
<Bars3Icon className="h-5 w-5" aria-hidden="true" />
</button>
<LangfuseLogo
version
@@ -406,7 +406,7 @@ export default function Layout(props: PropsWithChildren) {
showEnvLabel={session.data?.user?.email?.endsWith("@langfuse.com")}
/>
<Menu as="div" className="relative">
<Menu.Button className="flex items-center gap-x-4 text-sm font-semibold leading-6 text-gray-900">
<Menu.Button className="flex items-center gap-x-4 text-sm font-semibold text-gray-900">
<span className="sr-only">Open user menu</span>
<Avatar className="h-8 w-8">
<AvatarImage src={session.data?.user?.image ?? undefined} />
@@ -431,7 +431,7 @@ export default function Layout(props: PropsWithChildren) {
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 z-10 mt-2.5 rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
<span className="mb-1 block border-b px-3 pb-2 text-sm leading-6 text-gray-500">
<span className="mb-1 block border-b px-3 pb-2 text-sm text-gray-500">
{session.data?.user?.email}
</span>
{userNavigation.map((item) => (
@@ -441,7 +441,7 @@ export default function Layout(props: PropsWithChildren) {
onClick={() => void item.onClick()}
className={cn(
active ? "bg-gray-50" : "",
"block cursor-pointer px-3 py-1 text-sm leading-6 text-gray-900",
"block cursor-pointer px-3 py-1 text-sm text-gray-900",
)}
>
{item.name}
@@ -453,7 +453,7 @@ export default function Layout(props: PropsWithChildren) {
</Transition>
</Menu>
</div>
<div className="lg:pl-60">
<div className="lg:pl-56">
{env.NEXT_PUBLIC_DEMO_PROJECT_ID &&
projectId === env.NEXT_PUBLIC_DEMO_PROJECT_ID &&
(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "STAGING" ||
@@ -525,7 +525,7 @@ const MainNavigation: React.FC<{
item.current
? "bg-gray-50 text-indigo-600"
: "text-gray-700 hover:bg-gray-50 hover:text-indigo-600",
"group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6",
"group flex gap-x-3 rounded-md p-1.5 text-sm font-semibold",
)}
onClick={onNavitemClick}
target={item.newTab ? "_blank" : undefined}
@@ -536,7 +536,7 @@ const MainNavigation: React.FC<{
item.current
? "text-indigo-600"
: "text-gray-400 group-hover:text-indigo-600",
"h-6 w-6 shrink-0",
"h-5 w-5 shrink-0",
)}
aria-hidden="true"
/>
@@ -545,7 +545,7 @@ const MainNavigation: React.FC<{
{item.label && (
<span
className={cn(
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
"-my-0.5 self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
item.current
? "border-indigo-600 text-indigo-600"
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
@@ -565,12 +565,12 @@ const MainNavigation: React.FC<{
{({ open }) => (
<>
<Disclosure.Button
className="group flex w-full items-center gap-x-3 rounded-md p-2 text-left text-sm font-semibold leading-6 hover:bg-gray-50 hover:text-indigo-600"
className="group flex w-full items-center gap-x-3 rounded-md p-1.5 text-left text-sm font-semibold hover:bg-gray-50 hover:text-indigo-600"
onClick={() => setIsOpen(!isOpen)}
>
{item.icon && (
<item.icon
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
className="h-5 w-5 shrink-0 text-gray-400 group-hover:text-indigo-600"
aria-hidden="true"
/>
)}
@@ -578,7 +578,7 @@ const MainNavigation: React.FC<{
{item.label && (
<span
className={cn(
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
"-my-0.5 self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
item.current
? "border-indigo-600 text-indigo-600"
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
@@ -595,7 +595,7 @@ const MainNavigation: React.FC<{
aria-hidden="true"
/>
</Disclosure.Button>
<Disclosure.Panel as="ul" className="mt-1 px-2">
<Disclosure.Panel as="ul" className="mt-1 space-y-1 px-2">
{item.children?.map((subItem) => (
<li key={subItem.name}>
{/* 44px */}
@@ -605,7 +605,7 @@ const MainNavigation: React.FC<{
subItem.current
? "bg-gray-50 text-indigo-600"
: "text-gray-700 hover:bg-gray-50 hover:text-indigo-600",
"flex w-full items-center gap-x-3 rounded-md py-2 pl-9 pr-2 text-sm leading-6",
"ml-0.5 flex w-full items-center gap-x-3 rounded-md p-1.5 pl-7 pr-2 text-sm",
)}
target={subItem.newTab ? "_blank" : undefined}
>
+1 -1
View File
@@ -32,7 +32,7 @@ export const ProjectNavigation: React.FC<ProjectNavigationProps> = ({
router.push(`/project/${value}`);
}}
>
<SelectTrigger className="text-gray-700 ring-transparent focus:ring-0 focus:ring-offset-0">
<SelectTrigger className="h-8 text-gray-700 ring-transparent focus:ring-0 focus:ring-offset-0">
<SelectValue
className="text-sm font-semibold text-gray-700"
placeholder={currentProjectId}
@@ -59,6 +59,7 @@ export type GenerationsTableRow = {
endTime?: string;
completionStartTime?: Date;
latency?: number;
timeToFirstToken?: number;
name?: string;
model?: string;
// i/o not set explicitly, but fetched from the server from the cell
@@ -310,21 +311,12 @@ export default function GenerationsTable({
enableHiding: true,
enableSorting: true,
cell: ({ row }) => {
const startTime: Date = row.getValue("startTime");
const completionStartTime: Date | undefined =
const timeToFirstToken: number | undefined =
row.getValue("timeToFirstToken");
if (!completionStartTime) {
return undefined;
}
const latencyInSeconds =
intervalInSeconds(startTime, completionStartTime) || "-";
return (
<span>
{typeof latencyInSeconds === "number"
? formatIntervalSeconds(latencyInSeconds)
: latencyInSeconds}
{timeToFirstToken ? formatIntervalSeconds(timeToFirstToken) : "-"}
</span>
);
},
@@ -623,7 +615,7 @@ export default function GenerationsTable({
traceName: generation.traceName ?? "",
startTime: generation.startTime,
endTime: generation.endTime?.toLocaleString() ?? undefined,
timeToFirstToken: generation.completionStartTime ?? undefined,
timeToFirstToken: generation.timeToFirstToken ?? undefined,
latency: generation.latency ?? undefined,
totalCost: generation.calculatedTotalCost ?? undefined,
inputCost: generation.calculatedInputCost ?? undefined,
+1 -2
View File
@@ -128,12 +128,11 @@ const ChatMlMessageSchema = z
.optional(),
name: z.string().optional(),
content: z
.union([z.record(z.any()), z.record(z.any()).array(), z.string()])
.union([z.record(z.any()), z.string(), z.array(z.any())])
.nullish(),
additional_kwargs: z.record(z.any()).optional(),
})
.passthrough()
.refine((value) => value.content !== null || value.role !== undefined)
.transform(({ additional_kwargs, ...other }) => ({
...other,
@@ -67,14 +67,10 @@ export const ObservationPreview = (props: {
projectId={preloadedObservation.projectId}
/>
) : undefined}
{preloadedObservation.completionStartTime ? (
{preloadedObservation.timeToFirstToken ? (
<Badge variant="outline">
Time to first token:{" "}
{formatIntervalSeconds(
(preloadedObservation.completionStartTime.getTime() -
preloadedObservation.startTime.getTime()) /
1000,
)}
{formatIntervalSeconds(preloadedObservation.timeToFirstToken)}
</Badge>
) : null}
{preloadedObservation.endTime ? (
+1 -1
View File
@@ -1 +1 @@
export const VERSION = "v2.43.2";
export const VERSION = "v2.44.0";
+5 -1
View File
@@ -27,7 +27,11 @@ export async function createUserEmailPassword(
},
});
if (user !== null) {
throw new Error("User with email already exists. Please sign in.");
throw new Error(
user.password !== null
? "User with email already exists. Please sign in."
: "You have already signed up via an identity provider. Please sign in.",
);
}
const newUser = await prisma.user.create({
@@ -41,6 +41,7 @@ export const sendProjectInvitation = async (
projectName: projectName,
recieverEmail: to,
inviteLink: authUrl,
langfuseCloudRegion: env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION,
}),
);
@@ -23,6 +23,7 @@ interface ProjectInvitationTemplateProps {
projectName: string;
recieverEmail: string;
inviteLink: string;
langfuseCloudRegion?: string;
}
export const ProjectInvitationTemplate = ({
@@ -31,6 +32,7 @@ export const ProjectInvitationTemplate = ({
projectName,
recieverEmail,
inviteLink,
langfuseCloudRegion,
}: ProjectInvitationTemplateProps) => {
const previewText = `Join ${invitedByUsername} on Langfuse`;
@@ -63,7 +65,10 @@ export const ProjectInvitationTemplate = ({
{invitedByUserEmail}
</Link>
) has invited you to the <strong>{projectName}</strong> project on
Langfuse.
{langfuseCloudRegion
? ` Langfuse (${langfuseCloudRegion} data region)`
: " Langfuse"}
.
</Text>
<Section className="mb-4 mt-8 text-center">
<Button
+4
View File
@@ -5,5 +5,9 @@ import NextAuth from "next-auth";
export default async function auth(req: NextApiRequest, res: NextApiResponse) {
// Do whatever you want here, before the request is passed down to `NextAuth`
const authOptions = await getAuthOptions();
// https://github.com/nextauthjs/next-auth/issues/2408#issuecomment-1382629234
// for api routes, we need to call the headers in the api route itself
// disable caching for anything auth related
res.setHeader("Cache-Control", "no-store, max-age=0");
return await NextAuth(req, res, authOptions);
}
+19 -7
View File
@@ -234,19 +234,31 @@ export default function SignIn({ authProviders, signUpDisabled }: PageProps) {
redirect: false,
turnstileToken,
});
if (result?.error) {
setCredentialsFormError(result.error);
// Refresh turnstile as the token can only be used once
if (env.NEXT_PUBLIC_TURNSTILE_SITE_KEY && turnstileToken) {
setTurnstileCData(new Date().getTime().toString());
setTurnstileToken(undefined);
if (result === undefined) {
setCredentialsFormError("An unexpected error occurred.");
captureException(new Error("Sign in result is undefined"));
} else if (!result.ok) {
if (!result.error) {
captureException(
new Error(
`Sign in result error is falsy, result: ${JSON.stringify(result)}`,
),
);
}
setCredentialsFormError(
result?.error ?? "An unexpected error occurred.",
);
}
} catch (error) {
captureException(error);
console.error(error);
setCredentialsFormError("An unexpected error occurred.");
} finally {
// Refresh turnstile as the token can only be used once
if (env.NEXT_PUBLIC_TURNSTILE_SITE_KEY && turnstileToken) {
setTurnstileCData(new Date().getTime().toString());
setTurnstileToken(undefined);
}
}
}
@@ -17,7 +17,7 @@ export default function TemplatesPage() {
title="Eval Log"
help={{
description: "View of all running evals.",
href: "https://langfuse.com/docs/evals",
href: "https://langfuse.com/docs/scores/model-based-evals",
}}
/>
<EvalLogTable projectId={projectId} />
@@ -101,6 +101,7 @@ export async function getAllGenerations({
o.trace_id as "traceId",
t.name as "traceName",
o.completion_start_time as "completionStartTime",
o.time_to_first_token as "timeToFirstToken",
o.prompt_tokens as "promptTokens",
o.completion_tokens as "completionTokens",
o.total_tokens as "totalTokens",
@@ -59,9 +59,7 @@ export function transformStreamToCsv(): Transform {
row.endTime?.toISOString() ?? "",
row.completionStartTime?.toISOString() ?? "",
// time to first token
row.completionStartTime
? intervalInSeconds(row.startTime, row.completionStartTime).toFixed(2)
: "",
row.timeToFirstToken?.toFixed(2) ?? "",
row.scores ? JSON.stringify(row.scores) : "",
row.latency ? formatIntervalSeconds(row.latency).slice(0, -1) : "",
// latency per token
+1
View File
@@ -257,6 +257,7 @@ export const traceRouter = createTRPCRouter({
totalTokens: true,
unit: true,
completionStartTime: true,
timeToFirstToken: true,
promptId: true,
modelId: true,
inputPrice: true,
@@ -449,6 +449,10 @@ export class TraceProcessor implements EventProcessor {
body.metadata ?? undefined,
);
const mergedTags = existingTrace?.tags
? existingTrace.tags.concat(body.tags ?? [])
: body.tags;
if (body.sessionId) {
await prisma.traceSession.upsert({
where: {
@@ -486,7 +490,7 @@ export class TraceProcessor implements EventProcessor {
sessionId: body.sessionId ?? undefined,
public: body.public ?? undefined,
projectId: apiScope.projectId,
tags: body.tags ?? undefined,
tags: mergedTags ?? undefined,
},
update: {
name: body.name ?? undefined,
@@ -501,7 +505,7 @@ export class TraceProcessor implements EventProcessor {
version: body.version ?? undefined,
sessionId: body.sessionId ?? undefined,
public: body.public ?? undefined,
tags: body.tags ?? undefined,
tags: mergedTags ?? undefined,
},
});
return upsertedTrace;
+16 -2
View File
@@ -27,6 +27,7 @@ import {
loadSsoProviders,
} from "@langfuse/ee/sso";
import { z } from "zod";
import * as Sentry from "@sentry/nextjs";
const staticProviders: Provider[] = [
CredentialsProvider({
@@ -90,7 +91,10 @@ const staticProviders: Provider[] = [
});
if (!dbUser) throw new Error("Invalid credentials");
if (dbUser.password === null) throw new Error("Invalid credentials");
if (dbUser.password === null)
throw new Error(
"Please sign in with the identity provider that is linked to your account.",
);
const isValidPassword = await verifyPassword(
credentials.password,
@@ -212,7 +216,13 @@ const extendedPrismaAdapter: Adapter = {
* @see https://next-auth.js.org/configuration/options
*/
export async function getAuthOptions(): Promise<NextAuthOptions> {
const dynamicSsoProviders = await loadSsoProviders();
let dynamicSsoProviders: Provider[] = [];
try {
dynamicSsoProviders = await loadSsoProviders();
} catch (e) {
console.error("Error loading dynamic SSO providers", e);
Sentry.captureException(e);
}
const providers = [...staticProviders, ...dynamicSsoProviders];
const data: NextAuthOptions = {
@@ -365,5 +375,9 @@ export const getServerAuthSession = async (ctx: {
res: GetServerSidePropsContext["res"];
}) => {
const authOptions = await getAuthOptions();
// https://github.com/nextauthjs/next-auth/issues/2408#issuecomment-1382629234
// for api routes, we need to call the headers in the api route itself
// disable caching for anything auth related
ctx.res.setHeader("Cache-Control", "no-store, max-age=0");
return getServerSession(ctx.req, ctx.res, authOptions);
};
+1
View File
@@ -298,6 +298,7 @@ export type ObservationView = {
calculated_output_cost: string | null;
calculated_total_cost: string | null;
latency: number | null;
time_to_first_token: number | null;
};
export type PosthogIntegration = {
project_id: string;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "worker",
"version": "2.43.2",
"version": "2.44.0",
"description": "",
"license": "MIT",
"main": "index.js",
+1 -1
View File
@@ -1 +1 @@
export const VERSION = "v2.43.2";
export const VERSION = "v2.44.0";