Skip to content

Commit f6ba0c4

Browse files
authored
Pipes through statsContext to evaluateLiquid directive function (#3024)
* WIP - pipes through statsContext to evaluateLiquid directive function * WIP - pipes through statsContext to evaluateLiquid directive function Tracks liquid metrics when batching as well WIP passes tags hopefully * Adds action and destination metadata values to tags * Removes Subscription values which are undefined * Imports performance * Throws error only after logging out metrics * Does not attempt to resolve the perf_hooks package as it's not browser supported * Removes perf_hooks usage since it proved to be a never-ending rabbithole of import errors * Reverts packages/browser-destinations/webpack.config.js * Dry suggestion: uses finally block to call histogram * Only append tags when isLiquidDirective is true
1 parent 59c6df5 commit f6ba0c4

File tree

4 files changed

+66
-15
lines changed

4 files changed

+66
-15
lines changed

packages/core/src/destination-kit/action.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ export class Action<Settings, Payload extends JSONLikeObject, AudienceSettings =
318318
const results: Result[] = []
319319

320320
// Resolve/transform the mapping with the input data
321-
let payload = transform(bundle.mapping, bundle.data) as Payload
321+
let payload = transform(bundle.mapping, bundle.data, bundle.statsContext) as Payload
322322
results.push({ output: 'Mappings resolved' })
323323

324324
// Remove empty values (`null`, `undefined`, `''`) when not explicitly accepted
@@ -387,7 +387,7 @@ export class Action<Settings, Payload extends JSONLikeObject, AudienceSettings =
387387

388388
const mapping: JSONObject = bundle.mapping
389389

390-
let payloads = transformBatch(mapping, bundle.data) as Payload[]
390+
let payloads = transformBatch(mapping, bundle.data, bundle.statsContext) as Payload[]
391391
const batchPayloadLength = payloads.length
392392

393393
const multiStatusResponse: ResultMultiStatusNode[] = []

packages/core/src/destination-kit/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,10 @@ export class Destination<Settings = JSONObject, AudienceSettings = JSONObject> {
725725
): Promise<Result[]> {
726726
const isBatch = Array.isArray(events)
727727

728+
if (options?.statsContext?.tags !== undefined) {
729+
options.statsContext.tags = [...options.statsContext.tags, `partnerAction:${subscription.partnerAction}`]
730+
}
731+
728732
const subscriptionStartedAt = time()
729733
const actionSlug = subscription.partnerAction
730734
const input = {

packages/core/src/mapping-kit/index.ts

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import validate from './validate'
88
import { arrify } from '../arrify'
99
import { flattenObject } from './flatten'
1010
import { evaluateLiquid } from './liquid-directive'
11+
import { StatsContext } from '../destination-kit'
12+
import { isLiquidDirective } from './value-keys'
1113

1214
export type InputData = { [key: string]: unknown }
1315
export type Features = { [key: string]: boolean }
14-
type Directive = (options: JSONValue, payload: JSONObject) => JSONLike
16+
type Directive = (options: JSONValue, payload: JSONObject, statsContext?: StatsContext | undefined) => JSONLike
1517
type StringDirective = (value: string, payload: JSONObject) => JSONLike
1618

1719
interface Directives {
@@ -42,7 +44,7 @@ function registerStringDirective(name: string, fn: StringDirective): void {
4244
})
4345
}
4446

45-
function runDirective(obj: JSONObject, payload: JSONObject): JSONLike {
47+
function runDirective(obj: JSONObject, payload: JSONObject, statsContext?: StatsContext | undefined): JSONLike {
4648
const name = Object.keys(obj).find((key) => key.startsWith('@')) as string
4749
const directiveFn = directives[name]
4850
const value = obj[name]
@@ -51,6 +53,10 @@ function runDirective(obj: JSONObject, payload: JSONObject): JSONLike {
5153
throw new Error(`${name} is not a valid directive, got ${realTypeOf(directiveFn)}`)
5254
}
5355

56+
if (name === '@liquid') {
57+
return directiveFn(value, payload, statsContext)
58+
}
59+
5460
return directiveFn(value, payload)
5561
}
5662

@@ -326,8 +332,8 @@ registerDirective('@excludeWhenNull', (value, payload) => {
326332
return cleanNulls(resolved)
327333
})
328334

329-
registerDirective('@liquid', (opts, payload) => {
330-
return evaluateLiquid(opts, payload)
335+
registerDirective('@liquid', (opts, payload, statsContext) => {
336+
return evaluateLiquid(opts, payload, statsContext)
331337
})
332338

333339
// Recursively remove all null values from an object
@@ -381,23 +387,40 @@ function getMappingToProcess(mapping: JSONLikeObject): JSONLikeObject {
381387
* @param payload - the input data to apply to the mapping directives
382388
* @todo support arrays or array directives?
383389
*/
384-
function resolve(mapping: JSONLike, payload: JSONObject): JSONLike {
390+
function resolve(mapping: JSONLike, payload: JSONObject, statsContext?: StatsContext | undefined): JSONLike {
385391
if (!isObject(mapping) && !isArray(mapping)) {
386392
return mapping
387393
}
388394

389395
if (isDirective(mapping)) {
396+
if (isLiquidDirective(mapping)) {
397+
// Only include stats, and therefore extra fieldKey tags, if the mapping is a liquid directive to save on costs
398+
return runDirective(mapping, payload, statsContext)
399+
}
400+
390401
return runDirective(mapping, payload)
391402
}
392403

393404
if (Array.isArray(mapping)) {
394-
return mapping.map((value) => resolve(value, payload))
405+
return mapping.map((value) => resolve(value, payload, statsContext))
395406
}
396407

397408
const resolved: JSONLikeObject = {}
398409

399410
for (const key of Object.keys(mapping)) {
400-
resolved[key] = resolve(mapping[key], payload)
411+
let originalTags: string[] = []
412+
const statsTagsExist = statsContext?.tags !== undefined
413+
414+
if (statsTagsExist) {
415+
originalTags = statsContext.tags
416+
statsContext.tags = [...statsContext.tags, `fieldKey:${key}`]
417+
}
418+
419+
resolved[key] = resolve(mapping[key], payload, statsContext)
420+
421+
if (statsTagsExist) {
422+
statsContext.tags = originalTags
423+
}
401424
}
402425

403426
return resolved
@@ -409,7 +432,11 @@ function resolve(mapping: JSONLike, payload: JSONObject): JSONLike {
409432
* @param mapping - the directives and raw values
410433
* @param data - the input data to apply to directives
411434
*/
412-
function transform(mapping: JSONLikeObject, data: InputData | undefined = {}): JSONObject {
435+
function transform(
436+
mapping: JSONLikeObject,
437+
data: InputData | undefined = {},
438+
statsContext?: StatsContext | undefined
439+
): JSONObject {
413440
const realType = realTypeOf(data)
414441
if (realType !== 'object') {
415442
throw new Error(`data must be an object, got ${realType}`)
@@ -420,7 +447,7 @@ function transform(mapping: JSONLikeObject, data: InputData | undefined = {}): J
420447
// throws if the mapping config is invalid
421448
validate(mappingToProcess)
422449

423-
const resolved = resolve(mappingToProcess, data as JSONObject)
450+
const resolved = resolve(mappingToProcess, data as JSONObject, statsContext)
424451
const cleaned = removeUndefined(resolved)
425452

426453
// Cast because we know there are no `undefined` values anymore
@@ -432,7 +459,11 @@ function transform(mapping: JSONLikeObject, data: InputData | undefined = {}): J
432459
* @param mapping - the directives and raw values
433460
* @param data - the array input data to apply to directives
434461
*/
435-
function transformBatch(mapping: JSONLikeObject, data: Array<InputData> | undefined = []): JSONObject[] {
462+
function transformBatch(
463+
mapping: JSONLikeObject,
464+
data: Array<InputData> | undefined = [],
465+
statsContext?: StatsContext | undefined
466+
): JSONObject[] {
436467
const realType = realTypeOf(data)
437468
if (!isArray(data)) {
438469
throw new Error(`data must be an array, got ${realType}`)
@@ -443,7 +474,7 @@ function transformBatch(mapping: JSONLikeObject, data: Array<InputData> | undefi
443474
// throws if the mapping config is invalid
444475
validate(mappingToProcess)
445476

446-
const resolved = data.map((d) => resolve(mappingToProcess, d as JSONObject))
477+
const resolved = data.map((d) => resolve(mappingToProcess, d as JSONObject, statsContext))
447478

448479
// Cast because we know there are no `undefined` values after `removeUndefined`
449480
return removeUndefined(resolved) as JSONObject[]

packages/core/src/mapping-kit/liquid-directive.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Liquid } from 'liquidjs'
2+
import { StatsContext } from '../destination-kit'
23

34
const liquidEngine = new Liquid({
45
renderLimit: 500, // 500 ms
@@ -58,7 +59,7 @@ export function getLiquidKeys(liquidValue: string): string[] {
5859
return liquidEngine.fullVariablesSync(liquidValue)
5960
}
6061

61-
export function evaluateLiquid(liquidValue: any, event: any): string {
62+
export function evaluateLiquid(liquidValue: any, event: any, statsContext?: StatsContext | undefined): string {
6263
if (typeof liquidValue !== 'string') {
6364
// type checking of @liquid directive is done in validate.ts as well
6465
throw new Error('liquid template value must be a string')
@@ -72,7 +73,22 @@ export function evaluateLiquid(liquidValue: any, event: any): string {
7273
throw new Error('liquid template values are limited to 1000 characters')
7374
}
7475

75-
const res = liquidEngine.parseAndRenderSync(liquidValue, event)
76+
let res: string
77+
const start = Date.now()
78+
let status: 'success' | 'fail' = 'success'
79+
80+
try {
81+
res = liquidEngine.parseAndRenderSync(liquidValue, event)
82+
} catch (e) {
83+
status = 'fail'
84+
throw e
85+
} finally {
86+
const duration = Date.now() - start
87+
statsContext?.statsClient?.histogram('liquid.template.evaluation_ms', duration, [
88+
...statsContext.tags,
89+
`result:${status}`
90+
])
91+
}
7692

7793
if (typeof res !== 'string') {
7894
return 'error'

0 commit comments

Comments
 (0)