From dcdffeeb0578532391eff66d8daeca8c65d8d370 Mon Sep 17 00:00:00 2001 From: Chris Anderson Date: Mon, 28 Jul 2025 14:48:37 -0500 Subject: [PATCH] feat: add cache key functionality to webhooks --- .../platform/src/providers/webhook/Webhook.ts | 1 + .../src/providers/webhook/WebhookChannel.ts | 39 +++++++++---------- apps/platform/src/render/Helpers/String.ts | 7 +++- apps/platform/src/render/Template.ts | 27 ++++++------- apps/platform/src/render/index.ts | 10 +++++ apps/ui/public/locales/en.json | 4 +- apps/ui/src/types.ts | 1 + apps/ui/src/views/campaign/TemplateDetail.tsx | 6 +++ 8 files changed, 56 insertions(+), 39 deletions(-) diff --git a/apps/platform/src/providers/webhook/Webhook.ts b/apps/platform/src/providers/webhook/Webhook.ts index 7e766ff7..b7d10044 100644 --- a/apps/platform/src/providers/webhook/Webhook.ts +++ b/apps/platform/src/providers/webhook/Webhook.ts @@ -3,6 +3,7 @@ export interface Webhook { endpoint: string headers: Record body?: Record + cacheKey?: string } export interface WebhookResponse { diff --git a/apps/platform/src/providers/webhook/WebhookChannel.ts b/apps/platform/src/providers/webhook/WebhookChannel.ts index 7cf81856..e35537d3 100644 --- a/apps/platform/src/providers/webhook/WebhookChannel.ts +++ b/apps/platform/src/providers/webhook/WebhookChannel.ts @@ -1,7 +1,9 @@ import { WebhookTemplate } from '../../render/Template' -import Render, { Variables } from '../../render' +import { Variables } from '../../render' import { WebhookProvider } from './WebhookProvider' import { WebhookResponse } from './Webhook' +import App from '../../app' +import { cacheGet, cacheSet } from '../../config/redis' export default class WebhookChannel { readonly provider: WebhookProvider @@ -13,27 +15,22 @@ export default class WebhookChannel { } } - async send(options: WebhookTemplate, variables: Variables): Promise { - const headers = this.compile(options.headers, variables) - const endpoint = Render(options.endpoint, variables) - const method = options.method - const body = method === 'POST' || method === 'PATCH' || method === 'PUT' - ? this.compile(options.body, variables) - : undefined + async send(template: WebhookTemplate, variables: Variables): Promise { - return await this.provider.send({ - endpoint, - method, - headers, - body, - }) - } + const message = template.compile(variables) + const redis = App.main.redis - private compile(object: Record | undefined, variables: Variables) { - if (!object) return {} - return Object.keys(object).reduce((body, key) => { - body[key] = Render(object[key], variables) - return body - }, {} as Record) + // If we have a cache key, check cache first + if (message.cacheKey?.length) { + const key = `wh:${variables.context.campaign_id}:${message.cacheKey}` + const value = await cacheGet(redis, key) + if (value) return value + const response = await this.provider.send(message) + + await cacheSet(redis, key, response, 3600) + return response + } + + return await this.provider.send(message) } } diff --git a/apps/platform/src/render/Helpers/String.ts b/apps/platform/src/render/Helpers/String.ts index dd0541af..05f201e9 100644 --- a/apps/platform/src/render/Helpers/String.ts +++ b/apps/platform/src/render/Helpers/String.ts @@ -208,6 +208,9 @@ export const truncateWords = function(str: string, count: number, suffix = ''): * Get the base locale from a given locale string * @param locale */ -export const baseLocale = (locale: string): string => { - return locale.split('-')[0] +export const baseLocale = (locale?: string): string | undefined => { + if (!locale) return '' + const parts = locale.split('-') + if (parts.length < 2) return locale + return parts[0] } diff --git a/apps/platform/src/render/Template.ts b/apps/platform/src/render/Template.ts index ee465904..c6d18e93 100644 --- a/apps/platform/src/render/Template.ts +++ b/apps/platform/src/render/Template.ts @@ -1,4 +1,4 @@ -import Render, { Variables, Wrap } from '.' +import Render, { RenderObject, Variables, Wrap } from '.' import { Webhook } from '../providers/webhook/Webhook' import { ChannelType } from '../config/channels' import Model, { ModelParams } from '../core/Model' @@ -183,11 +183,7 @@ export class PushTemplate extends Template { } compile(variables: Variables): CompiledPush { - const custom = Object.keys(this.custom).reduce((body, key) => { - body[key] = Render(this.custom[key], variables) - return body - }, {} as Record) - + const custom = RenderObject(this.custom, variables) const url = this.compileUrl(variables) return { @@ -239,6 +235,7 @@ export class WebhookTemplate extends Template { endpoint!: string body!: Record headers: Record = {} + cacheKey?: string parseJson(json: any) { super.parseJson(json) @@ -247,26 +244,26 @@ export class WebhookTemplate extends Template { this.endpoint = json?.data.endpoint this.body = json?.data.body this.headers = json?.data.headers || {} + this.cacheKey = json?.data.cache_key } compile(variables: Variables): Webhook { - const headers = Object.keys(this.headers ?? {}).reduce((headers, key) => { - headers[key] = Render(this.headers[key], variables) - return headers - }, {} as Record) - - const body = Object.keys(this.body ?? {}).reduce((body, key) => { - body[key] = Render(this.body[key], variables) - return body - }, {} as Record) + const headers = RenderObject(this.headers, variables) + const body = ['POST', 'PATCH', 'PUT'].includes(this.method) + ? RenderObject(this.body, variables) + : undefined const endpoint = Render(this.endpoint, variables) + const cacheKey = this.cacheKey + ? Render(this.cacheKey, variables) + : undefined const method = this.method return { endpoint, method, headers, body, + cacheKey, } } diff --git a/apps/platform/src/render/index.ts b/apps/platform/src/render/index.ts index 64dbc955..35198a5a 100644 --- a/apps/platform/src/render/index.ts +++ b/apps/platform/src/render/index.ts @@ -92,4 +92,14 @@ export const Render = (template: string, { user, event, journey, context }: Vari }) } +export const RenderObject = (object: Record | undefined, variables: Variables) => { + if (!object) return {} + return Object.keys(object).reduce((body, key) => { + body[key] = typeof object[key] === 'object' + ? RenderObject(object[key], variables) + : Render(object[key], variables) + return body + }, {} as Record) +} + export default Render diff --git a/apps/ui/public/locales/en.json b/apps/ui/public/locales/en.json index 4522d98a..8bdbfd8a 100644 --- a/apps/ui/public/locales/en.json +++ b/apps/ui/public/locales/en.json @@ -29,8 +29,10 @@ "balancer_edit_desc": "Randomly split users across paths. Configure an optional rate limit to limit the number of users that go down a path over a given time period.", "bcc": "BCC", "blast": "Blast", - "body": "Body", + "body": "Body (JSON)", "bounced": "Bounced", + "cache_key": "Cache Key", + "cache_key_subtitle": "A unique key to cache the response for this webhook. Only use if you intend to cache the response, leave empty to not cache.", "campaign_alert_pending": "This campaign has not been sent yet! Once the campaign is live or scheduled this tab will show the progress and results.", "campaign_alert_scheduled": "This campaign is pending delivery. It will begin to roll out at", "campaign_delivery_trigger_description": "Delivery for trigger campaigns is activated via API or journey action. An example request of how to trigger a send via API is available below.", diff --git a/apps/ui/src/types.ts b/apps/ui/src/types.ts index c78e8c0b..e1ffedfd 100644 --- a/apps/ui/src/types.ts +++ b/apps/ui/src/types.ts @@ -453,6 +453,7 @@ export interface WebhookTemplateData { endpoint: string body: Record headers: Record + cache_key?: string } export type Template = { diff --git a/apps/ui/src/views/campaign/TemplateDetail.tsx b/apps/ui/src/views/campaign/TemplateDetail.tsx index 3cb51fa2..112e5af0 100644 --- a/apps/ui/src/views/campaign/TemplateDetail.tsx +++ b/apps/ui/src/views/campaign/TemplateDetail.tsx @@ -133,6 +133,7 @@ const WebhookTable = ({ data }: { data: WebhookTemplateData }) => { [t('endpoint')]: data.endpoint ?? {t('missing')}, [t('headers')]: JSON.stringify(data.headers), [t('body')]: JSON.stringify(data.body), + [t('cache_key')]: data.cache_key, }} /> } @@ -160,6 +161,11 @@ const WebhookForm = ({ form }: { form: UseFormReturn name="data.body" label={t('body')} textarea /> + }