diff --git a/packages/app/src/cli/services/app-logs/logs-command/ui/components/Logs.test.tsx b/packages/app/src/cli/services/app-logs/logs-command/ui/components/Logs.test.tsx index b774b0a767..603afbef49 100644 --- a/packages/app/src/cli/services/app-logs/logs-command/ui/components/Logs.test.tsx +++ b/packages/app/src/cli/services/app-logs/logs-command/ui/components/Logs.test.tsx @@ -1,6 +1,8 @@ import {Logs} from './Logs.js' import {usePollAppLogs} from './hooks/usePollAppLogs.js' import { + AppLogPayload, + AppLogPrefix, BackgroundExecutionReason, FunctionRunLog, NetworkAccessRequestExecutedLog, @@ -45,114 +47,6 @@ const NETWORK_ACCESS_HTTP_RESPONSE = { }, } -const USE_POLL_APP_LOGS_RETURN_VALUE = { - appLogOutputs: [ - { - appLog: new FunctionRunLog({ - export: 'run', - input: INPUT, - inputBytes: INPUT_BYTES, - output: OUTPUT, - outputBytes: OUTPUT_BYTES, - logs: LOGS, - functionId: FUNCTION_ID, - fuelConsumed: FUEL_CONSUMED, - errorMessage: 'errorMessage', - errorType: 'errorType', - }), - prefix: { - functionId: FUNCTION_ID, - logTimestamp: TIME, - description: `export "run" executed in 0.5124M instructions`, - storeName: 'my-store', - status: STATUS === 'success' ? 'Success' : 'Failure', - source: SOURCE, - }, - }, - { - appLog: new NetworkAccessResponseFromCacheLog({ - cacheEntryEpochMs: 1683904621000, - cacheTtlMs: 300000, - httpRequest: NETWORK_ACCESS_HTTP_REQUEST, - httpResponse: NETWORK_ACCESS_HTTP_RESPONSE, - }), - prefix: { - functionId: FUNCTION_ID, - logTimestamp: TIME, - description: 'network access response from cache', - storeName: 'my-store', - status: 'Success', - source: SOURCE, - }, - }, - { - appLog: new NetworkAccessRequestExecutedLog({ - attempt: 1, - connectTimeMs: 40, - writeReadTimeMs: 40, - httpRequest: NETWORK_ACCESS_HTTP_REQUEST, - httpResponse: NETWORK_ACCESS_HTTP_RESPONSE, - error: null, - }), - prefix: { - functionId: FUNCTION_ID, - logTimestamp: TIME, - description: 'network access request executed in 80 ms', - status: 'Success', - storeName: 'my-store', - source: SOURCE, - }, - }, - { - appLog: new NetworkAccessRequestExecutedLog({ - attempt: 1, - connectTimeMs: null, - writeReadTimeMs: null, - httpRequest: NETWORK_ACCESS_HTTP_REQUEST, - httpResponse: null, - error: 'Timeout Error', - }), - prefix: { - functionId: FUNCTION_ID, - logTimestamp: TIME, - description: 'network access request executed', - storeName: 'my-store', - status: 'Failure', - source: SOURCE, - }, - }, - { - appLog: new NetworkAccessRequestExecutionInBackgroundLog({ - reason: BackgroundExecutionReason.NoCachedResponse, - httpRequest: NETWORK_ACCESS_HTTP_REQUEST, - }), - prefix: { - functionId: FUNCTION_ID, - logTimestamp: TIME, - description: 'network access request executing in background', - storeName: 'my-store', - status: 'Success', - source: SOURCE, - }, - }, - { - appLog: new NetworkAccessRequestExecutionInBackgroundLog({ - reason: BackgroundExecutionReason.CacheAboutToExpire, - httpRequest: NETWORK_ACCESS_HTTP_REQUEST, - }), - prefix: { - functionId: FUNCTION_ID, - logTimestamp: TIME, - description: 'network access request executing in background', - storeName: 'my-store', - status: 'Success', - source: SOURCE, - }, - }, - ], - errors: [], -} - const USE_POLL_APP_LOGS_ERRORS_RETURN_VALUE = { errors: ['Test Error'], appLogOutputs: [], @@ -163,118 +57,448 @@ STORE_NAME_BY_ID.set('1', 'my-store') const EMPTY_FILTERS = {status: undefined, sources: undefined} +const appLogFunctionRunOutputs = ({ + prefix = {}, + appLog = {}, +}: { + prefix?: Partial + appLog?: Partial +} = {}): {appLog: FunctionRunLog; prefix: AppLogPrefix} => { + const defaultPrefix = { + functionId: FUNCTION_ID, + logTimestamp: TIME, + description: `export "run" executed in 0.5124M instructions`, + storeName: 'my-store', + status: STATUS === 'success' ? 'Success' : 'Failure', + source: SOURCE, + } + + const defaultAppLog = { + export: 'run', + input: INPUT, + inputBytes: INPUT_BYTES, + output: OUTPUT, + outputBytes: OUTPUT_BYTES, + logs: LOGS, + functionId: FUNCTION_ID, + fuelConsumed: FUEL_CONSUMED, + errorMessage: 'errorMessage', + errorType: 'errorType', + inputQueryVariablesMetafieldValue: '{"key":"value"}', + inputQueryVariablesMetafieldNamespace: 'inputQueryVariablesMetafieldNamespace', + inputQueryVariablesMetafieldKey: 'inputQueryVariablesMetafieldKey', + } + + const resultPrefix = {...defaultPrefix, ...prefix} + const resultAppLog = {...defaultAppLog, ...appLog} + + return { + appLog: new FunctionRunLog(resultAppLog), + prefix: resultPrefix, + } +} + describe('Logs', () => { - test('renders prefix and applogs', async () => { - // Given - const mockedUsePollAppLogs = vi.fn().mockReturnValue(USE_POLL_APP_LOGS_RETURN_VALUE) - vi.mocked(usePollAppLogs).mockImplementation(mockedUsePollAppLogs) - // When - const renderInstance = render( - , - ) + describe('App Logs', () => { + test('renders FunctionRunLog correctly with metafield data', async () => { + const appLogOutput = appLogFunctionRunOutputs() - // Then - const lastFrame = renderInstance.lastFrame() - - expect(unstyled(lastFrame!)).toMatchInlineSnapshot(` - "2024-06-18 16:02:04.868 my-store my-function Success export \\"run\\" executed in 0.5124M instructions - test logs - Input (10 bytes): - { - \\"test\\": \\"input\\" - } - - Output (10 bytes): - { - \\"test\\": \\"output\\" - } - 2024-06-18 16:02:04.868 my-store my-function Success network access response from cache - Cache write time: 2023-05-12T15:17:01.000Z - Cache TTL: 300 s - HTTP request: - { - \\"url\\": \\"https://api.example.com/hello\\", - \\"method\\": \\"GET\\", - \\"headers\\": {}, - \\"body\\": null, - \\"policy\\": { - \\"read_timeout_ms\\": 500 - } - } - HTTP response: - { - \\"status\\": 200, - \\"body\\": \\"Success\\", - \\"headers\\": { - \\"header1\\": \\"value1\\" - } - } - 2024-06-18 16:02:04.868 my-store my-function Success network access request executed in 80 ms - Attempt: 1 - Connect time: 40 ms - Write read time: 40 ms - HTTP request: - { - \\"url\\": \\"https://api.example.com/hello\\", - \\"method\\": \\"GET\\", - \\"headers\\": {}, - \\"body\\": null, - \\"policy\\": { - \\"read_timeout_ms\\": 500 - } - } - HTTP response: - { - \\"status\\": 200, - \\"body\\": \\"Success\\", - \\"headers\\": { - \\"header1\\": \\"value1\\" - } - } - 2024-06-18 16:02:04.868 my-store my-function Failure network access request executed - Attempt: 1 - HTTP request: - { - \\"url\\": \\"https://api.example.com/hello\\", - \\"method\\": \\"GET\\", - \\"headers\\": {}, - \\"body\\": null, - \\"policy\\": { - \\"read_timeout_ms\\": 500 + const mockedUsePollAppLogs = vi.fn().mockReturnValue({appLogOutputs: [appLogOutput], errors: []}) + vi.mocked(usePollAppLogs).mockImplementation(mockedUsePollAppLogs) + + const renderInstance = render( + , + ) + + const lastFrame = renderInstance.lastFrame() + + expect(unstyled(lastFrame!)).toMatchInlineSnapshot(` + "2024-06-18 16:02:04.868 my-store my-function Success export \\"run\\" executed in 0.5124M instructions + test logs + + Input Query Variables: + + Namespace: inputQueryVariablesMetafieldNamespace + Key: inputQueryVariablesMetafieldKey + + { + \\"key\\": \\"value\\" + } + + Input (10 bytes): + + { + \\"test\\": \\"input\\" + } + + Output (10 bytes): + + { + \\"test\\": \\"output\\" + }" + `) + + renderInstance.unmount() + }) + + test('renders FunctionRunLog correctly with nil metafield value', async () => { + const appLogOutput = appLogFunctionRunOutputs({ + appLog: { + inputQueryVariablesMetafieldValue: null, + }, + }) + const mockedUsePollAppLogs = vi.fn().mockReturnValue({appLogOutputs: [appLogOutput], errors: []}) + vi.mocked(usePollAppLogs).mockImplementation(mockedUsePollAppLogs) + + const renderInstance = render( + , + ) + + const lastFrame = renderInstance.lastFrame() + + expect(unstyled(lastFrame!)).toMatchInlineSnapshot(` + "2024-06-18 16:02:04.868 my-store my-function Success export \\"run\\" executed in 0.5124M instructions + test logs + + Input Query Variables: + + Namespace: inputQueryVariablesMetafieldNamespace + Key: inputQueryVariablesMetafieldKey + + Metafield is not set + + Input (10 bytes): + + { + \\"test\\": \\"input\\" + } + + Output (10 bytes): + + { + \\"test\\": \\"output\\" + }" + `) + + renderInstance.unmount() + }) + + test('redners FunctionRunLog without iqv when key and namespace are null', async () => { + const appLogOutput = appLogFunctionRunOutputs({ + appLog: { + inputQueryVariablesMetafieldValue: null, + inputQueryVariablesMetafieldNamespace: null, + inputQueryVariablesMetafieldKey: null, + }, + }) + + const mockedUsePollAppLogs = vi.fn().mockReturnValue({appLogOutputs: [appLogOutput], errors: []}) + vi.mocked(usePollAppLogs).mockImplementation(mockedUsePollAppLogs) + + const renderInstance = render( + , + ) + + const lastFrame = renderInstance.lastFrame() + + expect(unstyled(lastFrame!)).toMatchInlineSnapshot(` + "2024-06-18 16:02:04.868 my-store my-function Success export \\"run\\" executed in 0.5124M instructions + test logs + + Input (10 bytes): + + { + \\"test\\": \\"input\\" + } + + Output (10 bytes): + + { + \\"test\\": \\"output\\" + }" + `) + + renderInstance.unmount() + }) + }) + + describe('Network Access', () => { + test('renders NetworkAccessResponseFromCacheLog correctly', async () => { + const webhookLogOutput = { + appLog: new NetworkAccessResponseFromCacheLog({ + cacheEntryEpochMs: 1683904621000, + cacheTtlMs: 300000, + httpRequest: NETWORK_ACCESS_HTTP_REQUEST, + httpResponse: NETWORK_ACCESS_HTTP_RESPONSE, + }), + prefix: { + functionId: FUNCTION_ID, + logTimestamp: TIME, + description: 'network access response from cache', + storeName: 'my-store', + status: 'Success', + source: SOURCE, + }, + } + + const mockedUsePollAppLogs = vi.fn().mockReturnValue({appLogOutputs: [webhookLogOutput], errors: []}) + vi.mocked(usePollAppLogs).mockImplementation(mockedUsePollAppLogs) + + const renderInstance = render( + , + ) + + const lastFrame = renderInstance.lastFrame() + + expect(unstyled(lastFrame!)).toMatchInlineSnapshot(` + "2024-06-18 16:02:04.868 my-store my-function Success network access response from cache + Cache write time: 2023-05-12T15:17:01.000Z + Cache TTL: 300 s + HTTP request: + { + \\"url\\": \\"https://api.example.com/hello\\", + \\"method\\": \\"GET\\", + \\"headers\\": {}, + \\"body\\": null, + \\"policy\\": { + \\"read_timeout_ms\\": 500 + } } - } - Error: Timeout Error - 2024-06-18 16:02:04.868 my-store my-function Success network access request executing in background - Reason: No cached response available - HTTP request: - { - \\"url\\": \\"https://api.example.com/hello\\", - \\"method\\": \\"GET\\", - \\"headers\\": {}, - \\"body\\": null, - \\"policy\\": { - \\"read_timeout_ms\\": 500 + HTTP response: + { + \\"status\\": 200, + \\"body\\": \\"Success\\", + \\"headers\\": { + \\"header1\\": \\"value1\\" + } + }" + `) + + renderInstance.unmount() + }) + + test('renders NetworkAccessRequestExecutedLog correctly with success', async () => { + const webhookLogOutput = { + appLog: new NetworkAccessRequestExecutedLog({ + attempt: 1, + connectTimeMs: 40, + writeReadTimeMs: 40, + httpRequest: NETWORK_ACCESS_HTTP_REQUEST, + httpResponse: NETWORK_ACCESS_HTTP_RESPONSE, + error: null, + }), + prefix: { + functionId: FUNCTION_ID, + logTimestamp: TIME, + description: 'network access request executed in 80 ms', + status: 'Success', + storeName: 'my-store', + source: SOURCE, + }, + } + + const mockedUsePollAppLogs = vi.fn().mockReturnValue({appLogOutputs: [webhookLogOutput], errors: []}) + vi.mocked(usePollAppLogs).mockImplementation(mockedUsePollAppLogs) + + const renderInstance = render( + , + ) + + const lastFrame = renderInstance.lastFrame() + + expect(unstyled(lastFrame!)).toMatchInlineSnapshot(` + "2024-06-18 16:02:04.868 my-store my-function Success network access request executed in 80 ms + Attempt: 1 + Connect time: 40 ms + Write read time: 40 ms + HTTP request: + { + \\"url\\": \\"https://api.example.com/hello\\", + \\"method\\": \\"GET\\", + \\"headers\\": {}, + \\"body\\": null, + \\"policy\\": { + \\"read_timeout_ms\\": 500 + } } - } - 2024-06-18 16:02:04.868 my-store my-function Success network access request executing in background - Reason: Cache is about to expire - HTTP request: - { - \\"url\\": \\"https://api.example.com/hello\\", - \\"method\\": \\"GET\\", - \\"headers\\": {}, - \\"body\\": null, - \\"policy\\": { - \\"read_timeout_ms\\": 500 + HTTP response: + { + \\"status\\": 200, + \\"body\\": \\"Success\\", + \\"headers\\": { + \\"header1\\": \\"value1\\" + } + }" + `) + + renderInstance.unmount() + }) + + test('renders NetworkAccessRequestExecutedLog correctly with failure', async () => { + const webhookLogOutput = { + appLog: new NetworkAccessRequestExecutedLog({ + attempt: 1, + connectTimeMs: null, + writeReadTimeMs: null, + httpRequest: NETWORK_ACCESS_HTTP_REQUEST, + httpResponse: null, + error: 'Timeout Error', + }), + prefix: { + functionId: FUNCTION_ID, + logTimestamp: TIME, + description: 'network access request executed', + storeName: 'my-store', + status: 'Failure', + source: SOURCE, + }, + } + + const mockedUsePollAppLogs = vi.fn().mockReturnValue({appLogOutputs: [webhookLogOutput], errors: []}) + vi.mocked(usePollAppLogs).mockImplementation(mockedUsePollAppLogs) + + const renderInstance = render( + , + ) + + const lastFrame = renderInstance.lastFrame() + + expect(unstyled(lastFrame!)).toMatchInlineSnapshot(` + "2024-06-18 16:02:04.868 my-store my-function Failure network access request executed + Attempt: 1 + HTTP request: + { + \\"url\\": \\"https://api.example.com/hello\\", + \\"method\\": \\"GET\\", + \\"headers\\": {}, + \\"body\\": null, + \\"policy\\": { + \\"read_timeout_ms\\": 500 + } } - }" - `) + Error: Timeout Error" + `) - renderInstance.unmount() + renderInstance.unmount() + }) + + test('renders NetworkAccessRequestExecutionInBackgroundLog correctly with NoCachedResponse', async () => { + const webhookLogOutput = { + appLog: new NetworkAccessRequestExecutionInBackgroundLog({ + reason: BackgroundExecutionReason.NoCachedResponse, + httpRequest: NETWORK_ACCESS_HTTP_REQUEST, + }), + prefix: { + functionId: FUNCTION_ID, + logTimestamp: TIME, + description: 'network access request executing in background', + storeName: 'my-store', + status: 'Success', + source: SOURCE, + }, + } + + const mockedUsePollAppLogs = vi.fn().mockReturnValue({appLogOutputs: [webhookLogOutput], errors: []}) + vi.mocked(usePollAppLogs).mockImplementation(mockedUsePollAppLogs) + + const renderInstance = render( + , + ) + + const lastFrame = renderInstance.lastFrame() + + expect(unstyled(lastFrame!)).toMatchInlineSnapshot(` + "2024-06-18 16:02:04.868 my-store my-function Success network access request executing in background + Reason: No cached response available + HTTP request: + { + \\"url\\": \\"https://api.example.com/hello\\", + \\"method\\": \\"GET\\", + \\"headers\\": {}, + \\"body\\": null, + \\"policy\\": { + \\"read_timeout_ms\\": 500 + } + }" + `) + + renderInstance.unmount() + }) + + test('renders NetworkAccessRequestExecutionInBackgroundLog correctly with CacheAboutToExpire', async () => { + const webhookLogOutput = { + appLog: new NetworkAccessRequestExecutionInBackgroundLog({ + reason: BackgroundExecutionReason.CacheAboutToExpire, + httpRequest: NETWORK_ACCESS_HTTP_REQUEST, + }), + prefix: { + functionId: FUNCTION_ID, + logTimestamp: TIME, + description: 'network access request executing in background', + storeName: 'my-store', + status: 'Success', + source: SOURCE, + }, + } + + const mockedUsePollAppLogs = vi.fn().mockReturnValue({appLogOutputs: [webhookLogOutput], errors: []}) + vi.mocked(usePollAppLogs).mockImplementation(mockedUsePollAppLogs) + + const renderInstance = render( + , + ) + + const lastFrame = renderInstance.lastFrame() + + expect(unstyled(lastFrame!)).toMatchInlineSnapshot(` + "2024-06-18 16:02:04.868 my-store my-function Success network access request executing in background + Reason: Cache is about to expire + HTTP request: + { + \\"url\\": \\"https://api.example.com/hello\\", + \\"method\\": \\"GET\\", + \\"headers\\": {}, + \\"body\\": null, + \\"policy\\": { + \\"read_timeout_ms\\": 500 + } + }" + `) + + renderInstance.unmount() + }) }) test('handles errors', async () => { diff --git a/packages/app/src/cli/services/app-logs/logs-command/ui/components/Logs.tsx b/packages/app/src/cli/services/app-logs/logs-command/ui/components/Logs.tsx index 9c505f6930..3c9053c435 100644 --- a/packages/app/src/cli/services/app-logs/logs-command/ui/components/Logs.tsx +++ b/packages/app/src/cli/services/app-logs/logs-command/ui/components/Logs.tsx @@ -63,19 +63,45 @@ const Logs: FunctionComponent = ({pollOptions: {jwtToken, filters}, r {appLog instanceof FunctionRunLog && ( <> {appLog.logs} + {appLog.inputQueryVariablesMetafieldKey && appLog.inputQueryVariablesMetafieldNamespace && ( + + Input Query Variables: + + Namespace: + {appLog.inputQueryVariablesMetafieldNamespace} + + + Key: + {appLog.inputQueryVariablesMetafieldKey} + + + + {prettyPrintJsonIfPossible(appLog.inputQueryVariablesMetafieldValue) || ( + Metafield is not set + )} + + + + )} {appLog.input && ( - <> - Input ({appLog.inputBytes} bytes): - {prettyPrintJsonIfPossible(appLog.input)} - + + + Input ({appLog.inputBytes} bytes): + + + {prettyPrintJsonIfPossible(appLog.input)} + + )} {appLog.output && ( - <> - - {'\n'}Output ({appLog.outputBytes} bytes): + + + Output ({appLog.outputBytes} bytes): - {prettyPrintJsonIfPossible(appLog.output)} - + + {prettyPrintJsonIfPossible(appLog.output)} + + )} )} diff --git a/packages/app/src/cli/services/app-logs/types.ts b/packages/app/src/cli/services/app-logs/types.ts index c0eb7e550d..b2b2dc7ce2 100644 --- a/packages/app/src/cli/services/app-logs/types.ts +++ b/packages/app/src/cli/services/app-logs/types.ts @@ -37,6 +37,9 @@ export class FunctionRunLog { fuelConsumed: number errorMessage: string | null errorType: string | null + inputQueryVariablesMetafieldValue: unknown + inputQueryVariablesMetafieldNamespace: string | null + inputQueryVariablesMetafieldKey: string | null constructor({ export: exportValue, @@ -49,6 +52,9 @@ export class FunctionRunLog { fuelConsumed, errorMessage, errorType, + inputQueryVariablesMetafieldValue, + inputQueryVariablesMetafieldNamespace, + inputQueryVariablesMetafieldKey, }: { export: string input: unknown @@ -60,6 +66,9 @@ export class FunctionRunLog { fuelConsumed: number errorMessage: string | null errorType: string | null + inputQueryVariablesMetafieldValue: unknown + inputQueryVariablesMetafieldNamespace: string | null + inputQueryVariablesMetafieldKey: string | null }) { this.export = exportValue this.input = input @@ -71,6 +80,9 @@ export class FunctionRunLog { this.fuelConsumed = fuelConsumed this.errorMessage = errorMessage this.errorType = errorType + this.inputQueryVariablesMetafieldValue = inputQueryVariablesMetafieldValue + this.inputQueryVariablesMetafieldNamespace = inputQueryVariablesMetafieldNamespace + this.inputQueryVariablesMetafieldKey = inputQueryVariablesMetafieldKey } } diff --git a/packages/app/src/cli/services/app-logs/utils.ts b/packages/app/src/cli/services/app-logs/utils.ts index a41288f881..79f0a448ca 100644 --- a/packages/app/src/cli/services/app-logs/utils.ts +++ b/packages/app/src/cli/services/app-logs/utils.ts @@ -32,6 +32,11 @@ export const REQUEST_EXECUTION_IN_BACKGROUND_CACHE_ABOUT_TO_EXPIRE_REASON = 'cac export function parseFunctionRunPayload(payload: string): FunctionRunLog { const parsedPayload = JSON.parse(payload) + + const parsedIqvValue = + parsedPayload.input_query_variables_metafield_value && + parseJson(parsedPayload.input_query_variables_metafield_value) + return new FunctionRunLog({ export: parsedPayload.export, input: parsedPayload.input, @@ -43,6 +48,9 @@ export function parseFunctionRunPayload(payload: string): FunctionRunLog { fuelConsumed: parsedPayload.fuel_consumed, errorMessage: parsedPayload.error_message, errorType: parsedPayload.error_type, + inputQueryVariablesMetafieldValue: parsedIqvValue, + inputQueryVariablesMetafieldNamespace: parsedPayload.input_query_variables_metafield_namespace, + inputQueryVariablesMetafieldKey: parsedPayload.input_query_variables_metafield_key, }) } @@ -192,6 +200,12 @@ export const toFormattedAppLogJson = ({ if (appLogPayload instanceof FunctionRunLog) { toSaveData.payload.logs = appLogPayload.logs.split('\n').filter(Boolean) + + if (toSaveData.payload.inputQueryVariablesMetafieldValue) { + toSaveData.payload.inputQueryVariablesMetafieldValue = parseJson( + toSaveData.payload.inputQueryVariablesMetafieldValue, + ) + } } if (prettyPrint) { @@ -252,3 +266,12 @@ export function prettyPrintJsonIfPossible(json: unknown) { throw new Error(`Error parsing JSON: ${error as string}`) } } + +const parseJson = (json: string): unknown => { + try { + return JSON.parse(json) + // eslint-disable-next-line no-catch-all/no-catch-all + } catch (error) { + return json + } +}