v3: Various fixes to support papermark use-case (#970)
* Adding a changeset for v3 * Add a version field to @trigger.dev/core-apps package.json * Build trigger.dev when doing a prerelease * bundle @trigger.dev/core-apps with trigger.dev cli * Fix the init command config template * Don’t use * for the @trigger.dev/core dep version specifier * strip workspace: from the package version before installing it * Added dependenciesToBundle config option to bundle ESM only packages * Added logging around resolving dependency paths * Try again * Resolve dependencies based on the project dir first * flip the bundled default * Adding some logs around dev task completion notifications * Adding some additional logs * Add more logs * Write out the log using process.stdout * Store pending completion notifications and resume them when awaited (fixes race condition) * Cleanup some of the logs * Copy over the postinstall step from the projects package.json * Add support for including additional files when deploying (e.g. prisma schema) * Don’t run scripts when resolving deps * copy all the files just in case anything is needed in postinstall * Remove duplicate option * Use the tag when outputting the dev command * Remove the postinstall script * add the trigger dir to the config if the default is not chosen * Remove the “hud” display in the dev command * trigger file names with dashes now work * Better file watching in dev * Much better duplicate ID experience now * Much better “Project not found” error * Export the handleError function types from sdk * Add support for configuring instrumentation * Upgrade and unify @opentelemetry/* packages (and remove storybook from the webapp) * Fix typescript error in react package * Upgrade react types in webapp * Allow span icons to be determined based on the span name (e.g. prisma:) * Ignore built-in env vars when checking for env vars, and allow continuing the deployment even if missing env vars were detected * Improve the retry.fetch default behavior and option structure * Fixed typescript errors with packages/email react types * Update the retry.fetch docs
This commit is contained in:
@@ -16,7 +16,10 @@
|
||||
"emails",
|
||||
"proxy",
|
||||
"yalt",
|
||||
"@trigger.dev/database"
|
||||
"@trigger.dev/database",
|
||||
"coordinator",
|
||||
"docker-provider",
|
||||
"kubernetes-provider"
|
||||
],
|
||||
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
|
||||
"onlyUpdatePeerDependentsWhenOutOfRange": true
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"trigger.dev": major
|
||||
"@trigger.dev/core": major
|
||||
"@trigger.dev/otlp-importer": major
|
||||
"@trigger.dev/sdk": major
|
||||
---
|
||||
|
||||
Updates to support Trigger.dev v3
|
||||
+1
-1
@@ -47,7 +47,7 @@ pnpm exec changeset version --snapshot prerelease
|
||||
3. Build the packages:
|
||||
|
||||
```sh
|
||||
pnpm run build --filter "@trigger.dev/*"
|
||||
pnpm run build --filter "@trigger.dev/*" --filter "trigger.dev"
|
||||
```
|
||||
|
||||
4. Publish the snapshot (replace "dev" with your tag)
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import type { StorybookConfig } from "@storybook/react-webpack5";
|
||||
import path from "path";
|
||||
|
||||
const root = path.resolve(__dirname, "../app");
|
||||
|
||||
const config: StorybookConfig = {
|
||||
webpackFinal: async (config) => {
|
||||
return {
|
||||
...config,
|
||||
resolve: {
|
||||
...config.resolve,
|
||||
alias: {
|
||||
...(config.resolve?.alias ?? {}),
|
||||
"~": root,
|
||||
},
|
||||
extensions: [
|
||||
...(config.resolve?.extensions ?? []),
|
||||
...[".ts", ".tsx", ".js", ".jsx", ".mdx"],
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
stories: ["../app/**/stories/*.mdx", "../app/**/stories/*.stories.@(js|jsx|ts|tsx)"],
|
||||
addons: [
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-essentials",
|
||||
"@storybook/addon-interactions",
|
||||
"storybook-addon-variants",
|
||||
"storybook-addon-designs",
|
||||
{
|
||||
name: "@storybook/addon-styling",
|
||||
options: {
|
||||
// Check out https://github.com/storybookjs/addon-styling/blob/main/docs/api.md
|
||||
// For more details on this addon's options.
|
||||
postCss: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
framework: {
|
||||
name: "@storybook/react-webpack5",
|
||||
options: {},
|
||||
},
|
||||
docs: {
|
||||
autodocs: "tag",
|
||||
},
|
||||
staticDirs: [path.resolve("public")],
|
||||
};
|
||||
export default config;
|
||||
@@ -1,50 +0,0 @@
|
||||
import type { Preview } from "@storybook/react";
|
||||
import "../app/tailwind.css";
|
||||
import { createRemixStub } from "@remix-run/testing";
|
||||
import React from "react";
|
||||
import { LocaleContextProvider } from "../app/components/primitives/LocaleProvider";
|
||||
import { OperatingSystemContextProvider } from "../app/components/primitives/OperatingSystemProvider";
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
backgrounds: {
|
||||
default: "App background",
|
||||
values: [
|
||||
{
|
||||
name: "App background",
|
||||
value: "#15171A",
|
||||
},
|
||||
],
|
||||
},
|
||||
layout: "fullscreen",
|
||||
},
|
||||
decorators: [
|
||||
(Story) => {
|
||||
const RemixStub = createRemixStub([
|
||||
{
|
||||
path: "/*",
|
||||
action: () => ({ redirect: "/" }),
|
||||
loader: () => ({ redirect: "/" }),
|
||||
Component: Story,
|
||||
},
|
||||
]);
|
||||
|
||||
return (
|
||||
<OperatingSystemContextProvider platform="mac">
|
||||
<LocaleContextProvider locales={window.navigator.languages as string[]}>
|
||||
<RemixStub initialEntries={["/"]} />
|
||||
</LocaleContextProvider>
|
||||
</OperatingSystemContextProvider>
|
||||
);
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default preview;
|
||||
@@ -12,10 +12,30 @@ import { cn } from "~/utils/cn";
|
||||
|
||||
type TaskIconProps = {
|
||||
name: string | undefined;
|
||||
spanName: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function RunIcon({ name, className }: TaskIconProps) {
|
||||
type SpanNameIcons = {
|
||||
matcher: RegExp;
|
||||
iconName: string;
|
||||
};
|
||||
|
||||
const spanNameIcons: SpanNameIcons[] = [{ matcher: /^prisma:/, iconName: "brand-prisma" }];
|
||||
|
||||
export function RunIcon({ name, className, spanName }: TaskIconProps) {
|
||||
const spanNameIcon = spanNameIcons.find(({ matcher }) => matcher.test(spanName));
|
||||
|
||||
if (spanNameIcon) {
|
||||
return (
|
||||
<NamedIcon
|
||||
name={spanNameIcon.iconName}
|
||||
className={cn(className)}
|
||||
fallback={<InformationCircleIcon className={cn(className, "text-text-dimmed")} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!name) return <Squares2X2Icon className={cn(className, "text-text-dimmed")} />;
|
||||
|
||||
switch (name) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { H } from "@highlight-run/node";
|
||||
import {
|
||||
createReadableStreamFromReadable,
|
||||
type DataFunctionArgs,
|
||||
@@ -165,10 +164,6 @@ function handleBrowserRequest(
|
||||
});
|
||||
}
|
||||
|
||||
if (env.HIGHLIGHT_PROJECT_ID) {
|
||||
H.init({ projectID: env.HIGHLIGHT_PROJECT_ID });
|
||||
}
|
||||
|
||||
export function handleError(error: unknown, { request, params, context }: DataFunctionArgs) {
|
||||
logError(error, request);
|
||||
}
|
||||
@@ -178,18 +173,6 @@ Worker.init().catch((error) => {
|
||||
});
|
||||
|
||||
function logError(error: unknown, request?: Request) {
|
||||
if (env.HIGHLIGHT_PROJECT_ID) {
|
||||
const parsed = request ? H.parseHeaders(Object.fromEntries(request.headers)) : undefined;
|
||||
if (error instanceof Error) {
|
||||
H.consumeError(error, parsed?.secureSessionId, parsed?.requestId);
|
||||
} else {
|
||||
H.consumeError(
|
||||
new Error(`Unknown error: ${JSON.stringify(error)}`),
|
||||
parsed?.secureSessionId,
|
||||
parsed?.requestId
|
||||
);
|
||||
}
|
||||
}
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ErrorBoundary as HighlightErrorBoundary } from "@highlight-run/react";
|
||||
import type { LinksFunction, LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
|
||||
import type { ShouldRevalidateFunction } from "@remix-run/react";
|
||||
import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration } from "@remix-run/react";
|
||||
@@ -111,9 +110,7 @@ function App() {
|
||||
<Links />
|
||||
</head>
|
||||
<body className="bg-darkBackground h-full overflow-hidden">
|
||||
<HighlightErrorBoundary>
|
||||
<Outlet />
|
||||
</HighlightErrorBoundary>
|
||||
<Outlet />
|
||||
<Toast />
|
||||
<ScrollRestoration />
|
||||
<ExternalScripts />
|
||||
|
||||
+5
-1
@@ -60,7 +60,11 @@ export default function Page() {
|
||||
>
|
||||
<div className="mx-3 flex items-center justify-between gap-2 border-b border-grid-dimmed">
|
||||
<div className="flex items-center gap-1 overflow-x-hidden">
|
||||
<RunIcon name={event.style?.icon} className="h-4 min-h-4 w-4 min-w-4" />
|
||||
<RunIcon
|
||||
name={event.style?.icon}
|
||||
spanName={event.message}
|
||||
className="h-4 min-h-4 w-4 min-w-4"
|
||||
/>
|
||||
<Header2 className={cn("whitespace-nowrap")}>
|
||||
<SpanTitle {...event} size="large" />
|
||||
</Header2>
|
||||
|
||||
+5
-1
@@ -362,7 +362,11 @@ function TasksTreeView({
|
||||
|
||||
<div className="flex w-full items-center justify-between gap-2 pl-1">
|
||||
<div className="flex items-center gap-2 overflow-x-hidden">
|
||||
<RunIcon name={node.data.style?.icon} className="h-4 min-h-4 w-4 min-w-4" />
|
||||
<RunIcon
|
||||
name={node.data.style?.icon}
|
||||
spanName={node.data.message}
|
||||
className="h-4 min-h-4 w-4 min-w-4"
|
||||
/>
|
||||
<NodeText node={node} />
|
||||
{node.data.isRoot && <Badge variant="outline-rounded">Root</Badge>}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CreateBackgroundWorkerRequestBody, TaskResource } from "@trigger.dev/core/v3";
|
||||
import type { BackgroundWorker } from "@trigger.dev/database";
|
||||
import { PrismaClientOrTransaction } from "~/db.server";
|
||||
import { Prisma, PrismaClientOrTransaction } from "~/db.server";
|
||||
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { generateFriendlyId } from "../friendlyIdentifiers";
|
||||
@@ -89,46 +89,83 @@ export async function createBackgroundTasks(
|
||||
prisma: PrismaClientOrTransaction
|
||||
) {
|
||||
for (const task of tasks) {
|
||||
await prisma.backgroundWorkerTask.create({
|
||||
data: {
|
||||
friendlyId: generateFriendlyId("task"),
|
||||
projectId: worker.projectId,
|
||||
runtimeEnvironmentId: worker.runtimeEnvironmentId,
|
||||
workerId: worker.id,
|
||||
slug: task.id,
|
||||
filePath: task.filePath,
|
||||
exportName: task.exportName,
|
||||
retryConfig: task.retry,
|
||||
queueConfig: task.queue,
|
||||
},
|
||||
});
|
||||
|
||||
const queueName = task.queue?.name ?? `task/${task.id}`;
|
||||
|
||||
const taskQueue = await prisma.taskQueue.upsert({
|
||||
where: {
|
||||
runtimeEnvironmentId_name: {
|
||||
try {
|
||||
await prisma.backgroundWorkerTask.create({
|
||||
data: {
|
||||
friendlyId: generateFriendlyId("task"),
|
||||
projectId: worker.projectId,
|
||||
runtimeEnvironmentId: worker.runtimeEnvironmentId,
|
||||
name: queueName,
|
||||
workerId: worker.id,
|
||||
slug: task.id,
|
||||
filePath: task.filePath,
|
||||
exportName: task.exportName,
|
||||
retryConfig: task.retry,
|
||||
queueConfig: task.queue,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
concurrencyLimit: task.queue?.concurrencyLimit,
|
||||
rateLimit: task.queue?.rateLimit,
|
||||
},
|
||||
create: {
|
||||
friendlyId: generateFriendlyId("queue"),
|
||||
name: queueName,
|
||||
concurrencyLimit: task.queue?.concurrencyLimit,
|
||||
runtimeEnvironmentId: worker.runtimeEnvironmentId,
|
||||
projectId: worker.projectId,
|
||||
rateLimit: task.queue?.rateLimit,
|
||||
type: task.queue?.name ? "NAMED" : "VIRTUAL",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
if (taskQueue.concurrencyLimit) {
|
||||
await marqs?.updateQueueConcurrency(env, taskQueue.name, taskQueue.concurrencyLimit);
|
||||
const queueName = task.queue?.name ?? `task/${task.id}`;
|
||||
|
||||
const taskQueue = await prisma.taskQueue.upsert({
|
||||
where: {
|
||||
runtimeEnvironmentId_name: {
|
||||
runtimeEnvironmentId: worker.runtimeEnvironmentId,
|
||||
name: queueName,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
concurrencyLimit: task.queue?.concurrencyLimit,
|
||||
rateLimit: task.queue?.rateLimit,
|
||||
},
|
||||
create: {
|
||||
friendlyId: generateFriendlyId("queue"),
|
||||
name: queueName,
|
||||
concurrencyLimit: task.queue?.concurrencyLimit,
|
||||
runtimeEnvironmentId: worker.runtimeEnvironmentId,
|
||||
projectId: worker.projectId,
|
||||
rateLimit: task.queue?.rateLimit,
|
||||
type: task.queue?.name ? "NAMED" : "VIRTUAL",
|
||||
},
|
||||
});
|
||||
|
||||
if (taskQueue.concurrencyLimit) {
|
||||
await marqs?.updateQueueConcurrency(env, taskQueue.name, taskQueue.concurrencyLimit);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
// The error code for unique constraint violation in Prisma is P2002
|
||||
if (error.code === "P2002") {
|
||||
logger.warn("Task already exists", {
|
||||
task,
|
||||
worker,
|
||||
});
|
||||
} else {
|
||||
logger.error("Prisma Error creating background worker task", {
|
||||
error: {
|
||||
code: error.code,
|
||||
message: error.message,
|
||||
},
|
||||
task,
|
||||
worker,
|
||||
});
|
||||
}
|
||||
} else if (error instanceof Error) {
|
||||
logger.error("Error creating background worker task", {
|
||||
error: {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
},
|
||||
task,
|
||||
worker,
|
||||
});
|
||||
} else {
|
||||
logger.error("Unknown error creating background worker task", {
|
||||
error,
|
||||
task,
|
||||
worker,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+17
-35
@@ -19,9 +19,7 @@
|
||||
"generate:sourcemaps": "remix build --sourcemap",
|
||||
"clean:sourcemaps": "run-s clean:sourcemaps:*",
|
||||
"clean:sourcemaps:public": "rimraf ./build/**/*.map",
|
||||
"clean:sourcemaps:build": "rimraf ./public/build/**/*.map",
|
||||
"storybook": "storybook dev -p 6006 --no-open",
|
||||
"build-storybook": "storybook build"
|
||||
"clean:sourcemaps:build": "rimraf ./public/build/**/*.map"
|
||||
},
|
||||
"eslintIgnore": [
|
||||
"/node_modules",
|
||||
@@ -44,24 +42,22 @@
|
||||
"@depot/sdk-node": "^0.5.0",
|
||||
"@headlessui/react": "^1.7.8",
|
||||
"@heroicons/react": "^2.0.12",
|
||||
"@highlight-run/node": "^3.1.0",
|
||||
"@highlight-run/react": "^3.2.0",
|
||||
"@internationalized/date": "^3.5.1",
|
||||
"@lezer/highlight": "^1.1.6",
|
||||
"@opentelemetry/api": "^1.7.0",
|
||||
"@opentelemetry/core": "^1.21.0",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.48.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.48.0",
|
||||
"@opentelemetry/instrumentation": "^0.48.0",
|
||||
"@opentelemetry/instrumentation-express": "^0.35.0",
|
||||
"@opentelemetry/instrumentation-http": "^0.48.0",
|
||||
"@opentelemetry/resources": "^1.21.0",
|
||||
"@opentelemetry/sdk-logs": "^0.48.0",
|
||||
"@opentelemetry/sdk-node": "^0.48.0",
|
||||
"@opentelemetry/sdk-trace-base": "^1.21.0",
|
||||
"@opentelemetry/sdk-trace-node": "^1.21.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.21.0",
|
||||
"@prisma/instrumentation": "^5.9.1",
|
||||
"@opentelemetry/api": "^1.8.0",
|
||||
"@opentelemetry/core": "^1.22.0",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.49.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.49.1",
|
||||
"@opentelemetry/instrumentation": "^0.49.1",
|
||||
"@opentelemetry/instrumentation-express": "^0.36.1",
|
||||
"@opentelemetry/instrumentation-http": "^0.49.1",
|
||||
"@opentelemetry/resources": "^1.22.0",
|
||||
"@opentelemetry/sdk-logs": "^0.49.1",
|
||||
"@opentelemetry/sdk-node": "^0.49.1",
|
||||
"@opentelemetry/sdk-trace-base": "^1.22.0",
|
||||
"@opentelemetry/sdk-trace-node": "^1.22.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.22.0",
|
||||
"@prisma/instrumentation": "^5.11.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.3",
|
||||
"@radix-ui/react-label": "^2.0.1",
|
||||
@@ -165,17 +161,6 @@
|
||||
"@remix-run/dev": "2.1.0",
|
||||
"@remix-run/eslint-config": "2.1.0",
|
||||
"@remix-run/testing": "^2.1.0",
|
||||
"@storybook/addon-backgrounds": "^7.0.7",
|
||||
"@storybook/addon-docs": "^7.0.12",
|
||||
"@storybook/addon-essentials": "^7.0.7",
|
||||
"@storybook/addon-interactions": "^7.0.7",
|
||||
"@storybook/addon-links": "^7.0.7",
|
||||
"@storybook/addon-styling": "^1.0.5",
|
||||
"@storybook/addon-viewport": "^7.0.7",
|
||||
"@storybook/blocks": "^7.0.7",
|
||||
"@storybook/react": "^7.0.7",
|
||||
"@storybook/react-webpack5": "^7.0.7",
|
||||
"@storybook/testing-library": "^0.0.14-next.2",
|
||||
"@swc/core": "^1.3.4",
|
||||
"@swc/helpers": "^0.4.11",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
@@ -194,7 +179,7 @@
|
||||
"@types/node-fetch": "^2.6.2",
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"@types/qs": "^6.9.7",
|
||||
"@types/react": "18.2.17",
|
||||
"@types/react": "18.2.69",
|
||||
"@types/react-collapse": "^5.0.4",
|
||||
"@types/react-dom": "18.2.7",
|
||||
"@types/semver": "^7.3.13",
|
||||
@@ -220,9 +205,6 @@
|
||||
"prettier-plugin-tailwindcss": "^0.3.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"storybook": "^7.0.7",
|
||||
"storybook-addon-designs": "7.0.0-beta.2",
|
||||
"storybook-addon-variants": "^0.2.0",
|
||||
"style-loader": "^3.3.4",
|
||||
"tailwind-scrollbar": "^3.0.1",
|
||||
"tailwindcss": "3.4.1",
|
||||
@@ -233,4 +215,4 @@
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
-16
@@ -106,12 +106,14 @@ export const taskWithFetchRetries = task({
|
||||
//if the Response is a 429 (too many requests), it will retry using the data from the response. A lot of good APIs send these headers.
|
||||
const headersResponse = await retry.fetch("http://my.host/test-headers", {
|
||||
retry: {
|
||||
"429": {
|
||||
strategy: "headers",
|
||||
limitHeader: "x-ratelimit-limit",
|
||||
remainingHeader: "x-ratelimit-remaining",
|
||||
resetHeader: "x-ratelimit-reset",
|
||||
resetFormat: "unix_timestamp_in_ms",
|
||||
byStatus: {
|
||||
"429": {
|
||||
strategy: "headers",
|
||||
limitHeader: "x-ratelimit-limit",
|
||||
remainingHeader: "x-ratelimit-remaining",
|
||||
resetHeader: "x-ratelimit-reset",
|
||||
resetFormat: "unix_timestamp_in_ms",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -121,13 +123,15 @@ export const taskWithFetchRetries = task({
|
||||
//if the Response is a 500-599 (issue with the server you're calling), it will retry up to 10 times with exponential backoff
|
||||
const backoffResponse = await retry.fetch("http://my.host/test-backoff", {
|
||||
retry: {
|
||||
"500-599": {
|
||||
strategy: "backoff",
|
||||
maxAttempts: 10,
|
||||
factor: 2,
|
||||
minTimeoutInMs: 1_000,
|
||||
maxTimeoutInMs: 30_000,
|
||||
randomize: false,
|
||||
byStatus: {
|
||||
"500-599": {
|
||||
strategy: "backoff",
|
||||
maxAttempts: 10,
|
||||
factor: 2,
|
||||
minTimeoutInMs: 1_000,
|
||||
maxTimeoutInMs: 30_000,
|
||||
randomize: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -136,9 +140,9 @@ export const taskWithFetchRetries = task({
|
||||
|
||||
//You can additionally specify a timeout. In this case if the response takes longer than 1 second, it will retry up to 5 times with exponential backoff
|
||||
const timeoutResponse = await retry.fetch("https://httpbin.org/delay/2", {
|
||||
timeout: {
|
||||
durationInMs: 1000,
|
||||
retry: {
|
||||
timeoutInMs: 1000,
|
||||
retry: {
|
||||
timeout: {
|
||||
maxAttempts: 5,
|
||||
factor: 1.8,
|
||||
minTimeoutInMs: 500,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "trigger.dev",
|
||||
"version": "1.0.7",
|
||||
"version": "2.3.18",
|
||||
"description": "A Command-Line Interface for Trigger.dev (v3) projects",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -35,7 +35,7 @@
|
||||
"trigger.dev": "./dist/index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@trigger.dev/core": "workspace:*",
|
||||
"@trigger.dev/core-apps": "workspace:*",
|
||||
"@trigger.dev/tsconfig": "workspace:*",
|
||||
"@types/gradient-string": "^1.1.2",
|
||||
"@types/jsonlines": "^0.1.5",
|
||||
@@ -75,27 +75,21 @@
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@baselime/node-opentelemetry": "^0.4.6",
|
||||
"@clack/prompts": "^0.7.0",
|
||||
"@depot/cli": "0.0.1-cli.2.55.0",
|
||||
"@opentelemetry/api": "^1.7.0",
|
||||
"@opentelemetry/api-logs": "^0.48.0",
|
||||
"@opentelemetry/auto-instrumentations-node": "^0.40.3",
|
||||
"@opentelemetry/exporter-collector": "^0.25.0",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.48.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.48.0",
|
||||
"@opentelemetry/instrumentation": "^0.48.0",
|
||||
"@opentelemetry/instrumentation-fetch": "^0.48.0",
|
||||
"@opentelemetry/instrumentation-http": "^0.48.0",
|
||||
"@opentelemetry/resources": "^1.21.0",
|
||||
"@opentelemetry/sdk-logs": "^0.48.0",
|
||||
"@opentelemetry/sdk-node": "^0.48.0",
|
||||
"@opentelemetry/sdk-trace-base": "^1.21.0",
|
||||
"@opentelemetry/sdk-trace-node": "^1.21.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.21.0",
|
||||
"@traceloop/instrumentation-openai": "^0.3.9",
|
||||
"@trigger.dev/core": "workspace:*",
|
||||
"@trigger.dev/core-apps": "workspace:*",
|
||||
"@opentelemetry/api": "^1.8.0",
|
||||
"@opentelemetry/api-logs": "^0.49.1",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.49.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.49.1",
|
||||
"@opentelemetry/instrumentation": "^0.49.1",
|
||||
"@opentelemetry/instrumentation-fetch": "^0.49.1",
|
||||
"@opentelemetry/resources": "^1.22.0",
|
||||
"@opentelemetry/sdk-logs": "^0.49.1",
|
||||
"@opentelemetry/sdk-node": "^0.49.1",
|
||||
"@opentelemetry/sdk-trace-base": "^1.22.0",
|
||||
"@opentelemetry/sdk-trace-node": "^1.22.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.22.0",
|
||||
"@trigger.dev/core": "workspace:^2.3.18",
|
||||
"@types/degit": "^2.8.3",
|
||||
"chalk": "^5.2.0",
|
||||
"chokidar": "^3.5.3",
|
||||
@@ -107,6 +101,7 @@
|
||||
"evt": "^2.4.13",
|
||||
"execa": "^8.0.0",
|
||||
"find-up": "^7.0.0",
|
||||
"glob": "^10.3.10",
|
||||
"gradient-string": "^2.0.2",
|
||||
"import-meta-resolve": "^4.0.0",
|
||||
"ink": "^4.4.1",
|
||||
@@ -130,6 +125,7 @@
|
||||
"supports-color": "^9.4.0",
|
||||
"terminal-link": "^3.0.0",
|
||||
"tiny-invariant": "^1.2.0",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"update-check": "^1.5.4",
|
||||
"url": "^0.11.1",
|
||||
"ws": "^8.12.0",
|
||||
|
||||
@@ -4,16 +4,13 @@ RUN apk add --no-cache dumb-init
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
COPY --chown=node:node package.json package-lock.json ./
|
||||
# copy all the files just in case anything is needed in postinstall
|
||||
COPY --chown=node:node . .
|
||||
RUN npm ci --no-fund --no-audit && npm cache clean --force
|
||||
|
||||
# Development or production stage builds upon the base stage
|
||||
FROM base AS final
|
||||
|
||||
# Copy the rest of the application
|
||||
COPY --chown=node:node . .
|
||||
|
||||
# Use ARG for build-time variables
|
||||
ARG TRIGGER_PROJECT_ID
|
||||
ARG TRIGGER_DEPLOYMENT_ID
|
||||
|
||||
@@ -15,7 +15,7 @@ import { resolve as importResolve } from "import-meta-resolve";
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { dirname, join, relative } from "node:path";
|
||||
import { setTimeout } from "node:timers/promises";
|
||||
import terminalLink from "terminal-link";
|
||||
import invariant from "tiny-invariant";
|
||||
@@ -34,15 +34,23 @@ import {
|
||||
import { readConfig } from "../utilities/configFiles.js";
|
||||
import { createTempDir, readJSONFile, writeJSONFile } from "../utilities/fileSystem";
|
||||
import { printStandloneInitialBanner } from "../utilities/initialBanner.js";
|
||||
import { detectPackageNameFromImportPath, parsePackageName } from "../utilities/installPackages";
|
||||
import {
|
||||
detectPackageNameFromImportPath,
|
||||
parsePackageName,
|
||||
stripWorkspaceFromVersion,
|
||||
} from "../utilities/installPackages";
|
||||
import { logger } from "../utilities/logger.js";
|
||||
import { createTaskFileImports, gatherTaskFiles } from "../utilities/taskFiles";
|
||||
import { login } from "./login";
|
||||
import { SetOptional } from "type-fest";
|
||||
|
||||
import type { SetOptional } from "type-fest";
|
||||
import { bundleDependenciesPlugin, workerSetupImportConfigPlugin } from "../utilities/build";
|
||||
import { Glob } from "glob";
|
||||
|
||||
const DeployCommandOptions = CommonCommandOptions.extend({
|
||||
skipTypecheck: z.boolean().default(false),
|
||||
skipDeploy: z.boolean().default(false),
|
||||
ignoreEnvVarCheck: z.boolean().default(false),
|
||||
env: z.enum(["prod", "staging"]),
|
||||
loadImage: z.boolean().default(false),
|
||||
buildPlatform: z.enum(["linux/amd64", "linux/arm64"]).default("linux/amd64"),
|
||||
@@ -68,7 +76,11 @@ export function configureDeployCommand(program: Command) {
|
||||
"Deploy to a specific environment (currently only prod and staging are supported)",
|
||||
"prod"
|
||||
)
|
||||
.option("-T, --skip-typecheck", "Whether to skip the pre-build typecheck")
|
||||
.option("--skip-typecheck", "Whether to skip the pre-build typecheck")
|
||||
.option(
|
||||
"--ignore-env-var-check",
|
||||
"Detected missing environment variables won't block deployment"
|
||||
)
|
||||
.option("-c, --config <config file>", "The name of the config file, found at [path]")
|
||||
.option(
|
||||
"-p, --project-ref <project ref>",
|
||||
@@ -425,7 +437,11 @@ async function checkEnvVars(
|
||||
environmentVariablesSpinner.stop(
|
||||
`Found missing env vars in ${options.env}: ${arrayToSentence(
|
||||
missingEnvironmentVariables
|
||||
)}. Aborting deployment. ${chalk.bgBlueBright(
|
||||
)}. ${
|
||||
options.ignoreEnvVarCheck
|
||||
? "Continuing deployment because of --ignore-env-var-check. "
|
||||
: "Aborting deployment. "
|
||||
}${chalk.bgBlueBright(
|
||||
terminalLink(
|
||||
"Manage env vars",
|
||||
`${apiUrl}/projects/v3/${config.project}/environment-variables`
|
||||
@@ -437,7 +453,12 @@ async function checkEnvVars(
|
||||
"envVars.missing": missingEnvironmentVariables,
|
||||
});
|
||||
|
||||
throw new SkipLoggingError("Found missing environment variables");
|
||||
if (!options.ignoreEnvVarCheck) {
|
||||
throw new SkipLoggingError("Found missing environment variables");
|
||||
} else {
|
||||
span.end();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
environmentVariablesSpinner.stop(`Environment variable check passed`);
|
||||
@@ -859,7 +880,6 @@ async function compileProject(
|
||||
write: false,
|
||||
minify: false,
|
||||
sourcemap: "external", // does not set the //# sourceMappingURL= comment in the file, we handle it ourselves
|
||||
packages: "external", // https://esbuild.github.io/api/#packages
|
||||
logLevel: "error",
|
||||
platform: "node",
|
||||
format: "cjs", // This is needed to support opentelemetry instrumentation that uses module patching
|
||||
@@ -869,6 +889,7 @@ async function compileProject(
|
||||
TRIGGER_API_URL: `"${config.triggerUrl}"`,
|
||||
__PROJECT_CONFIG__: JSON.stringify(config),
|
||||
},
|
||||
plugins: [bundleDependenciesPlugin(config), workerSetupImportConfigPlugin(configPath)],
|
||||
});
|
||||
|
||||
if (result.errors.length > 0) {
|
||||
@@ -984,17 +1005,29 @@ async function compileProject(
|
||||
|
||||
// Get all the required dependencies from the metaOutputs and save them to /tmp/dir/package.json
|
||||
const allImports = [...metaOutput.imports, ...entryPointMetaOutput.imports];
|
||||
const dependencies = await gatherRequiredDependencies(allImports, config);
|
||||
|
||||
const externalPackageJson = await readJSONFile(join(config.projectDir, "package.json"));
|
||||
|
||||
const dependencies = await gatherRequiredDependencies(
|
||||
allImports,
|
||||
config,
|
||||
externalPackageJson
|
||||
);
|
||||
|
||||
const packageJsonContents = {
|
||||
name: "trigger-worker",
|
||||
version: "0.0.0",
|
||||
description: "",
|
||||
dependencies,
|
||||
scripts: {
|
||||
postinstall: externalPackageJson?.scripts?.postinstall,
|
||||
},
|
||||
};
|
||||
|
||||
await writeJSONFile(join(tempDir, "package.json"), packageJsonContents);
|
||||
|
||||
await copyAdditionalFiles(config, tempDir);
|
||||
|
||||
compileSpinner.stop("Project built successfully");
|
||||
|
||||
const resolvingDependenciesResult = await resolveDependencies(
|
||||
@@ -1112,7 +1145,7 @@ async function resolveDependencies(
|
||||
logger.debug(`No cached package-lock.json found for ${digest}`);
|
||||
|
||||
try {
|
||||
await execa("npm", ["install", "--package-lock-only"], {
|
||||
await execa("npm", ["install", "--package-lock-only", "--ignore-scripts", "--no-audit"], {
|
||||
cwd: projectDir,
|
||||
stdio: logger.loggerLevel === "debug" ? "inherit" : "pipe",
|
||||
});
|
||||
@@ -1207,10 +1240,9 @@ async function typecheckProject(config: ResolvedConfig, options: DeployCommandOp
|
||||
// Returns the dependency names and the version to use (taken from the CLI deps package.json)
|
||||
async function gatherRequiredDependencies(
|
||||
imports: Metafile["outputs"][string]["imports"],
|
||||
config: ResolvedConfig
|
||||
config: ResolvedConfig,
|
||||
projectPackageJson: any
|
||||
) {
|
||||
const externalPackageJson = await readJSONFile(join(config.projectDir, "package.json"));
|
||||
|
||||
const dependencies: Record<string, string> = {};
|
||||
|
||||
for (const file of imports) {
|
||||
@@ -1224,10 +1256,10 @@ async function gatherRequiredDependencies(
|
||||
continue;
|
||||
}
|
||||
|
||||
const externalDependencyVersion = (externalPackageJson?.dependencies ?? {})[packageName];
|
||||
const externalDependencyVersion = (projectPackageJson?.dependencies ?? {})[packageName];
|
||||
|
||||
if (externalDependencyVersion) {
|
||||
dependencies[packageName] = externalDependencyVersion;
|
||||
dependencies[packageName] = stripWorkspaceFromVersion(externalDependencyVersion);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1236,7 +1268,7 @@ async function gatherRequiredDependencies(
|
||||
detectDependencyVersion(packageName);
|
||||
|
||||
if (internalDependencyVersion) {
|
||||
dependencies[packageName] = internalDependencyVersion;
|
||||
dependencies[packageName] = stripWorkspaceFromVersion(internalDependencyVersion);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1253,8 +1285,8 @@ async function gatherRequiredDependencies(
|
||||
continue;
|
||||
} else {
|
||||
const externalDependencyVersion = {
|
||||
...externalPackageJson?.devDependencies,
|
||||
...externalPackageJson?.dependencies,
|
||||
...projectPackageJson?.devDependencies,
|
||||
...projectPackageJson?.dependencies,
|
||||
}[packageName];
|
||||
|
||||
if (externalDependencyVersion) {
|
||||
@@ -1273,6 +1305,56 @@ async function gatherRequiredDependencies(
|
||||
return Object.fromEntries(Object.entries(dependencies).sort(([a], [b]) => a.localeCompare(b)));
|
||||
}
|
||||
|
||||
async function copyAdditionalFiles(config: ResolvedConfig, tempDir: string) {
|
||||
const additionalFiles = config.additionalFiles ?? [];
|
||||
|
||||
if (additionalFiles.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
return await tracer.startActiveSpan(
|
||||
"copyAdditionalFiles",
|
||||
{
|
||||
attributes: {
|
||||
"config.additionalFiles": additionalFiles,
|
||||
},
|
||||
},
|
||||
async (span) => {
|
||||
try {
|
||||
logger.debug(`Copying files to ${tempDir}`, {
|
||||
additionalFiles,
|
||||
});
|
||||
|
||||
const glob = new Glob(additionalFiles, {
|
||||
withFileTypes: true,
|
||||
ignore: ["node_modules"],
|
||||
cwd: config.projectDir,
|
||||
nodir: true,
|
||||
});
|
||||
|
||||
for await (const file of glob) {
|
||||
const relativeDestinationPath = join(
|
||||
tempDir,
|
||||
relative(config.projectDir, file.fullpath())
|
||||
);
|
||||
|
||||
logger.debug(`Copying file ${file.fullpath()} to ${relativeDestinationPath}`);
|
||||
await mkdir(dirname(relativeDestinationPath), { recursive: true });
|
||||
await copyFile(file.fullpath(), relativeDestinationPath);
|
||||
}
|
||||
|
||||
span.end();
|
||||
} catch (error) {
|
||||
recordSpanException(span, error);
|
||||
|
||||
span.end();
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function ensureLoggedIntoDockerRegistry(
|
||||
registryHost: string,
|
||||
auth: { username: string; password: string }
|
||||
@@ -1300,6 +1382,8 @@ async function findAllEnvironmentVariableReferencesInFile(filePath: string) {
|
||||
return findAllEnvironmentVariableReferences(fileContents);
|
||||
}
|
||||
|
||||
const IGNORED_ENV_VARS = ["NODE_ENV", "SHELL", "HOME", "PWD", "LOGNAME", "USER", "PATH"];
|
||||
|
||||
function findAllEnvironmentVariableReferences(code: string): string[] {
|
||||
const regex = /\bprocess\.env\.([a-zA-Z_][a-zA-Z0-9_]*)\b/g;
|
||||
|
||||
@@ -1307,8 +1391,10 @@ function findAllEnvironmentVariableReferences(code: string): string[] {
|
||||
|
||||
const matchesArray = Array.from(matches, (match) => match[1]).filter(Boolean) as string[];
|
||||
|
||||
const filteredMatches = matchesArray.filter((match) => !IGNORED_ENV_VARS.includes(match));
|
||||
|
||||
// Make sure and remove duplicates
|
||||
return Array.from(new Set(matchesArray));
|
||||
return Array.from(new Set(filteredMatches));
|
||||
}
|
||||
|
||||
function arrayToSentence(items: string[]): string {
|
||||
|
||||
@@ -13,7 +13,7 @@ import { watch } from "chokidar";
|
||||
import { Command } from "commander";
|
||||
import { BuildContext, Metafile, context } from "esbuild";
|
||||
import { resolve as importResolve } from "import-meta-resolve";
|
||||
import { Box, Text, render, useApp, useInput } from "ink";
|
||||
import { render, useInput } from "ink";
|
||||
import { createHash } from "node:crypto";
|
||||
import fs, { readFileSync } from "node:fs";
|
||||
import { ClientRequestArgs } from "node:http";
|
||||
@@ -26,10 +26,16 @@ import { z } from "zod";
|
||||
import * as packageJson from "../../package.json";
|
||||
import { CliApiClient } from "../apiClient";
|
||||
import { CommonCommandOptions, commonOptions, wrapCommandAction } from "../cli/common.js";
|
||||
import { bundleDependenciesPlugin, workerSetupImportConfigPlugin } from "../utilities/build";
|
||||
import { chalkPurple } from "../utilities/colors";
|
||||
import { readConfig } from "../utilities/configFiles";
|
||||
import { readJSONFile } from "../utilities/fileSystem";
|
||||
import { printStandloneInitialBanner } from "../utilities/initialBanner.js";
|
||||
import { detectPackageNameFromImportPath, parsePackageName } from "../utilities/installPackages";
|
||||
import {
|
||||
detectPackageNameFromImportPath,
|
||||
parsePackageName,
|
||||
stripWorkspaceFromVersion,
|
||||
} from "../utilities/installPackages";
|
||||
import { logger } from "../utilities/logger.js";
|
||||
import { isLoggedIn } from "../utilities/session.js";
|
||||
import { createTaskFileImports, gatherTaskFiles } from "../utilities/taskFiles";
|
||||
@@ -124,7 +130,17 @@ async function startDev(
|
||||
});
|
||||
|
||||
if (!devEnv.success) {
|
||||
throw new Error(devEnv.error);
|
||||
if (devEnv.error === "Project not found") {
|
||||
logger.error(
|
||||
`Project not found: ${config.config.project}. Ensure you are using the correct project ref and CLI profile (use --profile). Currently using the "${options.profile}" profile, which points to ${authorization.apiUrl}`
|
||||
);
|
||||
} else {
|
||||
logger.error(
|
||||
`Failed to initialize dev environment: ${devEnv.error}. Using project ref ${config.config.project}`
|
||||
);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const environmentClient = new CliApiClient(apiUrl, devEnv.data.apiKey);
|
||||
@@ -283,7 +299,7 @@ function useDev({
|
||||
|
||||
async function runBuild() {
|
||||
if (ctx) {
|
||||
await ctx.cancel();
|
||||
// This will stop the watching
|
||||
await ctx.dispose();
|
||||
}
|
||||
|
||||
@@ -336,7 +352,6 @@ function useDev({
|
||||
write: false,
|
||||
minify: false,
|
||||
sourcemap: "external", // does not set the //# sourceMappingURL= comment in the file, we handle it ourselves
|
||||
packages: "external", // https://esbuild.github.io/api/#packages
|
||||
logLevel: "error",
|
||||
platform: "node",
|
||||
format: "cjs", // This is needed to support opentelemetry instrumentation that uses module patching
|
||||
@@ -347,6 +362,8 @@ function useDev({
|
||||
__PROJECT_CONFIG__: JSON.stringify(config),
|
||||
},
|
||||
plugins: [
|
||||
bundleDependenciesPlugin(config),
|
||||
workerSetupImportConfigPlugin(configPath),
|
||||
{
|
||||
name: "trigger.dev v3",
|
||||
setup(build) {
|
||||
@@ -458,6 +475,19 @@ function useDev({
|
||||
throw new Error(`Background Worker started without package version`);
|
||||
}
|
||||
|
||||
// Check for any duplicate task ids
|
||||
const taskIds = taskResources.map((task) => task.id);
|
||||
const duplicateTaskIds = taskIds.filter(
|
||||
(id, index) => taskIds.indexOf(id) !== index
|
||||
);
|
||||
|
||||
if (duplicateTaskIds.length > 0) {
|
||||
logger.error(
|
||||
createDuplicateTaskIdOutputErrorMessage(duplicateTaskIds, taskResources)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const backgroundWorkerBody: CreateBackgroundWorkerRequestBody = {
|
||||
localOnly: true,
|
||||
metadata: {
|
||||
@@ -526,8 +556,9 @@ function useDev({
|
||||
}
|
||||
|
||||
const throttle = pThrottle({
|
||||
limit: 2,
|
||||
limit: 1,
|
||||
interval: 1000,
|
||||
strict: true,
|
||||
});
|
||||
|
||||
const throttledRebuild = throttle(runBuild);
|
||||
@@ -539,7 +570,7 @@ function useDev({
|
||||
}
|
||||
);
|
||||
|
||||
taskFileWatcher.on("add", async (path) => {
|
||||
taskFileWatcher.on("change", async (path) => {
|
||||
throttledRebuild().catch((error) => {
|
||||
logger.error(error);
|
||||
});
|
||||
@@ -588,58 +619,13 @@ function DevUIImp(props: DevProps) {
|
||||
}
|
||||
|
||||
function useHotkeys() {
|
||||
const { exit } = useApp();
|
||||
|
||||
useInput(async (input, key) => {
|
||||
if (key.return) {
|
||||
console.log("");
|
||||
return;
|
||||
}
|
||||
switch (input.toLowerCase()) {
|
||||
// clear console
|
||||
case "c":
|
||||
console.clear();
|
||||
// This console.log causes Ink to re-render the `DevSession` component.
|
||||
// Couldn't find a better way to tell it to do so...
|
||||
console.log();
|
||||
break;
|
||||
// open browser
|
||||
case "b": {
|
||||
break;
|
||||
}
|
||||
// toggle inspector
|
||||
// case "d": {
|
||||
// if (inspect) {
|
||||
// await openInspector(inspectorPort, props.worker);
|
||||
// }
|
||||
// break;
|
||||
// }
|
||||
|
||||
// shut down
|
||||
case "q":
|
||||
case "x":
|
||||
exit();
|
||||
break;
|
||||
default:
|
||||
// nothing?
|
||||
break;
|
||||
}
|
||||
});
|
||||
useInput(async (input, key) => {});
|
||||
}
|
||||
|
||||
function HotKeys() {
|
||||
useHotkeys();
|
||||
|
||||
return (
|
||||
<Box borderStyle="round" paddingLeft={1} paddingRight={1}>
|
||||
<Text bold={true}>[b]</Text>
|
||||
<Text> open a browser, </Text>
|
||||
<Text bold={true}>[c]</Text>
|
||||
<Text> clear console, </Text>
|
||||
<Text bold={true}>[x]</Text>
|
||||
<Text> to exit</Text>
|
||||
</Box>
|
||||
);
|
||||
return <></>;
|
||||
}
|
||||
|
||||
function WebsocketFactory(apiKey: string) {
|
||||
@@ -674,7 +660,7 @@ async function gatherRequiredDependencies(
|
||||
detectDependencyVersion(packageName);
|
||||
|
||||
if (internalDependencyVersion) {
|
||||
dependencies[packageName] = internalDependencyVersion;
|
||||
dependencies[packageName] = stripWorkspaceFromVersion(internalDependencyVersion);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -712,6 +698,23 @@ async function gatherRequiredDependencies(
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
function createDuplicateTaskIdOutputErrorMessage(
|
||||
duplicateTaskIds: Array<string>,
|
||||
taskResources: Array<TaskResource>
|
||||
) {
|
||||
const duplicateTable = duplicateTaskIds
|
||||
.map((id) => {
|
||||
const tasks = taskResources.filter((task) => task.id === id);
|
||||
|
||||
return `id "${chalkPurple(id)}" was found in:\n${tasks
|
||||
.map((task) => `${task.filePath} -> ${task.exportName}`)
|
||||
.join("\n")}`;
|
||||
})
|
||||
.join("\n\n");
|
||||
|
||||
return `Duplicate task ids detected:\n\n${duplicateTable}\n\n`;
|
||||
}
|
||||
|
||||
function gatherProcessEnv() {
|
||||
const env = {
|
||||
NODE_ENV: process.env.NODE_ENV ?? "development",
|
||||
|
||||
@@ -52,10 +52,6 @@ export function configureInitCommand(program: Command) {
|
||||
"-p, --project-ref <project ref>",
|
||||
"The project ref to use when initializing the project"
|
||||
)
|
||||
.option(
|
||||
"-p, --project-ref <project ref>",
|
||||
"The project ref to use when initializing the project"
|
||||
)
|
||||
.option(
|
||||
"-t, --tag <package tag>",
|
||||
"The version of the @trigger.dev/sdk package to install",
|
||||
@@ -145,11 +141,11 @@ async function _initCommand(dir: string, options: InitCommandOptions) {
|
||||
log.info("Skipping package installation");
|
||||
}
|
||||
|
||||
// Create the config file
|
||||
await writeConfigFile(dir, selectedProject, options);
|
||||
|
||||
// Create the trigger dir
|
||||
await createTriggerDir(dir, options);
|
||||
const triggerDir = await createTriggerDir(dir, options);
|
||||
|
||||
// Create the config file
|
||||
await writeConfigFile(dir, selectedProject, options, triggerDir);
|
||||
|
||||
// Add trigger.config.ts to tsconfig.json
|
||||
await addConfigFileToTsConfig(dir, options);
|
||||
@@ -166,7 +162,7 @@ async function _initCommand(dir: string, options: InitCommandOptions) {
|
||||
log.info("Next steps:");
|
||||
log.info(
|
||||
` 1. To start developing, run ${chalk.green(
|
||||
"npx trigger.dev@latest dev"
|
||||
`npx trigger.dev@${options.tag} dev`
|
||||
)} in your project directory`
|
||||
);
|
||||
log.info(` 2. Visit your ${projectDashboard} to view your newly created tasks.`);
|
||||
@@ -189,10 +185,12 @@ async function _initCommand(dir: string, options: InitCommandOptions) {
|
||||
async function createTriggerDir(dir: string, options: InitCommandOptions) {
|
||||
return await tracer.startActiveSpan("createTriggerDir", async (span) => {
|
||||
try {
|
||||
const defaultValue = `${dir}/src/trigger`;
|
||||
|
||||
const location = await text({
|
||||
message: "Where would you like to create the Trigger.dev directory?",
|
||||
defaultValue: `${dir}/src/trigger`,
|
||||
placeholder: `${dir}/src/trigger`,
|
||||
defaultValue: defaultValue,
|
||||
placeholder: defaultValue,
|
||||
});
|
||||
|
||||
if (isCancel(location)) {
|
||||
@@ -238,10 +236,10 @@ async function createTriggerDir(dir: string, options: InitCommandOptions) {
|
||||
log.step(`Created directory at ${location}`);
|
||||
|
||||
span.end();
|
||||
return;
|
||||
return { location, isCustomValue: location !== defaultValue };
|
||||
}
|
||||
|
||||
const exampleFile = resolveInternalFilePath(`./templates/examples/${example}.js`);
|
||||
const exampleFile = resolveInternalFilePath(`./templates/examples/${example}.ts.template`);
|
||||
const outputPath = join(triggerDir, "example.ts");
|
||||
|
||||
await createFileFromTemplate({
|
||||
@@ -255,6 +253,8 @@ async function createTriggerDir(dir: string, options: InitCommandOptions) {
|
||||
log.step(`Created example file at ${relativeOutputPath}`);
|
||||
|
||||
span.end();
|
||||
|
||||
return { location, isCustomValue: location !== defaultValue };
|
||||
} catch (e) {
|
||||
if (!(e instanceof SkipCommandError)) {
|
||||
recordSpanException(span, e);
|
||||
@@ -431,7 +431,8 @@ async function installPackages(dir: string, options: InitCommandOptions) {
|
||||
async function writeConfigFile(
|
||||
dir: string,
|
||||
project: GetProjectResponseBody,
|
||||
options: InitCommandOptions
|
||||
options: InitCommandOptions,
|
||||
triggerDir: { location: string; isCustomValue: boolean }
|
||||
) {
|
||||
return await tracer.startActiveSpan("writeConfigFile", async (span) => {
|
||||
try {
|
||||
@@ -439,7 +440,7 @@ async function writeConfigFile(
|
||||
spnnr.start("Creating config file");
|
||||
|
||||
const projectDir = resolve(process.cwd(), dir);
|
||||
const templatePath = resolveInternalFilePath("./templates/trigger.config.ts");
|
||||
const templatePath = resolveInternalFilePath("./templates/trigger.config.ts.template");
|
||||
const outputPath = join(projectDir, "trigger.config.ts");
|
||||
|
||||
span.setAttributes({
|
||||
@@ -452,6 +453,9 @@ async function writeConfigFile(
|
||||
templatePath,
|
||||
replacements: {
|
||||
projectRef: project.externalRef,
|
||||
triggerDirectoriesOption: triggerDir.isCustomValue
|
||||
? `\n triggerDirectories: ["${triggerDir.location}"],`
|
||||
: "",
|
||||
},
|
||||
outputPath,
|
||||
override: options.overrideConfig,
|
||||
|
||||
@@ -1,20 +1,59 @@
|
||||
import { BaselimeSDK } from "@baselime/node-opentelemetry";
|
||||
import { trace } from "@opentelemetry/api";
|
||||
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
||||
import { registerInstrumentations } from "@opentelemetry/instrumentation";
|
||||
import { Resource, detectResourcesSync, processDetectorSync } from "@opentelemetry/resources";
|
||||
import { NodeTracerProvider, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-node";
|
||||
import { FetchInstrumentation } from "@opentelemetry/instrumentation-fetch";
|
||||
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
|
||||
import { DiagConsoleLogger, DiagLogLevel, diag, trace } from "@opentelemetry/api";
|
||||
import * as packageJson from "../../package.json";
|
||||
|
||||
const sdk = new BaselimeSDK({
|
||||
baselimeKey: "e9f963244f8b092850d42e34a5339b2d5e68070b".split("").reverse().join(""), // this is a joke
|
||||
instrumentations: [new FetchInstrumentation()],
|
||||
service: "cli-v3",
|
||||
serverless: true,
|
||||
});
|
||||
|
||||
function initializeTracing(): NodeTracerProvider | undefined {
|
||||
if (!process.argv.includes("--skip-telemetry")) {
|
||||
return sdk.start();
|
||||
if (process.argv.includes("--skip-telemetry") || process.env.TRIGGER_DEV_SKIP_TELEMETRY) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.env.OTEL_INTERNAL_DIAG_DEBUG) {
|
||||
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);
|
||||
}
|
||||
|
||||
const resource = detectResourcesSync({
|
||||
detectors: [processDetectorSync],
|
||||
}).merge(
|
||||
new Resource({
|
||||
service: "trigger.dev cli v3",
|
||||
})
|
||||
);
|
||||
|
||||
const traceProvider = new NodeTracerProvider({
|
||||
forceFlushTimeoutMillis: 500,
|
||||
resource,
|
||||
spanLimits: {
|
||||
attributeCountLimit: 1000,
|
||||
attributeValueLengthLimit: 1000,
|
||||
eventCountLimit: 100,
|
||||
attributePerEventCountLimit: 100,
|
||||
linkCountLimit: 10,
|
||||
attributePerLinkCountLimit: 100,
|
||||
},
|
||||
});
|
||||
|
||||
const spanExporter = new OTLPTraceExporter({
|
||||
url: "https://otel.baselime.io/v1",
|
||||
timeoutMillis: 500,
|
||||
headers: {
|
||||
"x-api-key": "e9f963244f8b092850d42e34a5339b2d5e68070b".split("").reverse().join(""), // this is a joke
|
||||
},
|
||||
});
|
||||
|
||||
const spanProcessor = new SimpleSpanProcessor(spanExporter);
|
||||
|
||||
traceProvider.addSpanProcessor(spanProcessor);
|
||||
traceProvider.register();
|
||||
|
||||
registerInstrumentations({
|
||||
instrumentations: [new FetchInstrumentation()],
|
||||
});
|
||||
|
||||
return traceProvider;
|
||||
}
|
||||
|
||||
export const provider = initializeTracing();
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
import type { ProjectConfig } from "@trigger.dev/core/v3";
|
||||
import type { TriggerConfig } from "@trigger.dev/sdk/v3";
|
||||
|
||||
export const config: ProjectConfig = {
|
||||
export const config: TriggerConfig = {
|
||||
project: "${projectRef}",
|
||||
retries: {
|
||||
enabledInDev: false,
|
||||
@@ -11,5 +11,5 @@ export const config: ProjectConfig = {
|
||||
factor: 2,
|
||||
randomize: true,
|
||||
},
|
||||
},
|
||||
},${triggerDirectoriesOption}
|
||||
};
|
||||
@@ -0,0 +1,198 @@
|
||||
import { ResolvedConfig } from "@trigger.dev/core/v3";
|
||||
import type * as esbuild from "esbuild";
|
||||
import type { Plugin } from "esbuild";
|
||||
import { extname, isAbsolute } from "node:path";
|
||||
import tsConfigPaths from "tsconfig-paths";
|
||||
import { logger } from "./logger";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
export function workerSetupImportConfigPlugin(configPath?: string): Plugin {
|
||||
return {
|
||||
name: "trigger-worker-setup",
|
||||
setup(build) {
|
||||
if (!configPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
build.onLoad({ filter: /worker-setup\.js$/ }, async (args) => {
|
||||
let workerSetupContents = readFileSync(args.path, "utf-8");
|
||||
|
||||
workerSetupContents = workerSetupContents.replace(
|
||||
"__SETUP_IMPORTED_PROJECT_CONFIG__",
|
||||
`import * as setupImportedConfigExports from "${configPath}"; const setupImportedConfig = setupImportedConfigExports.config;`
|
||||
);
|
||||
|
||||
logger.debug("Loading worker setup", {
|
||||
args,
|
||||
workerSetupContents,
|
||||
configPath,
|
||||
});
|
||||
|
||||
return {
|
||||
contents: workerSetupContents,
|
||||
loader: "js",
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function bundleDependenciesPlugin(config: ResolvedConfig): Plugin {
|
||||
const matchPath = config.tsconfigPath ? createMatchPath(config.tsconfigPath) : undefined;
|
||||
|
||||
function resolvePath(id: string) {
|
||||
if (!matchPath) {
|
||||
return id;
|
||||
}
|
||||
return matchPath(id, undefined, undefined, [".ts", ".tsx", ".js", ".jsx"]) || id;
|
||||
}
|
||||
|
||||
return {
|
||||
name: "trigger-bundle-dependencies",
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /.*/ }, (args) => {
|
||||
const resolvedPath = resolvePath(args.path);
|
||||
|
||||
logger.ignore(`Checking if ${args.path} should be bundled or external`, {
|
||||
...args,
|
||||
resolvedPath,
|
||||
});
|
||||
|
||||
if (!isBareModuleId(resolvedPath)) {
|
||||
logger.ignore(`Bundling ${args.path} because its not a bareModuleId`, {
|
||||
...args,
|
||||
});
|
||||
|
||||
return undefined; // let esbuild bundle it
|
||||
}
|
||||
|
||||
if (args.path.startsWith("@trigger.dev/")) {
|
||||
logger.ignore(`Bundling ${args.path} because its a trigger.dev package`, {
|
||||
...args,
|
||||
});
|
||||
|
||||
return undefined; // let esbuild bundle it
|
||||
}
|
||||
|
||||
// Skip assets that are treated as files (.css, .svg, .png, etc.).
|
||||
// Otherwise, esbuild would emit code that would attempt to require()
|
||||
// or import these files --- which aren't JavaScript!
|
||||
let loader;
|
||||
try {
|
||||
loader = getLoaderForFile(args.path);
|
||||
} catch (e) {
|
||||
if (!(e instanceof Error && e.message.startsWith("Cannot get loader for file"))) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
if (loader === "file") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (let pattern of config.dependenciesToBundle ?? []) {
|
||||
if (typeof pattern === "string" ? args.path === pattern : pattern.test(args.path)) {
|
||||
return undefined; // let esbuild bundle it
|
||||
}
|
||||
}
|
||||
|
||||
logger.ignore(`Externalizing ${args.path}`, {
|
||||
...args,
|
||||
});
|
||||
|
||||
// Everything else should be external
|
||||
return {
|
||||
path: args.path,
|
||||
external: true,
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function isBareModuleId(id: string): boolean {
|
||||
return !id.startsWith("node:") && !id.startsWith(".") && !isAbsolute(id);
|
||||
}
|
||||
|
||||
export function createMatchPath(tsconfigPath: string | undefined) {
|
||||
// There is no tsconfig to match paths against.
|
||||
if (!tsconfigPath) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// When passing a absolute path, loadConfig assumes that the path contains
|
||||
// a tsconfig file.
|
||||
// Ref.: https://github.com/dividab/tsconfig-paths/blob/v4.0.0/src/__tests__/config-loader.test.ts#L74
|
||||
let configLoaderResult = tsConfigPaths.loadConfig(tsconfigPath);
|
||||
|
||||
if (configLoaderResult.resultType === "failed") {
|
||||
if (configLoaderResult.message === "Missing baseUrl in compilerOptions") {
|
||||
throw new Error(
|
||||
`🚨 Oops! No baseUrl found, please set compilerOptions.baseUrl in your tsconfig or jsconfig`
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return tsConfigPaths.createMatchPath(
|
||||
configLoaderResult.absoluteBaseUrl,
|
||||
configLoaderResult.paths,
|
||||
configLoaderResult.mainFields,
|
||||
configLoaderResult.addMatchAll
|
||||
);
|
||||
}
|
||||
|
||||
const loaders: { [ext: string]: esbuild.Loader } = {
|
||||
".aac": "file",
|
||||
".avif": "file",
|
||||
".css": "file",
|
||||
".csv": "file",
|
||||
".eot": "file",
|
||||
".fbx": "file",
|
||||
".flac": "file",
|
||||
".gif": "file",
|
||||
".glb": "file",
|
||||
".gltf": "file",
|
||||
".gql": "text",
|
||||
".graphql": "text",
|
||||
".hdr": "file",
|
||||
".ico": "file",
|
||||
".jpeg": "file",
|
||||
".jpg": "file",
|
||||
".js": "jsx",
|
||||
".jsx": "jsx",
|
||||
".json": "json",
|
||||
// We preprocess md and mdx files using @mdx-js/mdx and send through
|
||||
// the JSX for esbuild to handle
|
||||
".md": "jsx",
|
||||
".mdx": "jsx",
|
||||
".mov": "file",
|
||||
".mp3": "file",
|
||||
".mp4": "file",
|
||||
".node": "copy",
|
||||
".ogg": "file",
|
||||
".otf": "file",
|
||||
".png": "file",
|
||||
".psd": "file",
|
||||
".sql": "text",
|
||||
".svg": "file",
|
||||
".ts": "ts",
|
||||
".tsx": "tsx",
|
||||
".ttf": "file",
|
||||
".wasm": "file",
|
||||
".wav": "file",
|
||||
".webm": "file",
|
||||
".webmanifest": "file",
|
||||
".webp": "file",
|
||||
".woff": "file",
|
||||
".woff2": "file",
|
||||
".zip": "file",
|
||||
};
|
||||
|
||||
export function getLoaderForFile(file: string): esbuild.Loader {
|
||||
const ext = extname(file);
|
||||
const loader = loaders[ext];
|
||||
|
||||
if (loader) return loader;
|
||||
|
||||
throw new Error(`Cannot get loader for file ${file}`);
|
||||
}
|
||||
@@ -131,7 +131,7 @@ export async function readConfig(
|
||||
|
||||
const tempDir = await createTempDir();
|
||||
|
||||
const builtConfigFilePath = join(tempDir, "config.mjs");
|
||||
const builtConfigFilePath = join(tempDir, "config.js");
|
||||
const builtConfigFileHref = pathToFileURL(builtConfigFilePath).href;
|
||||
|
||||
logger.debug("Building config file", {
|
||||
@@ -147,7 +147,7 @@ export async function readConfig(
|
||||
metafile: true,
|
||||
minify: false,
|
||||
write: true,
|
||||
format: "esm",
|
||||
format: "cjs",
|
||||
platform: "node",
|
||||
target: ["es2018", "node18"],
|
||||
outfile: builtConfigFilePath,
|
||||
@@ -183,6 +183,10 @@ export async function resolveConfig(path: string, config: Config): Promise<Resol
|
||||
config.projectDir = path;
|
||||
}
|
||||
|
||||
if (!config.tsconfigPath) {
|
||||
config.tsconfigPath = await getConfigPath(path, "tsconfig.json");
|
||||
}
|
||||
|
||||
return config as ResolvedConfig;
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,18 @@ export function detectPackageNameFromImportPath(path: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the workspace prefix from a version string.
|
||||
* @param version - The version string to strip the workspace prefix from.
|
||||
* @returns The version string without the workspace prefix.
|
||||
* @example
|
||||
* stripWorkspaceFromVersion("workspace:1.0.0") // "1.0.0"
|
||||
* stripWorkspaceFromVersion("1.0.0") // "1.0.0"
|
||||
*/
|
||||
export function stripWorkspaceFromVersion(version: string) {
|
||||
return version.replace(/^workspace:/, "");
|
||||
}
|
||||
|
||||
export function parsePackageName(packageSpecifier: string): { name: string; version?: string } {
|
||||
const parts = packageSpecifier.split("@");
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ export class Logger {
|
||||
columns = process.stdout.columns;
|
||||
|
||||
debug = (...args: unknown[]) => this.doLog("debug", args);
|
||||
ignore = (...args: unknown[]) => {};
|
||||
debugWithSanitization = (label: string, ...args: unknown[]) => {
|
||||
this.doLog("debug", [label, ...args]);
|
||||
};
|
||||
|
||||
@@ -30,7 +30,7 @@ export async function gatherTaskFiles(config: ResolvedConfig): Promise<Array<Tas
|
||||
|
||||
const filePath = relative(config.projectDir, fullPath);
|
||||
const importPath = filePath.replace(/\.(js|ts)$/, "");
|
||||
const importName = importPath.replace(/\//g, "_");
|
||||
const importName = importPath.replace(/\//g, "_").replace(/\./g, "_").replace(/-/g, "_");
|
||||
|
||||
taskFiles.push({ triggerDir, importPath, importName, filePath });
|
||||
}
|
||||
|
||||
@@ -364,6 +364,7 @@ export class BackgroundWorker {
|
||||
|
||||
if (!this._taskRunProcesses.has(payload.execution.run.id)) {
|
||||
const taskRunProcess = new TaskRunProcess(
|
||||
payload.execution.run.id,
|
||||
this.path,
|
||||
{
|
||||
...this.params.env,
|
||||
@@ -520,6 +521,7 @@ class TaskRunProcess {
|
||||
public onExit: Evt<number> = new Evt();
|
||||
|
||||
constructor(
|
||||
private runId: string,
|
||||
private path: string,
|
||||
private env: NodeJS.ProcessEnv,
|
||||
private metadata: BackgroundWorkerProperties,
|
||||
@@ -529,7 +531,7 @@ class TaskRunProcess {
|
||||
schema: workerToChildMessages,
|
||||
sender: async (message) => {
|
||||
if (this._child?.connected && !this._isBeingKilled && !this._child.killed) {
|
||||
this._child?.send?.(message);
|
||||
this._child.send(message);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -542,10 +544,9 @@ class TaskRunProcess {
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
logger.debug("initializing task run process", {
|
||||
logger.debug(`[${this.runId}] initializing task run process`, {
|
||||
env: this.env,
|
||||
path: this.path,
|
||||
processEnv: process.env,
|
||||
});
|
||||
|
||||
this._child = fork(this.path, {
|
||||
@@ -575,6 +576,8 @@ class TaskRunProcess {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(`[${this.runId}] cleaning up task run process`, { kill });
|
||||
|
||||
await this._sender.send("CLEANUP", {
|
||||
flush: true,
|
||||
kill,
|
||||
@@ -619,6 +622,13 @@ class TaskRunProcess {
|
||||
return;
|
||||
}
|
||||
|
||||
if (execution.run.id === this.runId) {
|
||||
// We don't need to notify the task run process if it's the same as the one we're running
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(`[${this.runId}] task run completed notification`, { completion, execution });
|
||||
|
||||
this._sender.send("TASK_RUN_COMPLETED_NOTIFICATION", {
|
||||
completion,
|
||||
execution,
|
||||
@@ -669,6 +679,8 @@ class TaskRunProcess {
|
||||
}
|
||||
|
||||
async #handleExit(code: number) {
|
||||
logger.debug(`[${this.runId}] task run process exiting`, { code });
|
||||
|
||||
// Go through all the attempts currently pending and reject them
|
||||
for (const [id, status] of this._attemptStatuses.entries()) {
|
||||
if (status === "PENDING") {
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
type TracingSDK,
|
||||
type HandleErrorFunction,
|
||||
} from "@trigger.dev/core/v3";
|
||||
import "source-map-support/register.js";
|
||||
|
||||
__WORKER_SETUP__;
|
||||
declare const __WORKER_SETUP__: unknown;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import "source-map-support/register.js";
|
||||
import { Resource } from "@opentelemetry/resources";
|
||||
import { OpenAIInstrumentation } from "@traceloop/instrumentation-openai";
|
||||
import {
|
||||
ProjectConfig,
|
||||
SemanticInternalAttributes,
|
||||
TracingDiagnosticLogLevel,
|
||||
TracingSDK,
|
||||
@@ -8,12 +9,16 @@ import {
|
||||
childToWorkerMessages,
|
||||
} from "@trigger.dev/core/v3";
|
||||
|
||||
__SETUP_IMPORTED_PROJECT_CONFIG__;
|
||||
declare const __SETUP_IMPORTED_PROJECT_CONFIG__: unknown;
|
||||
declare const setupImportedConfig: ProjectConfig | undefined;
|
||||
|
||||
export const tracingSDK = new TracingSDK({
|
||||
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "http://0.0.0.0:4318",
|
||||
resource: new Resource({
|
||||
[SemanticInternalAttributes.CLI_VERSION]: "3.0.0",
|
||||
}),
|
||||
instrumentations: [new OpenAIInstrumentation()],
|
||||
instrumentations: setupImportedConfig?.instrumentations ?? [],
|
||||
diagLogLevel: (process.env.OTEL_LOG_LEVEL as TracingDiagnosticLogLevel) ?? "none",
|
||||
});
|
||||
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import { Resource } from "@opentelemetry/resources";
|
||||
import { OpenAIInstrumentation } from "@traceloop/instrumentation-openai";
|
||||
import {
|
||||
ProjectConfig,
|
||||
SemanticInternalAttributes,
|
||||
TracingDiagnosticLogLevel,
|
||||
TracingSDK,
|
||||
ZodMessageSender,
|
||||
childToWorkerMessages,
|
||||
} from "@trigger.dev/core/v3";
|
||||
|
||||
__SETUP_IMPORTED_PROJECT_CONFIG__;
|
||||
declare const __SETUP_IMPORTED_PROJECT_CONFIG__: unknown;
|
||||
declare const setupImportedConfig: ProjectConfig | undefined;
|
||||
|
||||
export const tracingSDK = new TracingSDK({
|
||||
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "http://0.0.0.0:4318",
|
||||
resource: new Resource({
|
||||
[SemanticInternalAttributes.CLI_VERSION]: "3.0.0",
|
||||
}),
|
||||
instrumentations: [new OpenAIInstrumentation()],
|
||||
instrumentations: setupImportedConfig?.instrumentations ?? [],
|
||||
diagLogLevel: (process.env.OTEL_LOG_LEVEL as TracingDiagnosticLogLevel) ?? "none",
|
||||
});
|
||||
|
||||
|
||||
@@ -17,6 +17,6 @@ export default defineConfig({
|
||||
outDir: "dist",
|
||||
onSuccess: isDev ? `${copyTemplates} && node dist/index.js` : copyTemplates,
|
||||
banner: {
|
||||
js: "import { createRequire } from 'module';const require = createRequire(import.meta.url);",
|
||||
js: "import { createRequire as createRequireFromMetaUrl } from 'node:module';const require = createRequireFromMetaUrl(import.meta.url);",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -18,4 +18,5 @@ export default defineConfig({
|
||||
sourcemap: false,
|
||||
target: "esnext",
|
||||
outDir: "dist/workers",
|
||||
noExternal: ["@trigger.dev/core-apps"],
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@trigger.dev/core-apps",
|
||||
"description": "Backend core code used across apps",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -46,6 +46,6 @@
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.7.0"
|
||||
"@opentelemetry/api": "^1.8.0"
|
||||
}
|
||||
}
|
||||
+10
-14
@@ -59,21 +59,17 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@google-cloud/precise-date": "^4.0.0",
|
||||
"@opentelemetry/api": "^1.7.0",
|
||||
"@opentelemetry/api": "^1.8.0",
|
||||
"@opentelemetry/api-logs": "^0.48.0",
|
||||
"@opentelemetry/auto-instrumentations-node": "^0.40.3",
|
||||
"@opentelemetry/exporter-collector": "^0.25.0",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.48.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.48.0",
|
||||
"@opentelemetry/instrumentation": "^0.48.0",
|
||||
"@opentelemetry/instrumentation-fetch": "^0.48.0",
|
||||
"@opentelemetry/instrumentation-http": "^0.48.0",
|
||||
"@opentelemetry/resources": "^1.21.0",
|
||||
"@opentelemetry/sdk-logs": "^0.48.0",
|
||||
"@opentelemetry/sdk-node": "^0.48.0",
|
||||
"@opentelemetry/sdk-trace-base": "^1.21.0",
|
||||
"@opentelemetry/sdk-trace-node": "^1.21.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.21.0",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.49.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.49.1",
|
||||
"@opentelemetry/instrumentation": "^0.49.1",
|
||||
"@opentelemetry/resources": "^1.22.0",
|
||||
"@opentelemetry/sdk-logs": "^0.49.1",
|
||||
"@opentelemetry/sdk-node": "^0.49.1",
|
||||
"@opentelemetry/sdk-trace-base": "^1.22.0",
|
||||
"@opentelemetry/sdk-trace-node": "^1.22.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.22.0",
|
||||
"humanize-duration": "^3.27.3",
|
||||
"socket.io": "^4.7.4",
|
||||
"socket.io-client": "^4.7.4",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Span, SpanStatusCode } from "@opentelemetry/api";
|
||||
|
||||
export { TracingSDK, type TracingSDKConfig, type TracingDiagnosticLogLevel } from "./tracingSDK";
|
||||
export { HttpInstrumentation, FetchInstrumentation } from "./instrumentations";
|
||||
|
||||
export function recordSpanException(span: Span, error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
|
||||
export { FetchInstrumentation } from "@opentelemetry/instrumentation-fetch";
|
||||
@@ -20,6 +20,8 @@ export class DevRuntimeManager implements RuntimeManager {
|
||||
|
||||
_tasks: Map<string, TaskMetadataWithFilePath> = new Map();
|
||||
|
||||
_pendingCompletionNotifications: Map<string, TaskRunExecutionResult> = new Map();
|
||||
|
||||
disable(): void {
|
||||
// do nothing
|
||||
}
|
||||
@@ -47,6 +49,14 @@ export class DevRuntimeManager implements RuntimeManager {
|
||||
}
|
||||
|
||||
async waitForTask(params: { id: string; ctx: TaskRunContext }): Promise<TaskRunExecutionResult> {
|
||||
const pendingCompletion = this._pendingCompletionNotifications.get(params.id);
|
||||
|
||||
if (pendingCompletion) {
|
||||
this._pendingCompletionNotifications.delete(params.id);
|
||||
|
||||
return pendingCompletion;
|
||||
}
|
||||
|
||||
const promise = new Promise<TaskRunExecutionResult>((resolve, reject) => {
|
||||
this._taskWaits.set(params.id, { resolve, reject });
|
||||
});
|
||||
@@ -66,6 +76,20 @@ export class DevRuntimeManager implements RuntimeManager {
|
||||
const promise = Promise.all(
|
||||
params.runs.map((runId) => {
|
||||
return new Promise<TaskRunExecutionResult>((resolve, reject) => {
|
||||
const pendingCompletion = this._pendingCompletionNotifications.get(runId);
|
||||
|
||||
if (pendingCompletion) {
|
||||
this._pendingCompletionNotifications.delete(runId);
|
||||
|
||||
if (pendingCompletion.ok) {
|
||||
resolve(pendingCompletion);
|
||||
} else {
|
||||
reject(pendingCompletion);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this._taskWaits.set(runId, { resolve, reject });
|
||||
});
|
||||
})
|
||||
@@ -83,6 +107,9 @@ export class DevRuntimeManager implements RuntimeManager {
|
||||
const wait = this._taskWaits.get(execution.run.id);
|
||||
|
||||
if (!wait) {
|
||||
// We need to store the completion in case the task is awaited later
|
||||
this._pendingCompletionNotifications.set(execution.run.id, completion);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import { RetryOptions } from "./messages";
|
||||
import { EventFilter } from "./eventFilter";
|
||||
import { Prettify } from "../types";
|
||||
|
||||
export const FetchRetryHeadersStrategy = z.object({
|
||||
/** The `headers` strategy retries the request using info from the response headers. */
|
||||
@@ -45,14 +46,14 @@ export const FetchRetryStrategy = z.discriminatedUnion("strategy", [
|
||||
|
||||
export type FetchRetryStrategy = z.infer<typeof FetchRetryStrategy>;
|
||||
|
||||
export const FetchRetryOptions = z.record(FetchRetryStrategy);
|
||||
export const FetchRetryByStatusOptions = z.record(z.string(), FetchRetryStrategy);
|
||||
|
||||
/** An object where the key is a status code pattern and the value is a retrying strategy. Supported patterns are:
|
||||
- Specific status codes: 429
|
||||
- Ranges: 500-599
|
||||
- Wildcards: 2xx, 3xx, 4xx, 5xx
|
||||
*/
|
||||
export type FetchRetryOptions = z.infer<typeof FetchRetryOptions>;
|
||||
export type FetchRetryByStatusOptions = Prettify<z.infer<typeof FetchRetryByStatusOptions>>;
|
||||
|
||||
export const FetchTimeoutOptions = z.object({
|
||||
/** The maximum time to wait for the request to complete. */
|
||||
@@ -61,3 +62,16 @@ export const FetchTimeoutOptions = z.object({
|
||||
});
|
||||
|
||||
export type FetchTimeoutOptions = z.infer<typeof FetchTimeoutOptions>;
|
||||
|
||||
export const FetchRetryOptions = z.object({
|
||||
/** The retrying strategy for specific status codes. */
|
||||
byStatus: FetchRetryByStatusOptions.optional(),
|
||||
/** The timeout options for the request. */
|
||||
timeout: RetryOptions.optional(),
|
||||
/**
|
||||
* The retrying strategy for connection errors.
|
||||
*/
|
||||
connectionError: RetryOptions.optional(),
|
||||
});
|
||||
|
||||
export type FetchRetryOptions = Prettify<z.infer<typeof FetchRetryOptions>>;
|
||||
|
||||
@@ -10,11 +10,21 @@ import {
|
||||
} from "./messages";
|
||||
import { TaskResource } from "./resources";
|
||||
|
||||
const RegexSchema = z.custom<RegExp>((val) => {
|
||||
try {
|
||||
// Check to see if val is a regex
|
||||
return typeof (val as RegExp).test === "function";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
export const Config = z.object({
|
||||
project: z.string(),
|
||||
triggerDirectories: z.string().array().optional(),
|
||||
triggerUrl: z.string().optional(),
|
||||
projectDir: z.string().optional(),
|
||||
tsconfigPath: z.string().optional(),
|
||||
retries: z
|
||||
.object({
|
||||
enabledInDev: z.boolean().default(true),
|
||||
@@ -22,12 +32,14 @@ export const Config = z.object({
|
||||
})
|
||||
.optional(),
|
||||
additionalPackages: z.string().array().optional(),
|
||||
additionalFiles: z.string().array().optional(),
|
||||
dependenciesToBundle: z.array(z.union([z.string(), RegexSchema])).optional(),
|
||||
});
|
||||
|
||||
export type Config = z.infer<typeof Config>;
|
||||
export type ResolvedConfig = RequireKeys<
|
||||
Config,
|
||||
"triggerDirectories" | "triggerUrl" | "projectDir"
|
||||
"triggerDirectories" | "triggerUrl" | "projectDir" | "tsconfigPath"
|
||||
>;
|
||||
|
||||
export const Machine = z.object({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { RetryOptions } from "../schemas";
|
||||
import type { InstrumentationOption } from "@opentelemetry/instrumentation";
|
||||
|
||||
export interface ProjectConfig {
|
||||
project: string;
|
||||
@@ -9,4 +10,25 @@ export interface ProjectConfig {
|
||||
default?: RetryOptions;
|
||||
};
|
||||
additionalPackages?: string[];
|
||||
|
||||
/**
|
||||
* List of additional files to include in your trigger.dev bundle. e.g. ["./prisma/schema.prisma"]
|
||||
*
|
||||
* Supports glob patterns.
|
||||
*/
|
||||
additionalFiles?: string[];
|
||||
/**
|
||||
* List of patterns that determine if a module is included in your trigger.dev bundle. This is needed when consuming ESM only packages, since the trigger.dev bundle is currently built as a CJS module.
|
||||
*/
|
||||
dependenciesToBundle?: Array<string | RegExp>;
|
||||
|
||||
/**
|
||||
* The path to your project's tsconfig.json file. Will use tsconfig.json in the project directory if not provided.
|
||||
*/
|
||||
tsconfigPath?: string;
|
||||
|
||||
/**
|
||||
* The OpenTelemetry instrumentations to enable
|
||||
*/
|
||||
instrumentations?: InstrumentationOption[];
|
||||
}
|
||||
|
||||
@@ -32,9 +32,9 @@ export function flattenAttributes(
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
if (typeof value[i] === "object" && value[i] !== null) {
|
||||
// update null check here as well
|
||||
Object.assign(result, flattenAttributes(value[i], `${newPrefix}.${i}`));
|
||||
Object.assign(result, flattenAttributes(value[i], `${newPrefix}.[${i}]`));
|
||||
} else {
|
||||
result[`${newPrefix}.${i}`] = value[i];
|
||||
result[`${newPrefix}.[${i}]`] = value[i];
|
||||
}
|
||||
}
|
||||
} else if (isRecord(value)) {
|
||||
@@ -55,46 +55,54 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
}
|
||||
|
||||
export function unflattenAttributes(obj: Attributes): Record<string, unknown> {
|
||||
if (
|
||||
obj === null ||
|
||||
obj === undefined ||
|
||||
typeof obj === "string" ||
|
||||
typeof obj === "number" ||
|
||||
typeof obj === "boolean" ||
|
||||
Array.isArray(obj)
|
||||
) {
|
||||
if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
const result: Record<string, unknown> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const parts = key.split(".");
|
||||
let current = result;
|
||||
const parts = key.split(".").reduce((acc, part) => {
|
||||
// Splitting array indices as separate parts
|
||||
if (detectIsArrayIndex(part)) {
|
||||
acc.push(part);
|
||||
} else {
|
||||
acc.push(...part.split(/\.\[(.*?)\]/).filter(Boolean));
|
||||
}
|
||||
return acc;
|
||||
}, [] as string[]);
|
||||
|
||||
let current: Record<string, unknown> = result;
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
const part = parts[i];
|
||||
|
||||
// Check if part is not undefined and it's a string.
|
||||
if (typeof part === "string") {
|
||||
const nextPart = parts[i + 1];
|
||||
const isArray = nextPart ? parseInt(nextPart, 10).toString() === nextPart : false;
|
||||
if (current[part] == null) {
|
||||
current[part] = isArray ? [] : {};
|
||||
}
|
||||
|
||||
current = current[part] as Record<string, unknown>;
|
||||
const isArray = detectIsArrayIndex(part);
|
||||
const cleanPart = isArray ? part.substring(1, part.length - 1) : part;
|
||||
const nextIsArray = detectIsArrayIndex(parts[i + 1]);
|
||||
if (!current[cleanPart]) {
|
||||
current[cleanPart] = nextIsArray ? [] : {};
|
||||
}
|
||||
current = current[cleanPart] as Record<string, unknown>;
|
||||
}
|
||||
// For the last element, we must ensure we also check if it is not undefined and it's a string.
|
||||
const lastPart = parts[parts.length - 1];
|
||||
if (typeof lastPart === "string") {
|
||||
current[lastPart] = value;
|
||||
}
|
||||
const cleanLastPart = detectIsArrayIndex(lastPart)
|
||||
? parseInt(lastPart.substring(1, lastPart.length - 1), 10)
|
||||
: lastPart;
|
||||
current[cleanLastPart] = value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function detectIsArrayIndex(key: string): boolean {
|
||||
const match = key.match(/^\[(\d+)\]$/);
|
||||
|
||||
if (match) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function primitiveValueOrflattenedAttributes(
|
||||
obj: Record<string, unknown> | Array<unknown> | string | boolean | number | undefined,
|
||||
prefix: string | undefined
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type RetryOptions } from "../schemas";
|
||||
import { calculateResetAt as calculateResetAtInternal } from "../../retry";
|
||||
import { FetchRetryOptions, type RetryOptions } from "../schemas";
|
||||
|
||||
export const defaultRetryOptions = {
|
||||
maxAttempts: 3,
|
||||
@@ -9,6 +9,17 @@ export const defaultRetryOptions = {
|
||||
randomize: true,
|
||||
} satisfies RetryOptions;
|
||||
|
||||
export const defaultFetchRetryOptions = {
|
||||
byStatus: {
|
||||
"429,408,409,5xx": {
|
||||
strategy: "backoff",
|
||||
...defaultRetryOptions,
|
||||
},
|
||||
},
|
||||
connectionError: defaultRetryOptions,
|
||||
timeout: defaultRetryOptions,
|
||||
} satisfies FetchRetryOptions;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param options
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
import { flattenAttributes, unflattenAttributes } from "../src/v3/utils/flattenAttributes";
|
||||
|
||||
describe("flattenAttributes", () => {
|
||||
it("handles null and undefined gracefully", () => {
|
||||
expect(flattenAttributes(null)).toEqual({});
|
||||
expect(flattenAttributes(undefined)).toEqual({});
|
||||
});
|
||||
|
||||
it("flattens string attributes correctly", () => {
|
||||
const result = flattenAttributes("testString");
|
||||
expect(result).toEqual({ "": "testString" });
|
||||
});
|
||||
|
||||
it("flattens number attributes correctly", () => {
|
||||
const result = flattenAttributes(12345);
|
||||
expect(result).toEqual({ "": 12345 });
|
||||
});
|
||||
|
||||
it("flattens boolean attributes correctly", () => {
|
||||
const result = flattenAttributes(true);
|
||||
expect(result).toEqual({ "": true });
|
||||
});
|
||||
|
||||
it("flattens complex objects correctly", () => {
|
||||
const obj = {
|
||||
level1: {
|
||||
level2: {
|
||||
value: "test",
|
||||
},
|
||||
array: [1, 2, 3],
|
||||
},
|
||||
};
|
||||
const expected = {
|
||||
"level1.level2.value": "test",
|
||||
"level1.array.[0]": 1,
|
||||
"level1.array.[1]": 2,
|
||||
"level1.array.[2]": 3,
|
||||
};
|
||||
expect(flattenAttributes(obj)).toEqual(expected);
|
||||
});
|
||||
|
||||
it("applies prefixes correctly", () => {
|
||||
const obj = { key: "value" };
|
||||
const expected = { "prefix.key": "value" };
|
||||
expect(flattenAttributes(obj, "prefix")).toEqual(expected);
|
||||
});
|
||||
|
||||
it("handles arrays of objects correctly", () => {
|
||||
const obj = {
|
||||
array: [{ key: "value" }, { key: "value" }, { key: "value" }],
|
||||
};
|
||||
const expected = {
|
||||
"array.[0].key": "value",
|
||||
"array.[1].key": "value",
|
||||
"array.[2].key": "value",
|
||||
};
|
||||
expect(flattenAttributes(obj)).toEqual(expected);
|
||||
});
|
||||
|
||||
it("handles arrays of objects correctly with prefixing correctly", () => {
|
||||
const obj = {
|
||||
array: [{ key: "value" }, { key: "value" }, { key: "value" }],
|
||||
};
|
||||
const expected = {
|
||||
"prefix.array.[0].key": "value",
|
||||
"prefix.array.[1].key": "value",
|
||||
"prefix.array.[2].key": "value",
|
||||
};
|
||||
expect(flattenAttributes(obj, "prefix")).toEqual(expected);
|
||||
});
|
||||
|
||||
it("handles objects of objects correctly", () => {
|
||||
const obj = {
|
||||
level1: {
|
||||
level2: {
|
||||
key: "value",
|
||||
},
|
||||
},
|
||||
};
|
||||
const expected = { "level1.level2.key": "value" };
|
||||
expect(flattenAttributes(obj)).toEqual(expected);
|
||||
});
|
||||
|
||||
it("handles objects of objects correctly with prefixing", () => {
|
||||
const obj = {
|
||||
level1: {
|
||||
level2: {
|
||||
key: "value",
|
||||
},
|
||||
},
|
||||
};
|
||||
const expected = { "prefix.level1.level2.key": "value" };
|
||||
expect(flattenAttributes(obj, "prefix")).toEqual(expected);
|
||||
});
|
||||
|
||||
it("handles retry.byStatus correctly", () => {
|
||||
const obj = {
|
||||
"500": {
|
||||
strategy: "backoff",
|
||||
maxAttempts: 2,
|
||||
factor: 2,
|
||||
minTimeoutInMs: 1_000,
|
||||
maxTimeoutInMs: 30_000,
|
||||
randomize: false,
|
||||
},
|
||||
};
|
||||
|
||||
const expected = {
|
||||
"retry.byStatus.500.strategy": "backoff",
|
||||
"retry.byStatus.500.maxAttempts": 2,
|
||||
"retry.byStatus.500.factor": 2,
|
||||
"retry.byStatus.500.minTimeoutInMs": 1_000,
|
||||
"retry.byStatus.500.maxTimeoutInMs": 30_000,
|
||||
"retry.byStatus.500.randomize": false,
|
||||
};
|
||||
|
||||
expect(flattenAttributes(obj, "retry.byStatus")).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("unflattenAttributes", () => {
|
||||
it("returns the original object for primitive types", () => {
|
||||
// @ts-expect-error
|
||||
expect(unflattenAttributes("testString")).toEqual("testString");
|
||||
// @ts-expect-error
|
||||
expect(unflattenAttributes(12345)).toEqual(12345);
|
||||
// @ts-expect-error
|
||||
expect(unflattenAttributes(true)).toEqual(true);
|
||||
});
|
||||
|
||||
it("correctly reconstructs an object from flattened attributes", () => {
|
||||
const flattened = {
|
||||
"level1.level2.value": "test",
|
||||
"level1.array.[0]": 1,
|
||||
"level1.array.[1]": 2,
|
||||
"level1.array.[2]": 3,
|
||||
};
|
||||
const expected = {
|
||||
level1: {
|
||||
level2: {
|
||||
value: "test",
|
||||
},
|
||||
array: [1, 2, 3],
|
||||
},
|
||||
};
|
||||
expect(unflattenAttributes(flattened)).toEqual(expected);
|
||||
});
|
||||
|
||||
it("handles complex nested objects with mixed types", () => {
|
||||
const flattened = {
|
||||
"user.details.name": "John Doe",
|
||||
"user.details.age": 30,
|
||||
"user.preferences.colors.[0]": "blue",
|
||||
"user.preferences.colors.[1]": "green",
|
||||
"user.active": true,
|
||||
};
|
||||
const expected = {
|
||||
user: {
|
||||
details: {
|
||||
name: "John Doe",
|
||||
age: 30,
|
||||
},
|
||||
preferences: {
|
||||
colors: ["blue", "green"],
|
||||
},
|
||||
active: true,
|
||||
},
|
||||
};
|
||||
expect(unflattenAttributes(flattened)).toEqual(expected);
|
||||
});
|
||||
|
||||
it("correctly identifies arrays vs objects", () => {
|
||||
const flattened = {
|
||||
"array.[0]": 1,
|
||||
"array.[1]": 2,
|
||||
"object.key": "value",
|
||||
};
|
||||
const expected = {
|
||||
array: [1, 2],
|
||||
object: {
|
||||
key: "value",
|
||||
},
|
||||
};
|
||||
expect(unflattenAttributes(flattened)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -30,10 +30,10 @@
|
||||
"devDependencies": {
|
||||
"@trigger.dev/tsconfig": "workspace:*",
|
||||
"@types/node": "^18",
|
||||
"@types/react": "18.2.17",
|
||||
"@types/react": "18.2.69",
|
||||
"typescript": "^4.9.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"extends": "@trigger.dev/tsconfig/node18.json",
|
||||
"include": ["src/globals.d.ts", "./src/**/*.ts", "./src/**/*.tsx"],
|
||||
"compilerOptions": {
|
||||
"jsx": "react",
|
||||
"jsx": "react-jsx",
|
||||
"lib": ["ES2021", "dom"],
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"include": ["./src/**/*.ts", "./src/**/*.tsx", "tsup.config.ts"],
|
||||
"compilerOptions": {
|
||||
"jsx": "react",
|
||||
"jsx": "react-jsx",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
|
||||
@@ -45,9 +45,9 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.7.0",
|
||||
"@opentelemetry/api": "^1.8.0",
|
||||
"@opentelemetry/api-logs": "^0.48.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.21.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.22.0",
|
||||
"@trigger.dev/core": "workspace:^2.3.18",
|
||||
"@trigger.dev/core-backend": "workspace:^2.3.18",
|
||||
"chalk": "^5.2.0",
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
export type { ProjectConfig as Config } from "@trigger.dev/core/v3";
|
||||
export type {
|
||||
ProjectConfig as TriggerConfig,
|
||||
HandleErrorArgs,
|
||||
HandleErrorFunction,
|
||||
} from "@trigger.dev/core/v3";
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
import { Attributes, Span, SpanStatusCode, context, trace } from "@opentelemetry/api";
|
||||
import {
|
||||
SEMATTRS_HTTP_HOST,
|
||||
SEMATTRS_HTTP_METHOD,
|
||||
SEMATTRS_HTTP_RESPONSE_CONTENT_LENGTH,
|
||||
SEMATTRS_HTTP_SCHEME,
|
||||
SEMATTRS_HTTP_STATUS_CODE,
|
||||
SEMATTRS_HTTP_URL,
|
||||
} from "@opentelemetry/semantic-conventions";
|
||||
import {
|
||||
FetchRetryByStatusOptions,
|
||||
FetchRetryOptions,
|
||||
FetchRetryStrategy,
|
||||
RetryOptions,
|
||||
SemanticInternalAttributes,
|
||||
accessoryAttributes,
|
||||
calculateNextRetryDelay,
|
||||
defaultRetryOptions,
|
||||
runtime,
|
||||
eventFilterMatches,
|
||||
calculateResetAt,
|
||||
FetchTimeoutOptions,
|
||||
defaultRetryOptions,
|
||||
eventFilterMatches,
|
||||
flattenAttributes,
|
||||
runtime,
|
||||
} from "@trigger.dev/core/v3";
|
||||
import { tracer } from "./tracer";
|
||||
import { SemanticAttributes } from "@opentelemetry/semantic-conventions";
|
||||
import { AsyncLocalStorage } from "node:async_hooks";
|
||||
import { Attributes, Span, SpanStatusCode, context, trace } from "@opentelemetry/api";
|
||||
import { defaultFetchRetryOptions } from "@trigger.dev/core/v3/utils/retries";
|
||||
import type { HttpHandler } from "msw";
|
||||
import { AsyncLocalStorage } from "node:async_hooks";
|
||||
import { tracer } from "./tracer";
|
||||
|
||||
export type { RetryOptions };
|
||||
|
||||
@@ -102,7 +111,7 @@ function onThrow<T>(
|
||||
|
||||
export interface RetryFetchRequestInit extends RequestInit {
|
||||
retry?: FetchRetryOptions;
|
||||
timeout?: FetchTimeoutOptions;
|
||||
timeoutInMs?: number;
|
||||
}
|
||||
|
||||
const normalizeUrlFromInput = (input: RequestInfo | URL | string): URL => {
|
||||
@@ -162,75 +171,6 @@ class FetchErrorWithSpan extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
const doFetchRequest = async (
|
||||
input: RequestInfo | URL | string,
|
||||
init?: RequestInit,
|
||||
attemptCount: number = 0
|
||||
): Promise<[Response, Span]> => {
|
||||
const url = normalizeUrlFromInput(input);
|
||||
const httpMethod = normalizeHttpMethod(input, init);
|
||||
|
||||
const span = tracer.startSpan(`HTTP ${httpMethod}`, {
|
||||
attributes: {
|
||||
[SemanticAttributes.HTTP_METHOD]: httpMethod,
|
||||
[SemanticAttributes.HTTP_URL]: url.href,
|
||||
[SemanticAttributes.HTTP_HOST]: url.hostname,
|
||||
["server.host"]: url.hostname,
|
||||
["server.port"]: url.port,
|
||||
[SemanticAttributes.HTTP_SCHEME]: url.protocol.replace(":", ""),
|
||||
[SemanticInternalAttributes.STYLE_ICON]: "world",
|
||||
...accessoryAttributes({
|
||||
items: [
|
||||
{
|
||||
text: `${url.hostname}${url.pathname}`,
|
||||
variant: "normal",
|
||||
},
|
||||
],
|
||||
style: "codepath",
|
||||
}),
|
||||
...(attemptCount > 1 ? { ["http.request.resend_count"]: attemptCount - 1 } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetchWithInterceptors(input, {
|
||||
...init,
|
||||
headers: {
|
||||
...init?.headers,
|
||||
"x-retry-count": attemptCount.toString(),
|
||||
},
|
||||
});
|
||||
|
||||
span.setAttribute(SemanticAttributes.HTTP_STATUS_CODE, response.status);
|
||||
span.setAttribute("http.status_text", response.statusText);
|
||||
span.setAttribute(
|
||||
SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH,
|
||||
response.headers.get("content-length") || "0"
|
||||
);
|
||||
span.setAttributes(createAttributesFromHeaders(response.headers));
|
||||
|
||||
if (!response.ok) {
|
||||
span.recordException(`${response.status}: ${response.statusText}`);
|
||||
span.setStatus({
|
||||
code: SpanStatusCode.ERROR,
|
||||
message: `${response.status}: ${response.statusText}`,
|
||||
});
|
||||
}
|
||||
|
||||
return [response, span];
|
||||
} catch (e) {
|
||||
if (typeof e === "string" || e instanceof Error) {
|
||||
span.recordException(e);
|
||||
}
|
||||
|
||||
span.setStatus({ code: SpanStatusCode.ERROR });
|
||||
span.setAttribute(SemanticAttributes.HTTP_STATUS_CODE, 0);
|
||||
span.setAttribute("http.status_text", "This operation was aborted.");
|
||||
|
||||
throw new FetchErrorWithSpan(e, span);
|
||||
}
|
||||
};
|
||||
|
||||
const MAX_ATTEMPTS = 10;
|
||||
|
||||
async function retryFetch(
|
||||
@@ -246,12 +186,12 @@ async function retryFetch(
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
|
||||
const timeoutId = init?.timeout?.durationInMs
|
||||
const timeoutId = init?.timeoutInMs
|
||||
? setTimeout(
|
||||
() => {
|
||||
abortController.abort();
|
||||
},
|
||||
init?.timeout?.durationInMs
|
||||
init?.timeoutInMs
|
||||
)
|
||||
: undefined;
|
||||
|
||||
@@ -270,20 +210,30 @@ async function retryFetch(
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
span.setAttributes(createFetchResponseAttributes(response));
|
||||
|
||||
span.end();
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
const nextRetry = await calculateRetryDelayForResponse(init?.retry, response, attempt);
|
||||
const nextRetry = await calculateRetryDelayForResponse(
|
||||
resolveDefaults(init?.retry, "byStatus", defaultFetchRetryOptions.byStatus),
|
||||
response,
|
||||
attempt
|
||||
);
|
||||
|
||||
if (!nextRetry) {
|
||||
span.setAttributes(createFetchResponseAttributes(response));
|
||||
|
||||
span.end();
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
if (attempt >= MAX_ATTEMPTS) {
|
||||
span.setAttributes(createFetchResponseAttributes(response));
|
||||
|
||||
span.end();
|
||||
|
||||
return response;
|
||||
@@ -323,38 +273,72 @@ async function retryFetch(
|
||||
await runtime.waitUntil(new Date(nextRetry.value));
|
||||
}
|
||||
} catch (e) {
|
||||
if (
|
||||
e instanceof FetchErrorWithSpan &&
|
||||
e.originalError instanceof Error &&
|
||||
e.originalError.name === "AbortError"
|
||||
) {
|
||||
const nextRetryDelay = calculateNextRetryDelay(
|
||||
{ ...defaultRetryOptions, ...(init?.timeout?.retry ?? {}) },
|
||||
attempt
|
||||
);
|
||||
if (e instanceof FetchErrorWithSpan && e.originalError instanceof Error) {
|
||||
if (e.originalError.name === "AbortError") {
|
||||
const nextRetryDelay = calculateNextRetryDelay(
|
||||
resolveDefaults(init?.retry, "timeout", defaultFetchRetryOptions.timeout),
|
||||
attempt
|
||||
);
|
||||
|
||||
if (!nextRetryDelay) {
|
||||
e.span.end();
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (attempt >= MAX_ATTEMPTS) {
|
||||
e.span.end();
|
||||
throw e;
|
||||
}
|
||||
|
||||
e.span.setAttribute(
|
||||
SemanticInternalAttributes.RETRY_AT,
|
||||
new Date(Date.now() + nextRetryDelay).toISOString()
|
||||
);
|
||||
e.span.setAttribute(SemanticInternalAttributes.RETRY_COUNT, attempt);
|
||||
e.span.setAttribute(SemanticInternalAttributes.RETRY_DELAY, `${nextRetryDelay}ms`);
|
||||
|
||||
if (!nextRetryDelay) {
|
||||
e.span.end();
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (attempt >= MAX_ATTEMPTS) {
|
||||
await runtime.waitForDuration(nextRetryDelay);
|
||||
|
||||
continue; // Move to the next attempt
|
||||
} else if (
|
||||
e.originalError.name === "TypeError" &&
|
||||
"cause" in e.originalError &&
|
||||
e.originalError.cause instanceof Error
|
||||
) {
|
||||
const nextRetryDelay = calculateNextRetryDelay(
|
||||
resolveDefaults(
|
||||
init?.retry,
|
||||
"connectionError",
|
||||
defaultFetchRetryOptions.connectionError
|
||||
),
|
||||
attempt
|
||||
);
|
||||
|
||||
if (!nextRetryDelay) {
|
||||
e.span.end();
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (attempt >= MAX_ATTEMPTS) {
|
||||
e.span.end();
|
||||
throw e;
|
||||
}
|
||||
|
||||
e.span.setAttribute(
|
||||
SemanticInternalAttributes.RETRY_AT,
|
||||
new Date(Date.now() + nextRetryDelay).toISOString()
|
||||
);
|
||||
e.span.setAttribute(SemanticInternalAttributes.RETRY_COUNT, attempt);
|
||||
e.span.setAttribute(SemanticInternalAttributes.RETRY_DELAY, `${nextRetryDelay}ms`);
|
||||
|
||||
e.span.end();
|
||||
throw e;
|
||||
|
||||
await runtime.waitForDuration(nextRetryDelay);
|
||||
|
||||
continue; // Move to the next attempt
|
||||
}
|
||||
|
||||
e.span.setAttribute(
|
||||
SemanticInternalAttributes.RETRY_AT,
|
||||
new Date(Date.now() + nextRetryDelay).toISOString()
|
||||
);
|
||||
e.span.setAttribute(SemanticInternalAttributes.RETRY_COUNT, attempt);
|
||||
e.span.setAttribute(SemanticInternalAttributes.RETRY_DELAY, `${nextRetryDelay}ms`);
|
||||
|
||||
e.span.end();
|
||||
|
||||
await runtime.waitForDuration(nextRetryDelay);
|
||||
|
||||
continue; // Move to the next attempt
|
||||
}
|
||||
|
||||
if (e instanceof FetchErrorWithSpan) {
|
||||
@@ -370,13 +354,63 @@ async function retryFetch(
|
||||
{
|
||||
attributes: {
|
||||
[SemanticInternalAttributes.STYLE_ICON]: "arrow-capsule",
|
||||
...createFetchAttributes(input, init),
|
||||
...createFetchRetryOptionsAttributes(init?.retry),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const doFetchRequest = async (
|
||||
input: RequestInfo | URL | string,
|
||||
init?: RequestInit,
|
||||
attemptCount: number = 0
|
||||
): Promise<[Response, Span]> => {
|
||||
const httpMethod = normalizeHttpMethod(input, init);
|
||||
|
||||
const span = tracer.startSpan(`HTTP ${httpMethod}`, {
|
||||
attributes: {
|
||||
[SemanticInternalAttributes.STYLE_ICON]: "world",
|
||||
...(attemptCount > 1 ? { ["http.request.resend_count"]: attemptCount - 1 } : {}),
|
||||
...createFetchAttributes(input, init),
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetchWithInterceptors(input, {
|
||||
...init,
|
||||
headers: {
|
||||
...init?.headers,
|
||||
"x-retry-count": attemptCount.toString(),
|
||||
},
|
||||
});
|
||||
|
||||
span.setAttributes(createFetchResponseAttributes(response));
|
||||
|
||||
if (!response.ok) {
|
||||
span.recordException(`${response.status}: ${response.statusText}`);
|
||||
span.setStatus({
|
||||
code: SpanStatusCode.ERROR,
|
||||
message: `${response.status}: ${response.statusText}`,
|
||||
});
|
||||
}
|
||||
|
||||
return [response, span];
|
||||
} catch (e) {
|
||||
if (typeof e === "string" || e instanceof Error) {
|
||||
span.recordException(e);
|
||||
}
|
||||
|
||||
span.setStatus({ code: SpanStatusCode.ERROR });
|
||||
span.setAttribute(SEMATTRS_HTTP_STATUS_CODE, 0);
|
||||
span.setAttribute("http.status_text", "This operation was aborted.");
|
||||
|
||||
throw new FetchErrorWithSpan(e, span);
|
||||
}
|
||||
};
|
||||
|
||||
const calculateRetryDelayForResponse = async (
|
||||
retry: FetchRetryOptions | undefined,
|
||||
retry: FetchRetryByStatusOptions | undefined,
|
||||
response: Response,
|
||||
attemptCount: number
|
||||
): Promise<{ type: "delay"; value: number } | { type: "timestamp"; value: number } | undefined> => {
|
||||
@@ -421,7 +455,7 @@ const calculateRetryDelayForResponse = async (
|
||||
|
||||
const getRetryStrategyForResponse = async (
|
||||
response: Response,
|
||||
retry: FetchRetryOptions
|
||||
retry: FetchRetryByStatusOptions
|
||||
): Promise<FetchRetryStrategy | undefined> => {
|
||||
const statusCodes = Object.keys(retry);
|
||||
const clonedResponse = response.clone();
|
||||
@@ -455,7 +489,7 @@ const getRetryStrategyForResponse = async (
|
||||
* The range can be a single status code (e.g. "200"),
|
||||
* a range of status codes (e.g. "500-599"),
|
||||
* a range of status codes with a wildcard (e.g. "4xx" for any 4xx status code),
|
||||
* or a list of status codes separated by commas (e.g. "401,403,404").
|
||||
* or a list of status codes separated by commas (e.g. "401,403,404,409-412,5xx").
|
||||
* Returns `true` if the status code falls within the range, and `false` otherwise.
|
||||
*/
|
||||
const isStatusCodeInRange = (statusCode: number, statusRange: string): boolean => {
|
||||
@@ -465,7 +499,8 @@ const isStatusCodeInRange = (statusCode: number, statusRange: string): boolean =
|
||||
|
||||
if (statusRange.includes(",")) {
|
||||
const statusCodes = statusRange.split(",").map((s) => s.trim());
|
||||
return statusCodes.includes(statusCode.toString());
|
||||
|
||||
return statusCodes.some((s) => isStatusCodeInRange(statusCode, s));
|
||||
}
|
||||
|
||||
const [start, end] = statusRange.split("-");
|
||||
@@ -527,6 +562,80 @@ const interceptFetch = (...handlers: Array<HttpHandler>) => {
|
||||
};
|
||||
};
|
||||
|
||||
// This function will resolve the defaults of a property within an options object.
|
||||
// If the options object is undefined, it will return the defaults for that property (passed in as the 3rd arg).
|
||||
// if the options object is defined, and the property exists, then it will return the defaults if the value of the property is undefined or null
|
||||
const resolveDefaults = <
|
||||
TObject extends Record<string, unknown>,
|
||||
K extends keyof TObject,
|
||||
TValue extends TObject[K],
|
||||
>(
|
||||
obj: TObject | undefined,
|
||||
key: K,
|
||||
defaults: TValue
|
||||
): TValue => {
|
||||
if (!obj) {
|
||||
return defaults;
|
||||
}
|
||||
|
||||
if (obj[key] === undefined || obj[key] === null) {
|
||||
return defaults;
|
||||
}
|
||||
|
||||
return obj[key] as TValue;
|
||||
};
|
||||
|
||||
const createFetchAttributes = (
|
||||
input: RequestInfo | URL,
|
||||
init?: RetryFetchRequestInit | undefined
|
||||
): Attributes => {
|
||||
const url = normalizeUrlFromInput(input);
|
||||
const httpMethod = normalizeHttpMethod(input, init);
|
||||
|
||||
return {
|
||||
[SEMATTRS_HTTP_METHOD]: httpMethod,
|
||||
[SEMATTRS_HTTP_URL]: url.href,
|
||||
[SEMATTRS_HTTP_HOST]: url.hostname,
|
||||
["server.host"]: url.hostname,
|
||||
["server.port"]: url.port,
|
||||
[SEMATTRS_HTTP_SCHEME]: url.protocol.replace(":", ""),
|
||||
...accessoryAttributes({
|
||||
items: [
|
||||
{
|
||||
text: url.hostname,
|
||||
variant: "normal",
|
||||
},
|
||||
],
|
||||
style: "codepath",
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
const createFetchResponseAttributes = (response: Response): Attributes => {
|
||||
return {
|
||||
[SEMATTRS_HTTP_STATUS_CODE]: response.status,
|
||||
"http.status_text": response.statusText,
|
||||
[SEMATTRS_HTTP_RESPONSE_CONTENT_LENGTH]: response.headers.get("content-length") || "0",
|
||||
...createAttributesFromHeaders(response.headers),
|
||||
};
|
||||
};
|
||||
|
||||
const createFetchRetryOptionsAttributes = (retry?: FetchRetryOptions): Attributes => {
|
||||
const byStatus = resolveDefaults(retry, "byStatus", defaultFetchRetryOptions.byStatus);
|
||||
const connectionError = resolveDefaults(
|
||||
retry,
|
||||
"connectionError",
|
||||
defaultFetchRetryOptions.connectionError
|
||||
);
|
||||
const timeout = resolveDefaults(retry, "timeout", defaultFetchRetryOptions.timeout);
|
||||
|
||||
return {
|
||||
...flattenAttributes(byStatus, "retry.byStatus"),
|
||||
...flattenAttributes(connectionError, "retry.connectionError"),
|
||||
...flattenAttributes(timeout, "retry.timeout"),
|
||||
};
|
||||
};
|
||||
|
||||
export const retry = {
|
||||
onThrow,
|
||||
fetch: retryFetch,
|
||||
|
||||
Generated
+606
-7903
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,9 @@
|
||||
"start:stripe": "ts-node -r tsconfig-paths/register -r dotenv/config src/stripeUsage.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.8.0",
|
||||
"@sindresorhus/slugify": "^2.2.1",
|
||||
"@traceloop/instrumentation-openai": "^0.3.9",
|
||||
"@trigger.dev/sdk": "2.3.18",
|
||||
"msw": "^2.2.1",
|
||||
"openai": "^4.28.0",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { HandleErrorFunction } from "@trigger.dev/core/v3";
|
||||
import type { HandleErrorFunction } from "@trigger.dev/sdk/v3";
|
||||
|
||||
export const handleError: HandleErrorFunction = async (payload, error, { ctx, retry }) => {
|
||||
console.log("GOT TO handleError FUNCTION");
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { task } from "@trigger.dev/sdk/v3";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
|
||||
export const loggingTask = task({
|
||||
id: "logging-task",
|
||||
run: async () => {
|
||||
console.log("Hello world 9");
|
||||
console.log(`Hello world 9 ${slugify("foo bar")}`);
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
@@ -59,15 +59,19 @@ export const taskWithFetchRetries = task({
|
||||
return interceptor.run(next);
|
||||
},
|
||||
run: async (payload: any, { ctx }) => {
|
||||
logger.info("Fetching data", { foo: [1, 2, 3], bar: [{ hello: "world" }] });
|
||||
|
||||
//if the fetch fails, it will retry
|
||||
const headersResponse = await retry.fetch("http://my.host/test-headers", {
|
||||
retry: {
|
||||
"429": {
|
||||
strategy: "headers",
|
||||
limitHeader: "x-ratelimit-limit",
|
||||
remainingHeader: "x-ratelimit-remaining",
|
||||
resetHeader: "x-ratelimit-reset",
|
||||
resetFormat: "unix_timestamp_in_ms",
|
||||
byStatus: {
|
||||
"429": {
|
||||
strategy: "headers",
|
||||
limitHeader: "x-ratelimit-limit",
|
||||
remainingHeader: "x-ratelimit-remaining",
|
||||
resetHeader: "x-ratelimit-reset",
|
||||
resetFormat: "unix_timestamp_in_ms",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -76,26 +80,32 @@ export const taskWithFetchRetries = task({
|
||||
logger.info("Fetched headers response", { json });
|
||||
|
||||
const backoffResponse = await retry.fetch("http://my.host/test-backoff", {
|
||||
timeoutInMs: 1000,
|
||||
retry: {
|
||||
"500-599": {
|
||||
strategy: "backoff",
|
||||
maxAttempts: 10,
|
||||
factor: 2,
|
||||
minTimeoutInMs: 1_000,
|
||||
maxTimeoutInMs: 30_000,
|
||||
randomize: false,
|
||||
byStatus: {
|
||||
"500-599": {
|
||||
strategy: "backoff",
|
||||
maxAttempts: 5,
|
||||
factor: 2,
|
||||
minTimeoutInMs: 1_000,
|
||||
maxTimeoutInMs: 30_000,
|
||||
randomize: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const json2 = await backoffResponse.json();
|
||||
|
||||
// This should use the defaults.
|
||||
await retry.fetch("http://my.host/test-connection-errors");
|
||||
|
||||
logger.info("Fetched backoff response", { json2 });
|
||||
|
||||
const timeoutResponse = await retry.fetch("https://httpbin.org/delay/2", {
|
||||
timeout: {
|
||||
durationInMs: 1000,
|
||||
retry: {
|
||||
timeoutInMs: 1000,
|
||||
retry: {
|
||||
timeout: {
|
||||
maxAttempts: 5,
|
||||
factor: 1.8,
|
||||
minTimeoutInMs: 500,
|
||||
@@ -105,16 +115,11 @@ export const taskWithFetchRetries = task({
|
||||
},
|
||||
});
|
||||
|
||||
const json3 = await timeoutResponse.json();
|
||||
|
||||
logger.info("Fetched timeout response", { json3 });
|
||||
|
||||
return {
|
||||
result: "successss",
|
||||
payload,
|
||||
json,
|
||||
json2,
|
||||
json3,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -28,5 +28,14 @@ export const interceptor = retry.interceptFetch(
|
||||
return new HttpResponse(null, {
|
||||
status: 500,
|
||||
});
|
||||
}),
|
||||
http.get("http://my.host/test-connection-errors", ({ request }) => {
|
||||
const retryCount = request.headers.get("x-retry-count");
|
||||
|
||||
if (retryCount === "2") {
|
||||
return HttpResponse.json({ test: "connection-errors" });
|
||||
}
|
||||
|
||||
return HttpResponse.error();
|
||||
})
|
||||
);
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { task } from "@trigger.dev/sdk/v3";
|
||||
|
||||
export const weirdFileName = task({
|
||||
id: "weird-file-name",
|
||||
run: async (payload: { url: string }) => {},
|
||||
});
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { ProjectConfig } from "@trigger.dev/core/v3";
|
||||
import type { TriggerConfig } from "@trigger.dev/sdk/v3";
|
||||
import { OpenAIInstrumentation } from "@traceloop/instrumentation-openai";
|
||||
|
||||
export { handleError } from "./src/handleError";
|
||||
|
||||
export const config: ProjectConfig = {
|
||||
export const config: TriggerConfig = {
|
||||
project: "yubjwjsfkxnylobaqvqz",
|
||||
retries: {
|
||||
enabledInDev: true,
|
||||
@@ -15,4 +16,7 @@ export const config: ProjectConfig = {
|
||||
},
|
||||
},
|
||||
additionalPackages: ["wrangler@3.35.0"],
|
||||
additionalFiles: ["./wrangler/wrangler.toml"],
|
||||
dependenciesToBundle: [/@sindresorhus/, "escape-string-regexp"],
|
||||
instrumentations: [new OpenAIInstrumentation()],
|
||||
};
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
name = "proxy"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2023-10-30"
|
||||
compatibility_flags = ["nodejs_compat"]
|
||||
|
||||
[env.staging]
|
||||
[env.prod]
|
||||
Reference in New Issue
Block a user