feat: add cache key functionality to webhooks

This commit is contained in:
Chris Anderson 2025-07-28 14:48:37 -05:00
parent bdeaaaf4c8
commit dcdffeeb05
8 changed files with 56 additions and 39 deletions

View file

@ -3,6 +3,7 @@ export interface Webhook {
endpoint: string
headers: Record<string, string>
body?: Record<string, any>
cacheKey?: string
}
export interface WebhookResponse {

View file

@ -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<WebhookResponse> {
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<WebhookResponse> {
return await this.provider.send({
endpoint,
method,
headers,
body,
})
}
const message = template.compile(variables)
const redis = App.main.redis
private compile(object: Record<string, string> | undefined, variables: Variables) {
if (!object) return {}
return Object.keys(object).reduce((body, key) => {
body[key] = Render(object[key], variables)
return body
}, {} as Record<string, any>)
// 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<WebhookResponse>(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)
}
}

View file

@ -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]
}

View file

@ -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<string, any>)
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<string, any>
headers: Record<string, string> = {}
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<string, string>)
const body = Object.keys(this.body ?? {}).reduce((body, key) => {
body[key] = Render(this.body[key], variables)
return body
}, {} as Record<string, any>)
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,
}
}

View file

@ -92,4 +92,14 @@ export const Render = (template: string, { user, event, journey, context }: Vari
})
}
export const RenderObject = (object: Record<string, any> | 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<string, any>)
}
export default Render

View file

@ -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.",

View file

@ -453,6 +453,7 @@ export interface WebhookTemplateData {
endpoint: string
body: Record<string, any>
headers: Record<string, string>
cache_key?: string
}
export type Template = {

View file

@ -133,6 +133,7 @@ const WebhookTable = ({ data }: { data: WebhookTemplateData }) => {
[t('endpoint')]: data.endpoint ?? <Tag variant="warn">{t('missing')}</Tag>,
[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<TemplateUpdateParams, any>
name="data.body"
label={t('body')}
textarea />
<TextInput.Field
form={form}
name="data.cache_key"
label={t('cache_key')}
subtitle={t('cache_key_subtitle')} />
</>
}