From 79eb7aebd47855d5dadf09e7b835e3360fcf6bf2 Mon Sep 17 00:00:00 2001 From: Hidetake Iwata Date: Sat, 26 Feb 2022 18:48:48 +0900 Subject: [PATCH] Add `first_review_time_seconds` metric --- README.md | 3 + src/generated/graphql.ts | 2 +- src/pullRequest/metrics.ts | 12 ++++ src/queries/closedPullRequest.ts | 56 ++++++++++++++++++- tests/__snapshots__/run.test.ts.snap | 23 ++++++++ .../pullRequest/fixtures/closedPullRequest.ts | 16 ++++++ 6 files changed, 109 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 73c30564..72e246cf 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,9 @@ It has the following tags: - `base_ref` - `head_ref` - `merged` = `true` or `false` +- `first_review_time_seconds` + - Time from the first review request to the first review + - Available if a pull request has both review request and review - `requested_team` - Team(s) of requested reviewer(s) - `label` diff --git a/src/generated/graphql.ts b/src/generated/graphql.ts index ebebe4c0..392f9435 100644 --- a/src/generated/graphql.ts +++ b/src/generated/graphql.ts @@ -7,7 +7,7 @@ export type ClosedPullRequestQueryVariables = Types.Exact<{ }>; -export type ClosedPullRequestQuery = { __typename?: 'Query', repository?: { __typename?: 'Repository', pullRequest?: { __typename?: 'PullRequest', commits: { __typename?: 'PullRequestCommitConnection', nodes?: Array<{ __typename?: 'PullRequestCommit', commit: { __typename?: 'Commit', authoredDate: string, committedDate: string } } | null> | null } } | null } | null }; +export type ClosedPullRequestQuery = { __typename?: 'Query', rateLimit?: { __typename?: 'RateLimit', cost: number } | null, repository?: { __typename?: 'Repository', pullRequest?: { __typename?: 'PullRequest', commits: { __typename?: 'PullRequestCommitConnection', nodes?: Array<{ __typename?: 'PullRequestCommit', commit: { __typename?: 'Commit', authoredDate: string, committedDate: string } } | null> | null }, reviewRequests: { __typename?: 'PullRequestTimelineItemsConnection', nodes?: Array<{ __typename: 'AddedToProjectEvent' } | { __typename: 'AssignedEvent' } | { __typename: 'AutoMergeDisabledEvent' } | { __typename: 'AutoMergeEnabledEvent' } | { __typename: 'AutoRebaseEnabledEvent' } | { __typename: 'AutoSquashEnabledEvent' } | { __typename: 'AutomaticBaseChangeFailedEvent' } | { __typename: 'AutomaticBaseChangeSucceededEvent' } | { __typename: 'BaseRefChangedEvent' } | { __typename: 'BaseRefDeletedEvent' } | { __typename: 'BaseRefForcePushedEvent' } | { __typename: 'ClosedEvent' } | { __typename: 'CommentDeletedEvent' } | { __typename: 'ConnectedEvent' } | { __typename: 'ConvertToDraftEvent' } | { __typename: 'ConvertedNoteToIssueEvent' } | { __typename: 'CrossReferencedEvent' } | { __typename: 'DemilestonedEvent' } | { __typename: 'DeployedEvent' } | { __typename: 'DeploymentEnvironmentChangedEvent' } | { __typename: 'DisconnectedEvent' } | { __typename: 'HeadRefDeletedEvent' } | { __typename: 'HeadRefForcePushedEvent' } | { __typename: 'HeadRefRestoredEvent' } | { __typename: 'IssueComment' } | { __typename: 'LabeledEvent' } | { __typename: 'LockedEvent' } | { __typename: 'MarkedAsDuplicateEvent' } | { __typename: 'MentionedEvent' } | { __typename: 'MergedEvent' } | { __typename: 'MilestonedEvent' } | { __typename: 'MovedColumnsInProjectEvent' } | { __typename: 'PinnedEvent' } | { __typename: 'PullRequestCommit' } | { __typename: 'PullRequestCommitCommentThread' } | { __typename: 'PullRequestReview' } | { __typename: 'PullRequestReviewThread' } | { __typename: 'PullRequestRevisionMarker' } | { __typename: 'ReadyForReviewEvent' } | { __typename: 'ReferencedEvent' } | { __typename: 'RemovedFromProjectEvent' } | { __typename: 'RenamedTitleEvent' } | { __typename: 'ReopenedEvent' } | { __typename: 'ReviewDismissedEvent' } | { __typename: 'ReviewRequestRemovedEvent' } | { __typename: 'ReviewRequestedEvent', createdAt: string } | { __typename: 'SubscribedEvent' } | { __typename: 'TransferredEvent' } | { __typename: 'UnassignedEvent' } | { __typename: 'UnlabeledEvent' } | { __typename: 'UnlockedEvent' } | { __typename: 'UnmarkedAsDuplicateEvent' } | { __typename: 'UnpinnedEvent' } | { __typename: 'UnsubscribedEvent' } | { __typename: 'UserBlockedEvent' } | null> | null }, reviews: { __typename?: 'PullRequestTimelineItemsConnection', nodes?: Array<{ __typename: 'AddedToProjectEvent' } | { __typename: 'AssignedEvent' } | { __typename: 'AutoMergeDisabledEvent' } | { __typename: 'AutoMergeEnabledEvent' } | { __typename: 'AutoRebaseEnabledEvent' } | { __typename: 'AutoSquashEnabledEvent' } | { __typename: 'AutomaticBaseChangeFailedEvent' } | { __typename: 'AutomaticBaseChangeSucceededEvent' } | { __typename: 'BaseRefChangedEvent' } | { __typename: 'BaseRefDeletedEvent' } | { __typename: 'BaseRefForcePushedEvent' } | { __typename: 'ClosedEvent' } | { __typename: 'CommentDeletedEvent' } | { __typename: 'ConnectedEvent' } | { __typename: 'ConvertToDraftEvent' } | { __typename: 'ConvertedNoteToIssueEvent' } | { __typename: 'CrossReferencedEvent' } | { __typename: 'DemilestonedEvent' } | { __typename: 'DeployedEvent' } | { __typename: 'DeploymentEnvironmentChangedEvent' } | { __typename: 'DisconnectedEvent' } | { __typename: 'HeadRefDeletedEvent' } | { __typename: 'HeadRefForcePushedEvent' } | { __typename: 'HeadRefRestoredEvent' } | { __typename: 'IssueComment' } | { __typename: 'LabeledEvent' } | { __typename: 'LockedEvent' } | { __typename: 'MarkedAsDuplicateEvent' } | { __typename: 'MentionedEvent' } | { __typename: 'MergedEvent' } | { __typename: 'MilestonedEvent' } | { __typename: 'MovedColumnsInProjectEvent' } | { __typename: 'PinnedEvent' } | { __typename: 'PullRequestCommit' } | { __typename: 'PullRequestCommitCommentThread' } | { __typename: 'PullRequestReview', createdAt: string } | { __typename: 'PullRequestReviewThread' } | { __typename: 'PullRequestRevisionMarker' } | { __typename: 'ReadyForReviewEvent' } | { __typename: 'ReferencedEvent' } | { __typename: 'RemovedFromProjectEvent' } | { __typename: 'RenamedTitleEvent' } | { __typename: 'ReopenedEvent' } | { __typename: 'ReviewDismissedEvent' } | { __typename: 'ReviewRequestRemovedEvent' } | { __typename: 'ReviewRequestedEvent' } | { __typename: 'SubscribedEvent' } | { __typename: 'TransferredEvent' } | { __typename: 'UnassignedEvent' } | { __typename: 'UnlabeledEvent' } | { __typename: 'UnlockedEvent' } | { __typename: 'UnmarkedAsDuplicateEvent' } | { __typename: 'UnpinnedEvent' } | { __typename: 'UnsubscribedEvent' } | { __typename: 'UserBlockedEvent' } | null> | null } } | null } | null }; export type CompletedCheckSuiteQueryVariables = Types.Exact<{ node_id: Types.Scalars['ID']; diff --git a/src/pullRequest/metrics.ts b/src/pullRequest/metrics.ts index d8a1d74f..66d76c34 100644 --- a/src/pullRequest/metrics.ts +++ b/src/pullRequest/metrics.ts @@ -134,6 +134,18 @@ export const computePullRequestClosedMetrics = ( points: [[t, t - unixTime(pr.firstCommit.committedDate)]], } ) + + if (pr.firstReviewRequest && pr.firstReview) { + const firstReviewRequestedAt = unixTime(pr.firstReviewRequest.createdAt) + const firstReviewedAt = unixTime(pr.firstReview.createdAt) + series.push({ + host: 'github.com', + tags, + metric: 'github.actions.pull_request_closed.first_review_time_seconds', + type: 'gauge', + points: [[t, firstReviewedAt - firstReviewRequestedAt]], + }) + } } // Datadog treats a tag as combination of values. diff --git a/src/queries/closedPullRequest.ts b/src/queries/closedPullRequest.ts index 733dae04..8e746b31 100644 --- a/src/queries/closedPullRequest.ts +++ b/src/queries/closedPullRequest.ts @@ -3,6 +3,9 @@ import { Octokit } from '../types' const query = /* GraphQL */ ` query closedPullRequest($owner: String!, $name: String!, $number: Int!) { + rateLimit { + cost + } repository(owner: $owner, name: $name) { pullRequest(number: $number) { commits(first: 1) { @@ -13,6 +16,22 @@ const query = /* GraphQL */ ` } } } + reviewRequests: timelineItems(itemTypes: [REVIEW_REQUESTED_EVENT], first: 1) { + nodes { + __typename + ... on ReviewRequestedEvent { + createdAt + } + } + } + reviews: timelineItems(itemTypes: [PULL_REQUEST_REVIEW], first: 1) { + nodes { + __typename + ... on PullRequestReview { + createdAt + } + } + } } } } @@ -23,6 +42,12 @@ export type ClosedPullRequest = { authoredDate: string committedDate: string } + firstReviewRequest?: { + createdAt: string + } + firstReview?: { + createdAt: string + } } export const queryClosedPullRequest = async ( @@ -36,8 +61,35 @@ export const queryClosedPullRequest = async ( if (r.repository.pullRequest.commits.nodes[0] == null) { throw new Error(`commit is null: ${JSON.stringify(r)}`) } - const firstCommit = r.repository.pullRequest.commits.nodes[0].commit return { - firstCommit, + firstCommit: r.repository.pullRequest.commits.nodes[0].commit, + ...findFirstReviewRequest(r), + ...findFirstReview(r), + } +} + +const findFirstReviewRequest = ( + r: ClosedPullRequestQuery +): Pick | undefined => { + if (!r.repository?.pullRequest?.reviewRequests.nodes?.length) { + return undefined + } + if (r.repository.pullRequest.reviewRequests.nodes[0]?.__typename !== 'ReviewRequestedEvent') { + return undefined + } + return { + firstReviewRequest: r.repository.pullRequest.reviewRequests.nodes[0], + } +} + +const findFirstReview = (r: ClosedPullRequestQuery): Pick | undefined => { + if (!r.repository?.pullRequest?.reviews.nodes?.length) { + return undefined + } + if (r.repository.pullRequest.reviews.nodes[0]?.__typename !== 'PullRequestReview') { + return undefined + } + return { + firstReview: r.repository.pullRequest.reviews.nodes[0], } } diff --git a/tests/__snapshots__/run.test.ts.snap b/tests/__snapshots__/run.test.ts.snap index 371002a9..5e9cd612 100644 --- a/tests/__snapshots__/run.test.ts.snap +++ b/tests/__snapshots__/run.test.ts.snap @@ -189,6 +189,29 @@ Array [ ], "type": "gauge", }, + Object { + "host": "github.com", + "metric": "github.actions.pull_request_closed.first_review_time_seconds", + "points": Array [ + Array [ + 1558279233, + 600, + ], + ], + "tags": Array [ + "repository_owner:Codertocat", + "repository_name:Hello-World", + "sender:Codertocat", + "sender_type:User", + "user:Codertocat", + "pull_request_number:2", + "draft:false", + "base_ref:master", + "head_ref:changes", + "merged:false", + ], + "type": "gauge", + }, Object { "host": "github.com", "metric": "github.actions.api_rate_limit.remaining", diff --git a/tests/pullRequest/fixtures/closedPullRequest.ts b/tests/pullRequest/fixtures/closedPullRequest.ts index 5e649efa..ac45b95c 100644 --- a/tests/pullRequest/fixtures/closedPullRequest.ts +++ b/tests/pullRequest/fixtures/closedPullRequest.ts @@ -14,6 +14,22 @@ export const exampleClosedPullRequestQuery: ClosedPullRequestQuery = { }, ], }, + reviewRequests: { + nodes: [ + { + __typename: 'ReviewRequestedEvent', + createdAt: '2019-05-15T15:30:00Z', + }, + ], + }, + reviews: { + nodes: [ + { + __typename: 'PullRequestReview', + createdAt: '2019-05-15T15:40:00Z', + }, + ], + }, }, }, }