Skip to content
Merged
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
99 changes: 64 additions & 35 deletions README.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ inputs:
description: "Shows correlation graphs"
required: false
default: false
SHOW_ACTIVITY_TIME_GRAPHS:
description: "Shows activity time graphs"
required: false
default: false

outputs:
JSON_COLLECTION:
Expand Down
18,012 changes: 17,681 additions & 331 deletions build/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pull-request-analytics-action",
"version": "4.3.0",
"version": "4.4.0",
"description": "Generates detailed PR analytics reports within GitHub, focusing on review efficiency and team performance.",
"main": "build/index.js",
"scripts": {
Expand Down
1 change: 0 additions & 1 deletion src/analytics/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export { sendActionRun } from "./sendActionRun";
export { sendActionError } from "./sendActionError";
export { sendDiscussionUsage } from "./sendDiscussionUsage";
2 changes: 2 additions & 0 deletions src/analytics/sendActionError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export const sendActionError = (error: Error) => {
),
MERGE_TIME_INTERVALS: getMultipleValuesInput("MERGE_TIME_INTERVALS"),
USE_CHARTS: getValueAsIs("USE_CHARTS"),
SHOW_CORRELATION_GRAPHS: getValueAsIs("SHOW_CORRELATION_GRAPHS"),
SHOW_ACTIVITY_TIME_GRAPHS: getValueAsIs("SHOW_ACTIVITY_TIME_GRAPHS"),
Comment on lines +44 to +45
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

💡 Codebase verification

Add type definitions for the new analytics properties

The properties are consistently used across the codebase in:

  • Action configuration (action.yml)
  • Documentation (README.md)
  • Analytics tracking (sendActionRun.ts, sendActionError.ts)
  • Output generation (createOutput.ts)
  • Configuration utilities (createConfigParamsCode.ts)

However, no type definitions were found for these properties. Add them to the analytics event interface:

interface AnalyticsEvent {
  // ... existing properties
  SHOW_CORRELATION_GRAPHS?: string;
  SHOW_ACTIVITY_TIME_GRAPHS?: string;
}
🔗 Analysis chain

LGTM! Consider adding type definitions.

The new analytics properties follow the existing implementation pattern correctly. However, consider adding these properties to your analytics event type definitions for better type safety.

Let's verify the consistency of these property names across the codebase:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for consistent usage of the new property names
# Expected: Property names should match exactly in all occurrences

echo "Checking SHOW_CORRELATION_GRAPHS usage:"
rg "SHOW_CORRELATION_GRAPHS" -A 2

echo "Checking SHOW_ACTIVITY_TIME_GRAPHS usage:"
rg "SHOW_ACTIVITY_TIME_GRAPHS" -A 2

# Check if these properties are defined in any type definitions
echo "Checking for type definitions:"
fd -e ts -e d.ts --exec ast-grep --pattern 'interface $_ {
  $$$
  SHOW_CORRELATION_GRAPHS?: $_
  $$$
}'

fd -e ts -e d.ts --exec ast-grep --pattern 'interface $_ {
  $$$
  SHOW_ACTIVITY_TIME_GRAPHS?: $_
  $$$
}'

Length of output: 8152

});
} else {
mixpanel.track("Anonymous action error", { distinct_id: "anonymous" });
Expand Down
2 changes: 2 additions & 0 deletions src/analytics/sendActionRun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export const sendActionRun = () => {
),
MERGE_TIME_INTERVALS: getMultipleValuesInput("MERGE_TIME_INTERVALS"),
USE_CHARTS: getValueAsIs("USE_CHARTS"),
SHOW_CORRELATION_GRAPHS: getValueAsIs("SHOW_CORRELATION_GRAPHS"),
SHOW_ACTIVITY_TIME_GRAPHS: getValueAsIs("SHOW_ACTIVITY_TIME_GRAPHS"),
Comment on lines +42 to +43
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Consider adding type safety for analytics properties.

To improve maintainability and prevent potential issues, consider defining an interface for the analytics properties.

interface ActionAnalytics {
  distinct_id: string;
  SHOW_CORRELATION_GRAPHS: string;
  SHOW_ACTIVITY_TIME_GRAPHS: string;
  // ... other existing properties
}

});
} else {
mixpanel.track("Anomymous action run", { distinct_id: "anonymous" });
Expand Down
41 changes: 0 additions & 41 deletions src/analytics/sendDiscussionUsage.ts

This file was deleted.

7 changes: 7 additions & 0 deletions src/converters/collectData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
preparePullRequestTimeline,
prepareResponseTime,
prepareRequestedReviews,
prepareActionsTime,
} from "./utils";
import {
invalidUserLogin,
Expand Down Expand Up @@ -47,6 +48,12 @@ export const collectData = (
)
);

prepareActionsTime(
pullRequest,
data.events[index]?.filter((el) => el),
collection
);
Comment on lines +51 to +55
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider enhancing error handling and event filtering

The current implementation has a few areas that could be improved:

  1. The event filtering filter((el) => el) only removes falsy values. Consider being more explicit about which events are needed.
  2. There's no error handling around the function call.

Consider applying this improvement:

-    prepareActionsTime(
-      pullRequest,
-      data.events[index]?.filter((el) => el),
-      collection
-    );
+    try {
+      prepareActionsTime(
+        pullRequest,
+        data.events[index]?.filter((el) => el?.event), // Be explicit about required fields
+        collection
+      );
+    } catch (error) {
+      console.error('Failed to prepare actions time:', error);
+      // Consider how errors should affect the overall collection
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
prepareActionsTime(
pullRequest,
data.events[index]?.filter((el) => el),
collection
);
try {
prepareActionsTime(
pullRequest,
data.events[index]?.filter((el) => el?.event), // Be explicit about required fields
collection
);
} catch (error) {
console.error('Failed to prepare actions time:', error);
// Consider how errors should affect the overall collection
}

🧹 Nitpick (assertive)

💡 Codebase verification

Add tests for the prepareActionsTime functionality

Based on the codebase analysis:

  • The repository has an established test structure with several test files in src/converters/utils/calculations/
  • prepareActionsTime is a critical utility function in src/converters/utils/prepareActionsTime.ts
  • No existing tests were found for this functionality

Please add test coverage for:

  • src/converters/utils/prepareActionsTime.ts
  • Integration test in src/converters/collectData.ts where prepareActionsTime is used
🔗 Analysis chain

Add tests for the new functionality

The PR objectives indicate that no tests were added. Since this is adding new functionality for user activity visualization, it's important to have test coverage.

Let's check the existing test coverage:

Would you like me to help create test cases for:

  1. The prepareActionsTime integration in collectData
  2. Various scenarios of event filtering
  3. Error handling cases
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check test coverage for the collectData functionality
# Look for test files related to collectData and actionsTime

echo "Searching for related test files..."
fd -e test.ts -e spec.ts . | grep -i "collect\|action"

echo "Checking for existing test patterns..."
rg -l "describe.*collectData|test.*prepareActionsTime" 

Length of output: 281


Script:

#!/bin/bash
# Let's try a broader search for test files and patterns

# First, let's find all test files in the repository
echo "All test files in the repository:"
fd -e test.ts -e spec.ts

# Look for any mentions of collectData or prepareActionsTime in the codebase
echo -e "\nSearching for collectData references:"
rg "collectData" -A 2 -B 2

echo -e "\nSearching for prepareActionsTime references:"
rg "prepareActionsTime" -A 2 -B 2

# Let's also check the implementation to understand the scope
echo -e "\nChecking implementation of prepareActionsTime:"
ast-grep --pattern 'function prepareActionsTime($_) { $$$ }'

Length of output: 6707


const closedDate = pullRequest.closed_at
? parseISO(pullRequest.closed_at)
: null;
Expand Down
10 changes: 10 additions & 0 deletions src/converters/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,16 @@ export type Collection = {
commentsConducted?: number;
discussions?: Discussion;
discussionsTypes?: DiscussionType;
actionsTime?: Record<
string,
{
opened?: number;
merged?: number;
approved?: number;
changes_requested?: number;
commented?: number;
}
>;
Comment on lines +85 to +94
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Add JSDoc comments to document the property.

Adding documentation would help other developers understand the purpose and format of the timestamps.

Consider adding documentation:

+ /**
+  * Records timestamps of different PR actions throughout the day.
+  * The outer key represents the hour of the day (0-23).
+  * The inner object contains counts of different actions performed during that hour.
+  * All timestamp values should be in UTC to ensure consistent tracking across timezones.
+  */
  actionsTime?: Record<
    string,
    {
      opened?: number;
      merged?: number;
      approved?: number;
      changes_requested?: number;
      commented?: number;
    }
  >;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
actionsTime?: Record<
string,
{
opened?: number;
merged?: number;
approved?: number;
changes_requested?: number;
commented?: number;
}
>;
/**
* Records timestamps of different PR actions throughout the day.
* The outer key represents the hour of the day (0-23).
* The inner object contains counts of different actions performed during that hour.
* All timestamp values should be in UTC to ensure consistent tracking across timezones.
*/
actionsTime?: Record<
string,
{
opened?: number;
merged?: number;
approved?: number;
changes_requested?: number;
commented?: number;
}
>;

🧹 Nitpick (assertive)

Consider using a union type for action keys.

The type structure is well-designed for tracking timestamps of different PR actions. However, using string as the key type allows any string value, which could lead to inconsistencies.

Consider using a union type to restrict possible keys:

+ type ActionType = 'opened' | 'merged' | 'approved' | 'changes_requested' | 'commented';
  actionsTime?: Record<
-   string,
+   ActionType,
    {
      opened?: number;
      merged?: number;
      approved?: number;
      changes_requested?: number;
      commented?: number;
    }
  >;

Committable suggestion skipped: line range outside the PR's diff.

prSizes?: string[];
sizes?: Record<
string,
Expand Down
1 change: 1 addition & 0 deletions src/converters/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export { prepareDiscussions } from "./prepareDiscussions";
export { prepareReviews } from "./prepareReviews";
export { prepareResponseTime } from "./prepareResponseTime";
export { prepareRequestedReviews } from "./prepareRequestedReviews";
export { prepareActionsTime } from "./prepareActionsTime";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

⚠️ Potential issue

Add unit tests for the new functionality.

According to the PR checklist, tests haven't been added for the new changes. Since prepareActionsTime is a crucial component for the new user activity visualization feature, it should be thoroughly tested to ensure reliability.

Would you like me to help generate unit tests for the prepareActionsTime functionality? I can create a test suite that covers:

  • Input validation
  • Time calculation accuracy
  • Edge cases handling
  • Integration with the collection object

64 changes: 64 additions & 0 deletions src/converters/utils/prepareActionsTime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { getHours, parseISO } from "date-fns";
import { makeComplexRequest } from "../../requests";
import { Collection } from "../types";
import { get, set } from "lodash";
import { invalidUserLogin, reviewedTimelineEvent } from "../constants";

export const prepareActionsTime = (
pullRequest: Awaited<
ReturnType<typeof makeComplexRequest>
>["pullRequestInfo"][number],
events: any[] | undefined = [],
collection: Record<string, Record<string, Collection>>
Comment on lines +11 to +12
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider adding proper TypeScript types for the events parameter

The events parameter is typed as any[], which bypasses TypeScript's type checking benefits. Consider creating an interface for the event structure to improve type safety and code maintainability.

-  events: any[] | undefined = [],
+  events: {
+    event: string;
+    submitted_at?: string;
+    state: string;
+    user?: { login: string };
+  }[] | undefined = [],

Committable suggestion skipped: line range outside the PR's diff.

) => {
const openingHour = pullRequest?.created_at
? getHours(parseISO(pullRequest?.created_at))
: -1;
const mergingHour = pullRequest?.merged_at
? getHours(parseISO(pullRequest?.merged_at))
: -1;
Comment on lines +14 to +19
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Extract common hour parsing logic and define constants

The hour extraction logic is duplicated and uses a magic number (-1) for invalid hours. Consider refactoring to improve maintainability.

+const INVALID_HOUR = -1;
+
+const parseEventHour = (dateString?: string): number => {
+  return dateString ? getHours(parseISO(dateString)) : INVALID_HOUR;
+};
+
-  const openingHour = pullRequest?.created_at
-    ? getHours(parseISO(pullRequest?.created_at))
-    : -1;
-  const mergingHour = pullRequest?.merged_at
-    ? getHours(parseISO(pullRequest?.merged_at))
-    : -1;
+  const openingHour = parseEventHour(pullRequest?.created_at);
+  const mergingHour = parseEventHour(pullRequest?.merged_at);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const openingHour = pullRequest?.created_at
? getHours(parseISO(pullRequest?.created_at))
: -1;
const mergingHour = pullRequest?.merged_at
? getHours(parseISO(pullRequest?.merged_at))
: -1;
const INVALID_HOUR = -1;
const parseEventHour = (dateString?: string): number => {
return dateString ? getHours(parseISO(dateString)) : INVALID_HOUR;
};
const openingHour = parseEventHour(pullRequest?.created_at);
const mergingHour = parseEventHour(pullRequest?.merged_at);


events
?.filter((el) => el.event === reviewedTimelineEvent)
.map((el) => {
const submitHour = el?.submitted_at
? getHours(parseISO(el?.submitted_at))
: -1;
if (submitHour !== -1) {
const keys = ["total", "total", "actionsTime", submitHour, el.state];
set(collection, keys, (get(collection, keys, 0) as number) + 1);
const userKeys = [
el?.user?.login || invalidUserLogin,
"total",
"actionsTime",
submitHour,
el.state,
];
set(collection, userKeys, (get(collection, userKeys, 0) as number) + 1);
}
});
Comment on lines +21 to +39
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Refactor event processing to use forEach and extract collection update logic

The current implementation has two issues:

  1. Using map for side effects instead of transformation
  2. Duplicated collection update logic
+const updateActionCount = (
+  collection: Record<string, Record<string, Collection>>,
+  hour: number,
+  state: string,
+  userLogin: string
+) => {
+  const totalKeys = ["total", "total", "actionsTime", hour, state];
+  const userKeys = [userLogin, "total", "actionsTime", hour, state];
+  
+  set(collection, totalKeys, (get(collection, totalKeys, 0) as number) + 1);
+  set(collection, userKeys, (get(collection, userKeys, 0) as number) + 1);
+};
+
 events
   ?.filter((el) => el.event === reviewedTimelineEvent)
-  .map((el) => {
+  .forEach((el) => {
     const submitHour = el?.submitted_at
       ? getHours(parseISO(el?.submitted_at))
       : -1;
     if (submitHour !== -1) {
-      const keys = ["total", "total", "actionsTime", submitHour, el.state];
-      set(collection, keys, (get(collection, keys, 0) as number) + 1);
-      const userKeys = [
-        el?.user?.login || invalidUserLogin,
-        "total",
-        "actionsTime",
-        submitHour,
-        el.state,
-      ];
-      set(collection, userKeys, (get(collection, userKeys, 0) as number) + 1);
+      updateActionCount(
+        collection,
+        submitHour,
+        el.state,
+        el?.user?.login || invalidUserLogin
+      );
     }
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
events
?.filter((el) => el.event === reviewedTimelineEvent)
.map((el) => {
const submitHour = el?.submitted_at
? getHours(parseISO(el?.submitted_at))
: -1;
if (submitHour !== -1) {
const keys = ["total", "total", "actionsTime", submitHour, el.state];
set(collection, keys, (get(collection, keys, 0) as number) + 1);
const userKeys = [
el?.user?.login || invalidUserLogin,
"total",
"actionsTime",
submitHour,
el.state,
];
set(collection, userKeys, (get(collection, userKeys, 0) as number) + 1);
}
});
const updateActionCount = (
collection: Record<string, Record<string, Collection>>,
hour: number,
state: string,
userLogin: string
) => {
const totalKeys = ["total", "total", "actionsTime", hour, state];
const userKeys = [userLogin, "total", "actionsTime", hour, state];
set(collection, totalKeys, (get(collection, totalKeys, 0) as number) + 1);
set(collection, userKeys, (get(collection, userKeys, 0) as number) + 1);
};
events
?.filter((el) => el.event === reviewedTimelineEvent)
.forEach((el) => {
const submitHour = el?.submitted_at
? getHours(parseISO(el?.submitted_at))
: -1;
if (submitHour !== -1) {
updateActionCount(
collection,
submitHour,
el.state,
el?.user?.login || invalidUserLogin
);
}
});

if (openingHour !== -1) {
const keys = ["total", "total", "actionsTime", openingHour, "opened"];
set(collection, keys, get(collection, keys, 0) + 1);
const userKeys = [
pullRequest?.user.login || invalidUserLogin,
"total",
"actionsTime",
openingHour,
"opened",
];
set(collection, userKeys, get(collection, userKeys, 0) + 1);
}
if (mergingHour !== -1) {
const keys = ["total", "total", "actionsTime", mergingHour, "merged"];
set(collection, keys, get(collection, keys, 0) + 1);
const userKeys = [
pullRequest?.user.login || invalidUserLogin,
"total",
"actionsTime",
openingHour,
"merged",
];
set(collection, userKeys, get(collection, userKeys, 0) + 1);
}
Comment on lines +52 to +63
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix critical bug: Merge action recorded with wrong hour

There's a critical bug in the merge action recording logic. The code is using openingHour instead of mergingHour when recording the merge action for the user.

     const userKeys = [
       pullRequest?.user.login || invalidUserLogin,
       "total",
       "actionsTime",
-      openingHour,
+      mergingHour,
       "merged",
     ];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (mergingHour !== -1) {
const keys = ["total", "total", "actionsTime", mergingHour, "merged"];
set(collection, keys, get(collection, keys, 0) + 1);
const userKeys = [
pullRequest?.user.login || invalidUserLogin,
"total",
"actionsTime",
openingHour,
"merged",
];
set(collection, userKeys, get(collection, userKeys, 0) + 1);
}
if (mergingHour !== -1) {
const keys = ["total", "total", "actionsTime", mergingHour, "merged"];
set(collection, keys, get(collection, keys, 0) + 1);
const userKeys = [
pullRequest?.user.login || invalidUserLogin,
"total",
"actionsTime",
mergingHour,
"merged",
];
set(collection, userKeys, get(collection, userKeys, 0) + 1);
}

};
Comment on lines +7 to +64
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Consider architectural improvements for better maintainability

The current implementation could benefit from the following architectural improvements:

  1. Split the function into smaller, focused functions:

    • One for processing review events
    • One for processing PR lifecycle events (open/merge)
  2. Add validation for:

    • Hour ranges (should be 0-23)
    • Date parsing errors
    • Required fields in the input data
  3. Consider adding error logging for invalid data to help with debugging

Would you like me to provide an example of how to implement these improvements?

18 changes: 18 additions & 0 deletions src/createOutput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from "./view/utils";
import { octokit } from "./octokit";
import { showStatsTypes } from "./common/constants";
import { createActivityTimeMarkdown } from "./view/utils/createActivityTimeMarkdown";

export const createOutput = async (
data: Record<string, Record<string, Collection>>
Expand Down Expand Up @@ -75,6 +76,23 @@ export const createOutput = async (
title: "Correlation Graphs",
});
}
if (getValueAsIs("SHOW_ACTIVITY_TIME_GRAPHS") === "true") {
const activityComment = await octokit.rest.issues.createComment({
repo: getValueAsIs("GITHUB_REPO_FOR_ISSUE"),
owner: getValueAsIs("GITHUB_OWNER_FOR_ISSUE"),
issue_number: issue.data.number,
body: createActivityTimeMarkdown(data, users, [
{
title: "Pull Request report total",
link: `${issue.data.html_url}#`,
},
]),
});
comments.push({
comment: activityComment,
title: "Activity time Graphs",
});
}
Comment on lines +79 to +95
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

⚠️ Potential issue

Add unit tests for the new functionality.

The PR objectives mention that no tests were added. Please add unit tests to cover:

  1. Activity time graph generation
  2. Environment variable handling
  3. Comment creation error scenarios

Would you like me to help generate the test cases for this new functionality?

console.log("Issue successfully created.");
for (let date of dates) {
if (date === "total") continue;
Expand Down
2 changes: 0 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import { getRateLimit } from "./requests/getRateLimit";
import {
sendActionError,
sendActionRun,
sendDiscussionUsage,
} from "./analytics";

async function main() {
Expand Down Expand Up @@ -91,7 +90,6 @@ async function main() {
}
);
const preparedData = collectData(mergedData, teams);
sendDiscussionUsage(preparedData);
console.log("Calculation complete. Generating markdown.");
await createOutput(preparedData);

Expand Down
18 changes: 18 additions & 0 deletions src/view/utils/createActivityTimeMarkdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Collection } from "../../converters";
import { createActivityXYChart } from "./createActivityXYChart";
import { createReferences } from "./createReferences";

export const createActivityTimeMarkdown = (
data: Record<string, Record<string, Collection>>,
users: string[],
references: { title: string; link: string }[] = []
) => {
Comment on lines +5 to +9
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Consider adding input validation and improving type definitions.

  1. The nested Record type could be extracted to a dedicated type/interface for better maintainability.
  2. Missing validation for required parameters data and users.

Consider applying these improvements:

+type ActivityData = Record<string, Record<string, Collection>>;
+type Reference = { title: string; link: string };
+
 export const createActivityTimeMarkdown = (
-  data: Record<string, Record<string, Collection>>,
-  users: string[],
-  references: { title: string; link: string }[] = []
+  data: ActivityData,
+  users: string[],
+  references: Reference[] = []
 ) => {
+  if (!data || !users) {
+    throw new Error('Required parameters "data" and "users" must be provided');
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const createActivityTimeMarkdown = (
data: Record<string, Record<string, Collection>>,
users: string[],
references: { title: string; link: string }[] = []
) => {
type ActivityData = Record<string, Record<string, Collection>>;
type Reference = { title: string; link: string };
export const createActivityTimeMarkdown = (
data: ActivityData,
users: string[],
references: Reference[] = []
) => {
if (!data || !users) {
throw new Error('Required parameters "data" and "users" must be provided');
}

const activity = users
.filter(
(user) => Object.keys(data[user]?.total?.actionsTime || {}).length > 1
)
.map((user) => createActivityXYChart(data, user))
.join("\n");

return [createReferences(references)].concat(activity).join("\n").trim();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Consider improving readability and handling empty results.

The current implementation could be more explicit and handle edge cases better.

Consider this alternative:

-  return [createReferences(references)].concat(activity).join("\n").trim();
+  const parts = [
+    createReferences(references),
+    activity
+  ].filter(Boolean);
+  
+  if (parts.length === 0) {
+    return "No activity data available.";
+  }
+  
+  return parts.join("\n").trim();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return [createReferences(references)].concat(activity).join("\n").trim();
const parts = [
createReferences(references),
activity
].filter(Boolean);
if (parts.length === 0) {
return "No activity data available.";
}
return parts.join("\n").trim();

};
Comment on lines +1 to +18
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

⚠️ Potential issue

Add unit tests for the activity time markdown generation.

The PR objectives indicate that no tests were added. Given the complexity of data processing and the importance of this feature for visualizing user activity, unit tests are essential to ensure reliability.

Would you like me to help generate unit tests for this file? The tests should cover:

  1. Basic functionality with valid inputs
  2. Edge cases (empty data, no users)
  3. Error handling scenarios
  4. Integration with createActivityXYChart

60 changes: 60 additions & 0 deletions src/view/utils/createActivityXYChart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Collection } from "../../converters";
import { createXYChart } from "./common/createXYChart";

export const createActivityXYChart = (
data: Record<string, Record<string, Collection>>,
user: string
) => {
Comment on lines +1 to +7
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Enhance type safety with more specific types

Consider defining a more specific type for the nested data structure instead of using Record<string, Record<string, Collection>>. This would make the code more maintainable and self-documenting.

+type ActivityData = {
+  [user: string]: {
+    total?: {
+      actionsTime?: {
+        [hour: string]: {
+          opened?: number;
+          merged?: number;
+          approved?: number;
+          changes_requested?: number;
+          commented?: number;
+        };
+      };
+    };
+  };
+};

-export const createActivityXYChart = (
-  data: Record<string, Record<string, Collection>>,
+export const createActivityXYChart = (
+  data: ActivityData,
   user: string
 ) => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { Collection } from "../../converters";
import { createXYChart } from "./common/createXYChart";
export const createActivityXYChart = (
data: Record<string, Record<string, Collection>>,
user: string
) => {
import { Collection } from "../../converters";
import { createXYChart } from "./common/createXYChart";
type ActivityData = {
[user: string]: {
total?: {
actionsTime?: {
[hour: string]: {
opened?: number;
merged?: number;
approved?: number;
changes_requested?: number;
commented?: number;
};
};
};
};
};
export const createActivityXYChart = (
data: ActivityData,
user: string
) => {

const hours = Array.from({ length: 24 }, (_, i) => i.toString());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Consider memoizing the hours array

Since the hours array is constant and used in multiple map operations, consider moving it outside the function to avoid recreation on each call.

+const HOURS = Array.from({ length: 24 }, (_, i) => i.toString());
+
 export const createActivityXYChart = (
   data: ActivityData,
   user: string
 ) => {
-  const hours = Array.from({ length: 24 }, (_, i) => i.toString());
+  const hours = HOURS;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const hours = Array.from({ length: 24 }, (_, i) => i.toString());
const HOURS = Array.from({ length: 24 }, (_, i) => i.toString());
export const createActivityXYChart = (
data: ActivityData,
user: string
) => {
const hours = HOURS;

return createXYChart({
title: `Activity ${user}`,
xAxis: hours,
yAxis: {
min: 0,
max: Math.max(
...Object.values(data[user]?.total?.actionsTime || {}).map((value) =>
Math.max(...Object.values(value), 0)
),
1
),
title: "Amount",
},
Comment on lines +9 to +21
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve chart configuration robustness and clarity

Several improvements could enhance the chart configuration:

  1. The max calculation is complex and could benefit from a separate function
  2. The title could be more descriptive (e.g., "User Activity Throughout the Day")
  3. Consider making the minimum value configurable
+const calculateMaxValue = (actionsTime: Record<string, Record<string, number>> = {}) => {
+  const values = Object.values(actionsTime).flatMap(hourData => Object.values(hourData));
+  return values.length ? Math.max(...values) : 1;
+};
+
 return createXYChart({
-    title: `Activity ${user}`,
+    title: `Activity Throughout the Day - ${user}`,
     xAxis: hours,
     yAxis: {
       min: 0,
-      max: Math.max(
-        ...Object.values(data[user]?.total?.actionsTime || {}).map((value) =>
-          Math.max(...Object.values(value), 0)
-        ),
-        1
-      ),
+      max: calculateMaxValue(data[user]?.total?.actionsTime),
       title: "Amount",
     },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return createXYChart({
title: `Activity ${user}`,
xAxis: hours,
yAxis: {
min: 0,
max: Math.max(
...Object.values(data[user]?.total?.actionsTime || {}).map((value) =>
Math.max(...Object.values(value), 0)
),
1
),
title: "Amount",
},
const calculateMaxValue = (actionsTime: Record<string, Record<string, number>> = {}) => {
const values = Object.values(actionsTime).flatMap(hourData => Object.values(hourData));
return values.length ? Math.max(...values) : 1;
};
return createXYChart({
title: `Activity Throughout the Day - ${user}`,
xAxis: hours,
yAxis: {
min: 0,
max: calculateMaxValue(data[user]?.total?.actionsTime),
title: "Amount",
},

lines: [
{
name: "Opened",
color: "black",
values: hours.map(
(el) => data[user]?.total?.actionsTime?.[el]?.opened || 0
),
},
{
name: "Merged",
color: "purple",
values: hours.map(
(el) => data[user]?.total?.actionsTime?.[el]?.merged || 0
),
},
{
name: "Approved",
color: "green",
values: hours.map(
(el) => data[user]?.total?.actionsTime?.[el]?.approved || 0
),
},
{
name: "Changes Requested",
color: "red",
values: hours.map(
(el) => data[user]?.total?.actionsTime?.[el]?.changes_requested || 0
),
},
{
name: "Commented",
color: "blue",
values: hours.map(
(el) => data[user]?.total?.actionsTime?.[el]?.commented || 0
),
},
],
Comment on lines +22 to +58
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Refactor repeated code and improve accessibility

The lines configuration has several areas for improvement:

  1. The code is repetitive and could be DRYer
  2. Color choices should consider accessibility (color blindness)
  3. Consider adding error handling for completely missing data
+const ACTIVITY_TYPES = [
+  { name: 'Opened', color: '#000000', key: 'opened' },
+  { name: 'Merged', color: '#9333EA', key: 'merged' },
+  { name: 'Approved', color: '#16A34A', key: 'approved' },
+  { name: 'Changes Requested', color: '#DC2626', key: 'changes_requested' },
+  { name: 'Commented', color: '#2563EB', key: 'commented' }
+] as const;
+
+const getActivityValues = (
+  actionsTime: Record<string, Record<string, number>> = {},
+  hours: string[],
+  key: string
+) => hours.map(hour => actionsTime[hour]?.[key] || 0);
+
     lines: [
-      {
-        name: "Opened",
-        color: "black",
-        values: hours.map(
-          (el) => data[user]?.total?.actionsTime?.[el]?.opened || 0
-        ),
-      },
-      // ... other repeated blocks
+      ...ACTIVITY_TYPES.map(({ name, color, key }) => ({
+        name,
+        color,
+        values: getActivityValues(data[user]?.total?.actionsTime, hours, key)
+      }))
     ],

Also, consider adding a check for completely missing data:

if (!data[user]?.total?.actionsTime) {
  throw new Error(`No activity data found for user: ${user}`);
}

});
};
Comment on lines +4 to +60
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

⚠️ Potential issue

Add tests for the visualization logic

As mentioned in the PR description, tests haven't been added for this new feature. Given that this is a critical visualization component, please add tests to verify:

  1. Correct data transformation
  2. Edge cases (empty data, missing user)
  3. Maximum value calculation
  4. Hour formatting

Would you like me to help create a test suite for this component?

1 change: 1 addition & 0 deletions src/view/utils/createConfigParamsCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ ${[
"TOP_LIST_AMOUNT",
"AGGREGATE_VALUE_METHODS",
"SHOW_CORRELATION_GRAPHS",
"SHOW_ACTIVITY_TIME_GRAPHS",
"PERCENTILE",
"HIDE_USERS",
"SHOW_USERS",
Expand Down
Loading