diff --git a/apps/docs/mint.json b/apps/docs/mint.json index 7dd0e81b089..41e403a370c 100644 --- a/apps/docs/mint.json +++ b/apps/docs/mint.json @@ -39,7 +39,11 @@ "navigation": [ { "group": "Getting Started", - "pages": ["welcome", "getting-started", "get-help"] + "pages": [ + "welcome", + "getting-started", + "get-help" + ] }, { "group": "Examples", @@ -67,11 +71,15 @@ "pages": [ { "group": "Slack", - "pages": ["integrations/apis/slack/actions/post-message"] + "pages": [ + "integrations/apis/slack/actions/post-message" + ] }, { "group": "Resend.com", - "pages": ["integrations/apis/resend/actions/send-email"] + "pages": [ + "integrations/apis/resend/actions/send-email" + ] } ] }, @@ -102,7 +110,8 @@ "pages": [ "reference/trigger", "reference/custom-event", - "reference/webhook-event" + "reference/webhook-event", + "reference/schedule-event" ] }, { @@ -128,4 +137,4 @@ "github": "https://github.com/triggerdotdev/trigger.dev", "discord": "https://discord.gg/nkqV9xBYWy" } -} +} \ No newline at end of file diff --git a/apps/docs/openapi-spec.yaml b/apps/docs/openapi-spec.yaml new file mode 100644 index 00000000000..8395e3ad24f --- /dev/null +++ b/apps/docs/openapi-spec.yaml @@ -0,0 +1,36 @@ +openapi: 3.0.3 +info: + title: Trigger.dev API + version: v1.0.0 + description: Please see https://docs.trigger.dev for more details. + contact: + name: Eric + email: eric@trigger.dev + url: https://trigger.dev + termsOfService: https://trigger.dev/terms +servers: + - url: https://app.trigger.dev/api/v1 + description: Trigger.dev +paths: + /events: + post: + summary: Send Custom Event + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/customEvent" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/customEventResponse" + "422": + description: Unprocessable Entity + content: + application/json: + schema: + $ref: "#/components/schemas/error" diff --git a/apps/docs/reference/custom-event.mdx b/apps/docs/reference/custom-event.mdx index a124418982c..a6a4f892b9e 100644 --- a/apps/docs/reference/custom-event.mdx +++ b/apps/docs/reference/custom-event.mdx @@ -7,6 +7,8 @@ description: "Trigger a workflow when a custom event is received." ## Usage ```ts +import { customEvent, Trigger } from "@trigger.dev/sdk"; + new Trigger({ id: "user-created-notify-slack", name: "User Created - Notify Slack", diff --git a/apps/docs/reference/schedule-event.mdx b/apps/docs/reference/schedule-event.mdx new file mode 100644 index 00000000000..9cce0351ccb --- /dev/null +++ b/apps/docs/reference/schedule-event.mdx @@ -0,0 +1,68 @@ +--- +title: "scheduleEvent Trigger" +sidebarTitle: "scheduleEvent" +description: "Run a workflow on a recurring schedule." +--- + +## Usage + +```ts +import { scheduleEvent } from "@trigger.dev/sdk"; + +new Trigger({ + id: "usage", + name: "usage", + on: scheduleEvent({ rateof: { minutes: 10 } }), + run: async (event, ctx) => {}, +}).listen(); +``` + +## Options + +You must use one of the following options, but not both: + + + The rate of the schedule. This can be a number of minutes, hours, + or days. For example, `{ rateOf: { minutes: 10 } }` will run + every 10 minutes. + + + + A cron expression to run the workflow on. For example, `0 0 * * *` will run + the workflow every hour at the top of the hour. See + [crontab.guru](https://crontab.guru/) for more information. + + +## Event Payload + + + The time the event was scheduled to run. + + + + The time the event last run. This will be `undefined` if the event has never run. Use this parameter to run window queries: + +```ts +import { scheduleEvent, Trigger } from "@trigger.dev/sdk"; + +new Trigger({ + id: "usage", + name: "usage", + on: scheduleEvent({ rateof: { minutes: 10 } }), + run: async (event, ctx) => { + const { lastRunAt, scheduledTime } = event; + + const query = `SELECT * FROM users WHERE created_at < ${scheduledTime}`; + + if (lastRunAt) { + query += ` AND created_at > ${lastRunAt}`; + } + + const latestUsers = await db.query(query); + + // ... + }, +}).listen(); +``` + + diff --git a/apps/docs/reference/trigger.mdx b/apps/docs/reference/trigger.mdx index 9c8fd5ec9e9..d06f51cbfd0 100644 --- a/apps/docs/reference/trigger.mdx +++ b/apps/docs/reference/trigger.mdx @@ -9,6 +9,8 @@ description: "The Trigger class let's you define a workflow that is triggered by ### Usage ```ts +import { customEvent, Trigger } from "@trigger.dev/sdk"; + const trigger = new Trigger({ id: "user-created-notify-slack", name: "User Created - Notify Slack", @@ -39,11 +41,6 @@ const trigger = new Trigger({ `TRIGGER_API_KEY` environment variable. - - Your Trigger.dev API key. If not provided, the API key will be read from the - `TRIGGER_API_KEY` environment variable. - - The URL of the Trigger.dev WebSocket server. If not provided, the endpoint will point to the production server. @@ -87,6 +84,10 @@ const trigger = new Trigger({ for more info. + + Whether or not this trigger is being run as a test. + + A function that can be used to send an event to trigger.dev. See the [Sending Events](/functions/send-event) page for more info. diff --git a/apps/docs/reference/webhook-event.mdx b/apps/docs/reference/webhook-event.mdx index 1785cbac668..4fce5d7825a 100644 --- a/apps/docs/reference/webhook-event.mdx +++ b/apps/docs/reference/webhook-event.mdx @@ -7,6 +7,8 @@ description: "Trigger a workflow when a webhook event is received" ## Usage ```ts +import { webhookEvent, Trigger } from "@trigger.dev/sdk"; + new Trigger({ id: "caldotcom-to-slack", name: "Cal.com To Slack", diff --git a/apps/docs/triggers/scheduled.mdx b/apps/docs/triggers/scheduled.mdx index 5bea86e99eb..5a88f4b6e89 100644 --- a/apps/docs/triggers/scheduled.mdx +++ b/apps/docs/triggers/scheduled.mdx @@ -4,6 +4,8 @@ sidebarTitle: "Scheduled" description: "Run a workflow on a recurring schedule" --- +See the [reference](/reference/schedule-event) for more details. + ## Examples ### Every 5 minutes @@ -51,3 +53,33 @@ new Trigger({ }, }).listen(); ``` + +## Preventing late runs + +To prevent a scheduled trigger from running late, you can set a `triggerTTL` option when creating the `Trigger`, like so: + +```ts +new Trigger({ + id: "scheduled-workflow", + name: "Scheduled Workflow", + apiKey: "", + on: scheduleEvent({ rateOf: { minutes: 5 } }), + triggerTTL: 300, + run: async (event, ctx) => { + await ctx.logger.info("Received the scheduled event", { + event, + wallTime: new Date(), + }); + + return { foo: "bar" }; + }, +}).listen(); +``` + +This will prevent the trigger from running if it is running more than `300` seconds behind, which can happen if the server running your `Trigger` code goes down or is otherwise unavailable. + +This is especially useful for scheduled triggers that run on a very short interval, like every minute, so you don't get a backlog of runs that all run at once when the server comes back online. + + + Set your `triggerTTL` to the same time (or double) as the rateOf the trigger. + diff --git a/apps/webapp/app/services/externalSources/registerExternalSource.server.ts b/apps/webapp/app/services/externalSources/registerExternalSource.server.ts index 55d2d8a7b16..282d940df5a 100644 --- a/apps/webapp/app/services/externalSources/registerExternalSource.server.ts +++ b/apps/webapp/app/services/externalSources/registerExternalSource.server.ts @@ -26,6 +26,15 @@ export class RegisterExternalSource { } if (externalSource.status === "READY") { + await this.#prismaClient.workflow.updateMany({ + where: { + externalSourceId: externalSource.id, + }, + data: { + status: "READY", + }, + }); + return true; } diff --git a/apps/webapp/app/services/messageBroker.server.ts b/apps/webapp/app/services/messageBroker.server.ts index 189a5ff1476..2a99648a37f 100644 --- a/apps/webapp/app/services/messageBroker.server.ts +++ b/apps/webapp/app/services/messageBroker.server.ts @@ -664,6 +664,7 @@ function createTaskQueue() { "x-env": run.environment.slug, "x-workflow-run-id": run.id, "x-ttl": run.workflow.triggerTtlInSeconds, + "x-is-test": run.isTest ? "true" : "false", }, { eventTimestamp: run.event.timestamp.getTime(), diff --git a/apps/webapp/app/services/scheduler/scheduleNextEvent.server.ts b/apps/webapp/app/services/scheduler/scheduleNextEvent.server.ts index 39284533b8e..7f21d0b26ed 100644 --- a/apps/webapp/app/services/scheduler/scheduleNextEvent.server.ts +++ b/apps/webapp/app/services/scheduler/scheduleNextEvent.server.ts @@ -29,7 +29,13 @@ export class ScheduleNextEvent { const messageId = await taskQueue.publish( "DELIVER_SCHEDULED_EVENT", - { externalSourceId: schedulerSource.id, payload: { scheduledTime } }, + { + externalSourceId: schedulerSource.id, + payload: { + scheduledTime, + lastRunAt: fromEvent ? fromEvent.createdAt : undefined, + }, + }, {}, { deliverAt: scheduledTime.getTime() } ); diff --git a/apps/webapp/app/triggers/monitoring.server.ts b/apps/webapp/app/triggers/monitoring.server.ts index 01ff77c68b3..d4ea8637c76 100644 --- a/apps/webapp/app/triggers/monitoring.server.ts +++ b/apps/webapp/app/triggers/monitoring.server.ts @@ -5,19 +5,20 @@ import { prisma } from "~/db.server"; export const uptimeCheck = new Trigger({ id: "uptime-check", name: "Uptime Check", - on: scheduleEvent({ rateOf: { minutes: 1 } }), - triggerTTL: 300, + on: scheduleEvent({ rateOf: { minutes: 5 } }), + logLevel: "info", + triggerTTL: 60, run: async (event, context) => { + if (context.environment === "development" && !context.isTest) { + return; + } + // Grab counts of workflows, runs, and steps const userCount = await prisma.user.count(); const workflowCount = await prisma.workflow.count(); const runCount = await prisma.workflowRun.count(); const stepCount = await prisma.workflowRunStep.count(); - if (context.environment === "development") { - return; - } - await slack.postMessage("Uptime Notification", { channelName: "monitoring", text: `[${context.environment}] Uptime Check: ${userCount} users, ${workflowCount} workflows, ${runCount} runs, ${stepCount} steps.`, diff --git a/apps/wss/src/runController.ts b/apps/wss/src/runController.ts index 6070cd8bbdf..6a68016c7e9 100644 --- a/apps/wss/src/runController.ts +++ b/apps/wss/src/runController.ts @@ -24,6 +24,7 @@ export type WorkflowRunControllerOptions = { environment: string; apiKey: string; organizationId: string; + isTest: boolean; }; }; @@ -37,6 +38,7 @@ export class WorkflowRunController { environment: string; apiKey: string; organizationId: string; + isTest: boolean; }; #logger: Logger; diff --git a/apps/wss/src/server.ts b/apps/wss/src/server.ts index 32171bf2882..c9f446684a5 100644 --- a/apps/wss/src/server.ts +++ b/apps/wss/src/server.ts @@ -8,7 +8,6 @@ import { import { CommandCatalog, InternalApiClient, - MessageCatalogSchema, TriggerCatalog, triggerCatalog, ZodPublisher, @@ -420,6 +419,10 @@ export class TriggerServer { organizationId: properties["x-org-id"], environment: properties["x-env"], apiKey: properties["x-api-key"], + isTest: + typeof properties["x-is-test"] === "string" + ? properties["x-is-test"] === "true" + : false, }, }); @@ -519,55 +522,3 @@ function safeJsonParse(json?: string) { return undefined; } } - -function createForwardHandler< - TSubscriberSchema extends MessageCatalogSchema, - THandlerName extends keyof TSubscriberSchema ->( - schema: TSubscriberSchema, - handler: THandlerName, - logger: Logger, - topicFn: ( - data: z.infer - ) => string -) { - return async ( - id: string, - data: z.infer, - properties: z.infer - ) => { - const topicName = topicFn(properties); - - logger.debug(`Forwarding message to ${topicName}`, data, properties); - - const runPublisher = new ZodPublisher({ - schema: schema, - client: pulsarClient, - config: { - topic: topicName, - batchingEnabled: false, - }, - }); - - await runPublisher.initialize(); - - try { - await runPublisher.publish(handler, data, properties); - - logger.debug(`Forwarded message to ${topicName}`, data, properties); - - return true; - } catch (e) { - logger.error( - `Failed to forward message to ${topicName}`, - e, - data, - properties - ); - - return false; - } finally { - await runPublisher.close(); - } - }; -} diff --git a/examples/fetch-playground/src/index.ts b/examples/fetch-playground/src/index.ts index 9a2f2789f1d..fe37c774b1a 100644 --- a/examples/fetch-playground/src/index.ts +++ b/examples/fetch-playground/src/index.ts @@ -39,6 +39,10 @@ new Trigger({ wallTime: new Date(), }); + if (ctx.isTest) { + await ctx.logger.warn("This is only a test"); + } + const response = await ctx.fetch("do-fetch", `${event.url}${event.path}`, { method: event.method, responseSchema: z.any(), diff --git a/examples/schedule-to-slack/src/index.ts b/examples/schedule-to-slack/src/index.ts index 4f6eadbe1a2..6ca2262e233 100644 --- a/examples/schedule-to-slack/src/index.ts +++ b/examples/schedule-to-slack/src/index.ts @@ -2,20 +2,21 @@ import { Trigger, scheduleEvent } from "@trigger.dev/sdk"; import { slack } from "@trigger.dev/integrations"; const trigger = new Trigger({ - id: "schedule-to-slack", + id: "schedule-to-slack-2", name: "Send to Slack every minute", - apiKey: "trigger_development_vzNnO2DGBGcG", + apiKey: "trigger_dev_zC25mKNn6c0q", + endpoint: "ws://localhost:8889/ws", logLevel: "debug", on: scheduleEvent({ rateOf: { minutes: 1 } }), run: async (event, ctx) => { await ctx.logger.info("It's me, the annoying slack bot!"); - const response = await slack.postMessage("slaaaaaack", { - channel: "test-integrations", - text: `Hello, the time is ${event.scheduledTime}`, - }); + // const response = await slack.postMessage("slaaaaaack", { + // channelName: "test-integrations", + // text: `Hello, the time is ${event.scheduledTime}, and I was last run at ${event.lastRunAt}!`, + // }); - return response.message; + return event; }, }); diff --git a/packages/common-schemas/src/events.ts b/packages/common-schemas/src/events.ts index 26805d54492..5ece387271e 100644 --- a/packages/common-schemas/src/events.ts +++ b/packages/common-schemas/src/events.ts @@ -29,6 +29,7 @@ export const EventFilterSchema: z.ZodType = z.lazy(() => ); export const ScheduledEventPayloadSchema = z.object({ + lastRunAt: z.coerce.date().optional(), scheduledTime: z.coerce.date(), }); diff --git a/packages/internal-bridge/src/schemas/host.ts b/packages/internal-bridge/src/schemas/host.ts index bec310e2d37..2209197e711 100644 --- a/packages/internal-bridge/src/schemas/host.ts +++ b/packages/internal-bridge/src/schemas/host.ts @@ -14,6 +14,7 @@ export const HostRPCSchema = { workflowId: z.string(), organizationId: z.string(), apiKey: z.string(), + isTest: z.boolean().default(false), }), }), response: z.boolean(), diff --git a/packages/internal-integrations/src/github/index.ts b/packages/internal-integrations/src/github/index.ts index 660b09c0d00..853746135aa 100644 --- a/packages/internal-integrations/src/github/index.ts +++ b/packages/internal-integrations/src/github/index.ts @@ -1,13 +1,14 @@ +import { Webhooks } from "@octokit/webhooks"; +import { github } from "@trigger.dev/providers"; +import crypto from "crypto"; +import { z } from "zod"; +import { getAccessToken } from "../accessInfo"; import { DisplayProperty, HandleWebhookOptions, WebhookConfig, WebhookIntegration, } from "../types"; -import { Webhooks } from "@octokit/webhooks"; -import { z } from "zod"; -import { github } from "@trigger.dev/providers"; -import { getAccessToken } from "../accessInfo"; export class GitHubWebhookIntegration implements WebhookIntegration { keyForSource(source: unknown): string { @@ -71,9 +72,11 @@ export class GitHubWebhookIntegration implements WebhookIntegration { "x-forwarded-proto", ]); + const id = md5Hash([hookId, deliveryId].join("-")); + return { status: "ok" as const, - data: { id: hookId, payload: options.request.body, event, context }, + data: { id, payload: options.request.body, event, context }, }; } @@ -207,3 +210,10 @@ function omit, K extends keyof T>( return result; } + +function md5Hash(str: string): string { + const hash = crypto.createHash("md5"); + hash.update(str); + + return hash.digest("hex"); +} diff --git a/packages/internal-platform/src/messages/catalogs/triggers.ts b/packages/internal-platform/src/messages/catalogs/triggers.ts index 057308d93e9..53a6d02fe9c 100644 --- a/packages/internal-platform/src/messages/catalogs/triggers.ts +++ b/packages/internal-platform/src/messages/catalogs/triggers.ts @@ -7,6 +7,7 @@ const Catalog = { data: TriggerWorkflowMessageSchema, properties: WorkflowRunEventPropertiesSchema.extend({ "x-ttl": z.coerce.number().optional(), + "x-is-test": z.string().default("false"), }), }, }; diff --git a/packages/trigger-integrations/CHANGELOG.md b/packages/trigger-integrations/CHANGELOG.md index ce14d2ea69e..6910c5dfedd 100644 --- a/packages/trigger-integrations/CHANGELOG.md +++ b/packages/trigger-integrations/CHANGELOG.md @@ -1,5 +1,13 @@ # @trigger.dev/integrations +## 0.1.13 + +### Patch Changes + +- Updated dependencies [e37a200] +- Updated dependencies [e63d354] + - @trigger.dev/sdk@0.2.10 + ## 0.1.12 ### Patch Changes diff --git a/packages/trigger-integrations/package.json b/packages/trigger-integrations/package.json index 6bae5e18427..4b762884d35 100644 --- a/packages/trigger-integrations/package.json +++ b/packages/trigger-integrations/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/integrations", - "version": "0.1.12", + "version": "0.1.13", "description": "trigger.dev integrations", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/trigger-sdk/CHANGELOG.md b/packages/trigger-sdk/CHANGELOG.md index 2c8f4ba53c8..1b84f33cfdc 100644 --- a/packages/trigger-sdk/CHANGELOG.md +++ b/packages/trigger-sdk/CHANGELOG.md @@ -1,5 +1,12 @@ # @trigger.dev/sdk +## 0.2.10 + +### Patch Changes + +- e37a200: Added lastRunAt to the scheduleEvent payload +- e63d354: Added isTest to TriggerContext + ## 0.2.9 ### Patch Changes diff --git a/packages/trigger-sdk/package.json b/packages/trigger-sdk/package.json index e40b7e9622b..ad5074fbc28 100644 --- a/packages/trigger-sdk/package.json +++ b/packages/trigger-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/sdk", - "version": "0.2.9", + "version": "0.2.10", "description": "trigger.dev Node.JS SDK", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/trigger-sdk/src/client.ts b/packages/trigger-sdk/src/client.ts index ae181af679f..de8b6bad7ba 100644 --- a/packages/trigger-sdk/src/client.ts +++ b/packages/trigger-sdk/src/client.ts @@ -357,6 +357,7 @@ export class TriggerClient { environment: data.meta.environment, apiKey: data.meta.apiKey, organizationId: data.meta.organizationId, + isTest: data.meta.isTest, logger: new ContextLogger(async (level, message, properties) => { await serverRPC.send("SEND_LOG", { runId: data.id, diff --git a/packages/trigger-sdk/src/types.ts b/packages/trigger-sdk/src/types.ts index 075d94e717d..a99abe05501 100644 --- a/packages/trigger-sdk/src/types.ts +++ b/packages/trigger-sdk/src/types.ts @@ -62,6 +62,7 @@ export interface TriggerContext { apiKey: string; organizationId: string; logger: TriggerLogger; + isTest: boolean; sendEvent(key: string, event: TriggerCustomEvent): Promise; waitFor(key: string, options: WaitForOptions): Promise; waitUntil(key: string, date: Date): Promise;