From 0da48360768d6cac0331a99e8224a5a6d92e2fb4 Mon Sep 17 00:00:00 2001 From: Vladimir Klimontovich Date: Thu, 30 Nov 2023 14:40:59 -0500 Subject: [PATCH] fix: a couple of fixes for intercom integration; improved tests logging --- .../__tests__/intercom-destination.test.ts | 109 +++++++++++++----- .../__tests__/lib/testing-lib.ts | 29 ++++- libs/core-functions/jest.config.js | 2 +- libs/core-functions/jest.setup.js | 8 ++ .../src/functions/intercom-destination.ts | 17 +-- .../src/functions/lib/json-fetch.ts | 22 ++-- libs/juava/src/log.ts | 31 +++-- types/protocols/functions.d.ts | 13 ++- webapps/console/lib/server/telemetry.ts | 2 +- 9 files changed, 167 insertions(+), 66 deletions(-) create mode 100644 libs/core-functions/jest.setup.js diff --git a/libs/core-functions/__tests__/intercom-destination.test.ts b/libs/core-functions/__tests__/intercom-destination.test.ts index d3e189252..0b38d10fb 100644 --- a/libs/core-functions/__tests__/intercom-destination.test.ts +++ b/libs/core-functions/__tests__/intercom-destination.test.ts @@ -1,38 +1,60 @@ import { testJitsuFunction, TestOptions } from "./lib/testing-lib"; import { IntercomDestinationCredentials } from "../src/meta"; import IntercomDestination from "../src/functions/intercom-destination"; +import { setGlobalLogLevel, setServerLogColoring } from "juava"; + +setServerLogColoring(true); +setGlobalLogLevel("debug"); test("test", async () => { if (!process.env.TEST_INTERCOM_DESTINATION_CONFIG) { console.log("TEST_INTERCOM_DESTINATION_CONFIG is not set, skipping test"); return; } - const opts: TestOptions = { + const email = "dwight.schrute@dunder-mifflin.com"; + const userId = "user-id-ds"; + + const workspaceId = "workspace-id-dm"; + const workspaceName = "Dunder Mifflin"; + const workspaceSlug = "dunder-mifflin"; + const name = "Dwight Schrute"; + + let opts: TestOptions; + opts = { func: IntercomDestination, configEnvVar: "TEST_INTERCOM_DESTINATION_CONFIG", events: [ { type: "identify", - userId: "cleviyagu0000zl13jld7acac", - traits: { - email: "vladimir@jitsu.com", - name: "Vladimir Klimontovich", - }, + userId: userId, + traits: { email, name }, timestamp: "2023-11-28T20:37:14.693Z", sentAt: "2023-11-28T20:37:14.693Z", messageId: "7qfgopt6mo22xk2tqs0tb", - groupId: "cl9sotck40002tt2b18i2x430", + groupId: workspaceId, context: {}, receivedAt: "2023-11-28T20:37:14.799Z", }, { - type: "group", - groupId: "cl9sotck40002tt2b18i2x430", - traits: { - workspaceSlug: "jitsu", - workspaceName: "Jitsu Playground", - workspaceId: "cl9sotck40002tt2b18i2x430", + type: "track", + event: "user_created", + properties: {}, + userId: userId, + timestamp: "2023-11-29T16:55:50.255Z", + sentAt: "2023-11-29T16:55:50.255Z", + messageId: "22ccyzg8enx2duj3bcit8h", + writeKey: "FDiExsmGYePJa651vnN7LyhMQYq972s1:***", + context: { + traits: { email, name, externalId: `ext-${userId}` }, + page: {}, + clientIds: {}, + campaign: {}, }, + }, + { + type: "group", + groupId: workspaceId, + traits: { workspaceSlug, workspaceName, workspaceId, name: workspaceName }, timestamp: "2023-11-28T20:37:14.673Z", sentAt: "2023-11-28T20:37:14.673Z", messageId: "1xdx6pryjnuqgi4jz362j", @@ -41,30 +63,63 @@ test("test", async () => { }, { type: "track", - event: "workspace_access", - properties: { - workspaceSlug: "jitsu", - workspaceName: "Jitsu Playground", - workspaceId: "cl9sotck40002tt2b18i2x430", + event: "workspace_created", + properties: {}, + userId, + timestamp: "2023-11-29T19:02:36.535Z", + sentAt: "2023-11-29T19:02:36.535Z", + messageId: "223drh0xi901pujmvl2kds", + writeKey: "FDiExsmGYePJa651vnN7LyhMQYq972s1:***", + groupId: "clpk4wd6n0000l90fmzoahn63", + context: { + traits: { + workspaceName, + workspaceId, + email, + name, + externalId: `ext-${userId}`, + }, + page: {}, + clientIds: {}, + campaign: {}, + }, + receivedAt: "2023-11-29T19:02:36.643Z", + }, + { + type: "page", + userId, + groupId: workspaceId, + timestamp: "2023-11-29T19:02:36.152Z", + messageId: "1m6c2acu28b1bt4eak2qk1", + context: { + traits: { + email, + name, + externalId: `ext-${userId}`, + }, + page: { + title: "Jitsu", + url: "https://use.jitsu.com/", + path: `/${workspaceSlug}`, + }, }, - userId: "cleviyagu0000zl13jld7acac", + }, + { + type: "track", + event: "workspace_access", + properties: { workspaceSlug, workspaceName, workspaceId }, + userId, anonymousId: "7c3f1bac-2e04-4ebf-8d34-95d56fb126ac", timestamp: "2023-11-28T20:36:42.201Z", sentAt: "2023-11-28T20:36:42.201Z", messageId: "24zebq8rj3414ubowikeup", - groupId: "cl9sotck40002tt2b18i2x430", + groupId: workspaceId, context: { library: { name: "@jitsu/js", version: "0.0.0", }, - traits: { - workspaceSlug: "jitsu", - workspaceName: "Jitsu Playground", - workspaceId: "cl9sotck40002tt2b18i2x430", - email: "vladimir@jitsu.com", - name: "Vladimir Klimontovich", - }, + traits: { workspaceSlug, workspaceName, workspaceId, email, name }, page: {}, clientIds: {}, campaign: {}, diff --git a/libs/core-functions/__tests__/lib/testing-lib.ts b/libs/core-functions/__tests__/lib/testing-lib.ts index fa686722c..a96245449 100644 --- a/libs/core-functions/__tests__/lib/testing-lib.ts +++ b/libs/core-functions/__tests__/lib/testing-lib.ts @@ -1,5 +1,5 @@ import { AnalyticsInterface, AnalyticsServerEvent } from "@jitsu/protocols/analytics"; -import { requireDefined } from "juava"; +import { getLog, logFormat, requireDefined } from "juava"; import { AnyEvent, EventContext, FuncReturn, FunctionContext, JitsuFunction } from "@jitsu/protocols/functions"; import nodeFetch from "node-fetch-commonjs"; import { createStore } from "./mem-store"; @@ -20,6 +20,20 @@ export type TestOptions = { export function prefixLogMessage(level: string, msg: any) { return `[${level}] ${msg}`; } +const testLogger = getLog("function-tester"); + +function toDate(timestamp?: string | number | Date): Date { + if (!timestamp) { + return new Date(); + } + if (typeof timestamp === "string") { + return new Date(timestamp); + } else if (typeof timestamp === "number") { + return new Date(timestamp); + } else { + return timestamp; + } +} export async function testJitsuFunction(opts: TestOptions): Promise { const config = @@ -36,15 +50,20 @@ export async function testJitsuFunction(opts: TestOptions): Promise< const func = opts.func; const fetch = nodeFetch; const log = { - info: (msg: any, ...args: any[]) => console.log(prefixLogMessage("INFO", msg), args), - error: (msg: any, ...args: any[]) => console.error(prefixLogMessage("ERROR", msg), args), - debug: (msg: any, ...args: any[]) => console.debug(prefixLogMessage("DEBUG", msg), args), - warn: (msg: any, ...args: any[]) => console.warn(prefixLogMessage("WARN", msg), args), + info: (msg: any, ...args: any[]) => testLogger.atInfo().log(msg, ...args), + error: (msg: any, ...args: any[]) => testLogger.atError().log(msg, ...args), + debug: (msg: any, ...args: any[]) => testLogger.atDebug().log(msg, ...args), + warn: (msg: any, ...args: any[]) => testLogger.atWarn().log(msg, ...args), }; let res: AnyEvent[] = null; for (const event of events) { try { + testLogger + .atInfo() + .log( + `📌Testing ${logFormat.bold(event.event || event.type)} message of ${toDate(event.timestamp).toISOString()}` + ); const r = await func(event, { props: config, fetch, diff --git a/libs/core-functions/jest.config.js b/libs/core-functions/jest.config.js index 6ef2c4778..de6e6d30e 100644 --- a/libs/core-functions/jest.config.js +++ b/libs/core-functions/jest.config.js @@ -5,5 +5,5 @@ module.exports = { testMatch: ["**/__tests__/**/*.test.ts"], testEnvironment: "node", runner: "jest-runner", - testMatch: ["**/__tests__/**/*.test.ts"], + "setupFiles": ["./jest.setup.js"] }; diff --git a/libs/core-functions/jest.setup.js b/libs/core-functions/jest.setup.js new file mode 100644 index 000000000..847bac649 --- /dev/null +++ b/libs/core-functions/jest.setup.js @@ -0,0 +1,8 @@ + +global.console = { + log: message => process.stdout.write(message + '\n'), + error: console.error, + warn: console.warn, + info: console.info, + debug: console.debug, +}; diff --git a/libs/core-functions/src/functions/intercom-destination.ts b/libs/core-functions/src/functions/intercom-destination.ts index a73c3d98c..f6a035f14 100644 --- a/libs/core-functions/src/functions/intercom-destination.ts +++ b/libs/core-functions/src/functions/intercom-destination.ts @@ -2,7 +2,6 @@ import { FullContext, JitsuFunction } from "@jitsu/protocols/functions"; import { AnalyticsServerEvent } from "@jitsu/protocols/analytics"; import { IntercomDestinationCredentials } from "../meta"; import { JsonFetcher, jsonFetcher } from "./lib/json-fetch"; -import omit from "lodash/omit"; import { isEqual, pick } from "lodash"; import { requireDefined } from "juava"; @@ -210,7 +209,8 @@ async function createOrUpdateContact(event: AnalyticsServerEvent, { jsonFetch, l Accept: "application/json", "Content-Type": "application/json", }, - body: forComparison.external_id ? omit(newContact, "external_id") : newContact, + body: newContact, + //body: forComparison.external_id ? omit(newContact, "external_id") : newContact, }); } return contact.id; @@ -218,7 +218,7 @@ async function createOrUpdateContact(event: AnalyticsServerEvent, { jsonFetch, l } const IntercomDestination: JitsuFunction = async (event, ctx) => { - const jsonFetch = jsonFetcher(ctx.fetch); + const jsonFetch = jsonFetcher(ctx.fetch, { log: ctx.log, debug: true }); let intercomContactId: string | undefined; let intercomCompanyId: string | undefined; if (event.type === "identify") { @@ -230,7 +230,9 @@ const IntercomDestination: JitsuFunction & { body?: any; @@ -34,7 +34,10 @@ export class JsonFetchError extends Error { } } -export function jsonFetcher(fetch: FetchType): JsonFetcher { +export function jsonFetcher( + fetch: FetchType, + { log, debug }: { log: FunctionLogger; debug?: boolean } = { log: console } +): JsonFetcher { return async (url: string, options?: JsonFetchOpts) => { const method = options?.method || (options?.body ? "POST" : "GET"); const bodyStr = @@ -50,16 +53,21 @@ export function jsonFetcher(fetch: FetchType): JsonFetcher { body: bodyStr, }); let responseText = await response.text(); + if (debug) { + const message = `${method} ${url} → ${response.ok ? "🟢" : "🔴"}${response.status} ${response.statusText}.${ + bodyStr ? `\n📨Request body:\n${prettifyJson(bodyStr)}` : "" + }\n📩Response body${responseText ? `: \n${prettifyJson(responseText)}` : " is empty"}`; + if (response.ok) { + log.debug(message); + } else { + log.warn(message); + } + } if (!response.ok) { if (responseText.length > maxResponseLen) { responseText = responseText.substring(0, maxResponseLen) + "...(truncated, total length: " + responseText.length + ")"; } - console.log( - `Request error: ${method} ${url} failed with status ${response.status} ${response.statusText}: ${prettifyJson( - responseText - )}${bodyStr ? `. Request body: ${prettifyJson(bodyStr)}` : ""}` - ); throw new JsonFetchError( response.status, `${method} ${url} failed with status ${response.status} ${response.statusText}: ${responseText}` diff --git a/libs/juava/src/log.ts b/libs/juava/src/log.ts index 01116bed6..aaae7556c 100644 --- a/libs/juava/src/log.ts +++ b/libs/juava/src/log.ts @@ -18,7 +18,7 @@ function getComponent() { } let globalLogLevel: LogLevel = "info" as LogLevel; -let enableServerLogsColoring: boolean = false; +let enableServerLogsColoring: boolean = !(process.env.CI === "1" || process.env.CI === "true"); let enableJsonFormat: boolean = false; export function setGlobalLogLevel(level: LogLevel) { @@ -83,14 +83,23 @@ function inBrowser() { return typeof window !== "undefined"; } -function colorConsoleMessage(color: Color, str: string): string { - return !inBrowser() && enableServerLogsColoring && color !== undefined - ? str - .split("\n") - .map(line => `${colorsAnsi[color]}${line}\x1b[0m`) - .join("\n") - : str; -} +export const logFormat = { + bold(msg: string): string { + return msg; + }, + italic(msg: string): string { + return msg; + }, + color(color: Color, str: string): string { + //to implement bold and italic we should split string by [0m and apply bold/italic to each part + return !inBrowser() && enableServerLogsColoring && color !== undefined + ? str + .split("\n") + .map(line => `${colorsAnsi[color]}${line}\x1b[0m`) + .join("\n") + : str; + }, +}; //process.stdout is not available on Vercel's edge runtime const writeln = process.stdout @@ -125,9 +134,9 @@ function dispatch(msg: LogMessage) { if (enableJsonFormat) { writeln(JSON.stringify({ time: msg.date, level: msg.level, msg: lines.join("\n") })); } else { - const border = "│"; + const border = ""; // = "|"; const messageFormatted = lines.join(`\n${border} `); - writeln(colorConsoleMessage(levelColor, messageFormatted)); + writeln(logFormat.color(levelColor, messageFormatted)); } } msg.dispatched = true; diff --git a/types/protocols/functions.d.ts b/types/protocols/functions.d.ts index 29b731858..253226fda 100644 --- a/types/protocols/functions.d.ts +++ b/types/protocols/functions.d.ts @@ -44,13 +44,14 @@ export type FetchOpts = { headers?: Record; body?: string; }; +export type FunctionLogger = { + info: (message: string, ...args: any[]) => void; + warn: (message: string, ...args: any[]) => void; + debug: (message: string, ...args: any[]) => void; + error: (message: string, ...args: any[]) => void; +}; export type FunctionContext = { - log: { - info: (message: string, ...args: any[]) => void; - warn: (message: string, ...args: any[]) => void; - debug: (message: string, ...args: any[]) => void; - error: (message: string, ...args: any[]) => void; - }; + log: FunctionLogger; fetch: FetchType; store: Store; }; diff --git a/webapps/console/lib/server/telemetry.ts b/webapps/console/lib/server/telemetry.ts index ea75aea29..fe77b8a15 100644 --- a/webapps/console/lib/server/telemetry.ts +++ b/webapps/console/lib/server/telemetry.ts @@ -82,7 +82,7 @@ function createProductAnalytics(analytics: AnalyticsInterface, req?: NextApiRequ if (typeof idOrObject === "string") { return analytics.group( idOrObject, - opts ? { workspaceSlug: opts.slug, workspaceName: opts.name, workspaceId: idOrObject } : {} + opts ? { workspaceSlug: opts.slug, workspaceName: opts.name, name: opts.name, workspaceId: idOrObject } : {} ); } else { return analytics.group(idOrObject.id, {