Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
/* eslint-disable @typescript-eslint/consistent-type-definitions */
import * as Types from './types.js'

import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core'

export type AppLogsQueryVariables = Types.Exact<{
apiKey: Types.Scalars['String']['input']
startTime: Types.Scalars['DateTime']['input']
endTime: Types.Scalars['DateTime']['input']
types?: Types.InputMaybe<Types.Scalars['String']['input'][] | Types.Scalars['String']['input']>
status?: Types.InputMaybe<Types.Scalars['String']['input']>
target?: Types.InputMaybe<Types.Scalars['String']['input']>
shopIds?: Types.InputMaybe<Types.Scalars['Int']['input'][] | Types.Scalars['Int']['input']>
first?: Types.InputMaybe<Types.Scalars['Int']['input']>
after?: Types.InputMaybe<Types.Scalars['String']['input']>
}>

export interface AppLogNode {
id: string
timestamp: string
type: string
status: string | null
target: string | null
shopDomain: string | null
executionDurationMs: number | null
payload: Record<string, unknown> | null
}

export interface AppLogEdge {
node: AppLogNode
cursor: string
}

export interface AppLogsQueryResult {
appLogs: {
edges: AppLogEdge[]
pageInfo: {
hasNextPage: boolean
endCursor: string | null
}
}
}

export const AppLogsQuery = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'query',
name: {kind: 'Name', value: 'AppLogsQuery'},
variableDefinitions: [
{
kind: 'VariableDefinition',
variable: {kind: 'Variable', name: {kind: 'Name', value: 'apiKey'}},
type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}}},
},
{
kind: 'VariableDefinition',
variable: {kind: 'Variable', name: {kind: 'Name', value: 'startTime'}},
type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'DateTime'}}},
},
{
kind: 'VariableDefinition',
variable: {kind: 'Variable', name: {kind: 'Name', value: 'endTime'}},
type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'DateTime'}}},
},
{
kind: 'VariableDefinition',
variable: {kind: 'Variable', name: {kind: 'Name', value: 'types'}},
type: {
kind: 'ListType',
type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'AppLogEventType'}}},
},
},
{
kind: 'VariableDefinition',
variable: {kind: 'Variable', name: {kind: 'Name', value: 'status'}},
type: {kind: 'NamedType', name: {kind: 'Name', value: 'AppLogStatus'}},
},
{
kind: 'VariableDefinition',
variable: {kind: 'Variable', name: {kind: 'Name', value: 'target'}},
type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}},
},
{
kind: 'VariableDefinition',
variable: {kind: 'Variable', name: {kind: 'Name', value: 'shopIds'}},
type: {
kind: 'ListType',
type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'Int'}}},
},
},
{
kind: 'VariableDefinition',
variable: {kind: 'Variable', name: {kind: 'Name', value: 'first'}},
type: {kind: 'NamedType', name: {kind: 'Name', value: 'Int'}},
},
{
kind: 'VariableDefinition',
variable: {kind: 'Variable', name: {kind: 'Name', value: 'after'}},
type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: {kind: 'Name', value: 'appLogs'},
arguments: [
{
kind: 'Argument',
name: {kind: 'Name', value: 'apiKey'},
value: {kind: 'Variable', name: {kind: 'Name', value: 'apiKey'}},
},
{
kind: 'Argument',
name: {kind: 'Name', value: 'startTime'},
value: {kind: 'Variable', name: {kind: 'Name', value: 'startTime'}},
},
{
kind: 'Argument',
name: {kind: 'Name', value: 'endTime'},
value: {kind: 'Variable', name: {kind: 'Name', value: 'endTime'}},
},
{
kind: 'Argument',
name: {kind: 'Name', value: 'types'},
value: {kind: 'Variable', name: {kind: 'Name', value: 'types'}},
},
{
kind: 'Argument',
name: {kind: 'Name', value: 'status'},
value: {kind: 'Variable', name: {kind: 'Name', value: 'status'}},
},
{
kind: 'Argument',
name: {kind: 'Name', value: 'target'},
value: {kind: 'Variable', name: {kind: 'Name', value: 'target'}},
},
{
kind: 'Argument',
name: {kind: 'Name', value: 'shopIds'},
value: {kind: 'Variable', name: {kind: 'Name', value: 'shopIds'}},
},
{
kind: 'Argument',
name: {kind: 'Name', value: 'first'},
value: {kind: 'Variable', name: {kind: 'Name', value: 'first'}},
},
{
kind: 'Argument',
name: {kind: 'Name', value: 'after'},
value: {kind: 'Variable', name: {kind: 'Name', value: 'after'}},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: {kind: 'Name', value: 'edges'},
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: {kind: 'Name', value: 'node'},
selectionSet: {
kind: 'SelectionSet',
selections: [
{kind: 'Field', name: {kind: 'Name', value: 'id'}},
{kind: 'Field', name: {kind: 'Name', value: 'timestamp'}},
{kind: 'Field', name: {kind: 'Name', value: 'type'}},
{kind: 'Field', name: {kind: 'Name', value: 'status'}},
{kind: 'Field', name: {kind: 'Name', value: 'target'}},
{kind: 'Field', name: {kind: 'Name', value: 'shopDomain'}},
{kind: 'Field', name: {kind: 'Name', value: 'executionDurationMs'}},
{kind: 'Field', name: {kind: 'Name', value: 'payload'}},
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
],
},
},
{kind: 'Field', name: {kind: 'Name', value: 'cursor'}},
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
],
},
},
{
kind: 'Field',
name: {kind: 'Name', value: 'pageInfo'},
selectionSet: {
kind: 'SelectionSet',
selections: [
{kind: 'Field', name: {kind: 'Name', value: 'hasNextPage'}},
{kind: 'Field', name: {kind: 'Name', value: 'endCursor'}},
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
],
},
},
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
],
},
},
],
},
},
],
} as unknown as DocumentNode<AppLogsQueryResult, AppLogsQueryVariables>
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
query AppLogsQuery(
$apiKey: String!
$startTime: DateTime!
$endTime: DateTime!
$types: [AppLogEventType!]
$status: AppLogStatus
$target: String
$shopIds: [Int!]
$first: Int
$after: String
) {
appLogs(
apiKey: $apiKey
startTime: $startTime
endTime: $endTime
types: $types
status: $status
target: $target
shopIds: $shopIds
first: $first
after: $after
) {
edges {
node {
id
timestamp
type
status
target
shopDomain
executionDurationMs
payload
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
105 changes: 105 additions & 0 deletions packages/app/src/cli/commands/app/app-logs/query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import {queryLogs, queryLogsDirect} from '../../../services/app-logs/query-logs.js'
import {appFlags} from '../../../flags.js'
import AppLinkedCommand, {AppLinkedCommandOutput} from '../../../utilities/app-linked-command.js'
import {linkedAppContext} from '../../../services/app-context.js'
import {Flags} from '@oclif/core'
import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli'

export default class LogsQuery extends AppLinkedCommand {
static summary = 'Query app logs from Tracify/ClickHouse (all event types).'

static descriptionWithMarkdown = `
Queries app runtime logs from the Tracify-backed API. Unlike \`shopify app logs\` (which only supports Functions via polling),
this command supports all event types: webhooks, functions, GraphQL requests, REST requests, app events, and billing events.

Fetches logs for a given time range (default: last 1 hour) and outputs them as JSON.

\`\`\`
shopify app logs query --type=WEBHOOK_DELIVERY --status=FAILURE --last=2h
shopify app logs query --type=FUNCTION_RUN --target=run --first=20
shopify app logs query --api-key=YOUR_APP_KEY --type=WEBHOOK_DELIVERY --last=1h
\`\`\`
`

static description = this.descriptionWithoutMarkdown()

static flags = {
...globalFlags,
...appFlags,
...jsonFlag,
'api-key': Flags.string({
description: 'App API key (bypasses app linking — useful for testing).',
env: 'SHOPIFY_FLAG_API_KEY',
}),
type: Flags.string({
description:
'Filter by event type (WEBHOOK_DELIVERY, FUNCTION_RUN, GRAPHQL_REQUEST, REST_REQUEST, APP_EVENT, APP_BILLING_EVENT).',
multiple: true,
options: [
'WEBHOOK_DELIVERY',
'FUNCTION_RUN',
'GRAPHQL_REQUEST',
'REST_REQUEST',
'APP_EVENT',
'APP_BILLING_EVENT',
],
}),
status: Flags.string({
description: 'Filter by status.',
options: ['SUCCESS', 'FAILURE'],
}),
target: Flags.string({
description: 'Filter by target (e.g., webhook topic like "orders/create").',
}),
last: Flags.string({
description: 'Time range to look back (e.g., "1h", "30m", "2h"). Default: "1h".',
default: '1h',
}),
first: Flags.integer({
description: 'Number of results to return (max 1000).',
default: 50,
}),
after: Flags.string({
description: "Cursor for pagination (from previous query's endCursor).",
}),
}

public async run(): Promise<AppLinkedCommandOutput> {
const {flags} = await this.parse(LogsQuery)

// Direct mode: bypass app linking, just fire the query with an API key
if (flags['api-key']) {
await queryLogsDirect({
apiKey: flags['api-key'],
types: flags.type,
status: flags.status,
target: flags.target,
last: flags.last,
first: flags.first,
after: flags.after,
json: flags.json,
})
return {app: undefined as any}
}

const appContextResult = await linkedAppContext({
directory: flags.path,
clientId: flags['client-id'],
forceRelink: flags.reset,
userProvidedConfigName: flags.config,
})

await queryLogs({
...appContextResult,
types: flags.type,
status: flags.status,
target: flags.target,
last: flags.last,
first: flags.first,
after: flags.after,
json: flags.json,
})

return {app: appContextResult.app}
}
}
2 changes: 2 additions & 0 deletions packages/app/src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import DemoWatcher from './commands/app/demo/watcher.js'
import Deploy from './commands/app/deploy.js'
import Dev from './commands/app/dev.js'
import Logs from './commands/app/logs.js'
import LogsQuery from './commands/app/app-logs/query.js'
import Sources from './commands/app/app-logs/sources.js'
import EnvPull from './commands/app/env/pull.js'
import EnvShow from './commands/app/env/show.js'
Expand Down Expand Up @@ -49,6 +50,7 @@ export const commands: {[key: string]: typeof AppLinkedCommand | typeof AppUnlin
'app:dev': Dev,
'app:dev:clean': DevClean,
'app:logs': Logs,
'app:logs:query': LogsQuery,
'app:logs:sources': Sources,
'app:import-custom-data-definitions': ImportCustomDataDefinitions,
'app:import-extensions': ImportExtensions,
Expand Down
Loading
Loading