Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enhance(apps/analytics): add overview of element feedbacks on activities and instances #4401

Merged
merged 9 commits into from
Dec 20, 2024

Conversation

sjschlapbach
Copy link
Member

@sjschlapbach sjschlapbach commented Dec 20, 2024

Summary by CodeRabbit

  • New Features

    • Introduced new components for displaying activity and instance feedback, including ActivityInstanceFeedbacksPlot and ElementFeedbackBarChart.
    • Added a new navigation component, QuizAnalyticsNavigation, for improved course analytics navigation.
    • New GraphQL fields for instanceFeedbacks and activityFeedbacks have been added to enhance performance analytics.
  • Bug Fixes

    • Adjusted import paths for various components to ensure correct resolution and functionality.
  • Documentation

    • Updated localization files to include new keys for feedback analytics in both English and German.
  • Chores

    • Refactored existing components to streamline imports and improve code organization.

Copy link

aviator-app bot commented Dec 20, 2024

Current Aviator status

Aviator will automatically update this comment as the status of the PR changes.
Comment /aviator refresh to force Aviator to re-examine your PR (or learn about other /aviator commands).

This PR was merged manually (without Aviator). Merging manually can negatively impact the performance of the queue. Consider using Aviator next time.


See the real-time status of this PR on the Aviator webapp.
Use the Aviator Chrome Extension to see the status of your PR within GitHub.

Copy link

gitguardian bot commented Dec 20, 2024

️✅ There are no secrets present in this pull request anymore.

If these secrets were true positive and are still valid, we highly recommend you to revoke them.
While these secrets were previously flagged, we no longer have a reference to the
specific commits where they were detected. Once a secret has been leaked into a git
repository, you should consider it compromised, even if it was deleted immediately.
Find here more information about risks.


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@sjschlapbach sjschlapbach changed the base branch from v3 to v3-analytics December 20, 2024 07:33
Copy link

coderabbitai bot commented Dec 20, 2024

Caution

Review failed

The pull request is closed.

Warning

There were issues while running some tools. Please review the errors and either fix the tool’s configuration or disable the tool if it’s a critical failure.

🔧 ast-grep (0.31.1)
apps/frontend-manage/src/components/analytics/performance/useElementFeedbackSearch.ts

An unexpected error occurred while running ast-grep.

📝 Walkthrough
📝 Walkthrough

Walkthrough

This pull request introduces comprehensive enhancements to the learning analytics system across multiple packages and applications. The changes primarily focus on adding new feedback tracking mechanisms for activities and instances, updating import paths, and refactoring component names. The modifications span frontend management, PWA applications, GraphQL schema, internationalization, and type definitions, creating a more robust and detailed analytics infrastructure.

Changes

File Path Change Summary
apps/frontend-manage/src/components/analytics/ Renamed and refactored multiple components like PerformanceActivityTypeFilterActivityTypeFilter, PerformanceSearchFieldAnalyticsSearchField, and introduced new components such as StackedBarChartLabel, ActivityInstanceFeedbacksPlot, and ElementFeedbackBarChart.
apps/frontend-manage/src/pages/analytics/ Updated import paths from tilde-based (~) to relative paths, added new components for quiz and performance analytics, and modified existing components to utilize new feedback structures.
packages/graphql/src/schema/analytics.ts Added new types InstanceFeedback and ActivityFeedback, updated CoursePerformanceAnalytics interface to include feedback arrays.
packages/graphql/src/services/analytics.ts Introduced new functions for aggregating feedback: aggregateInstanceFeedbacks, aggregateActivityFeedbacks, and computeActivityInstanceFeedbacks.
packages/types/src/index.ts Added new types InstanceFeedback and ActivityFeedback to enhance feedback tracking capabilities.
packages/graphql/src/graphql/ops/QGetCoursePerformanceAnalytics.graphql Modified the GetCoursePerformanceAnalytics query to include new fields instanceFeedbacks and activityFeedbacks for detailed feedback metrics.
packages/i18n/messages/de.ts and packages/i18n/messages/en.ts Updated localization files to include new keys related to feedback analytics.

Possibly related PRs

Suggested reviewers

  • rschlaefli

📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b91ca12 and 85902f4.

📒 Files selected for processing (1)
  • apps/frontend-manage/src/components/analytics/performance/useElementFeedbackSearch.ts (1 hunks)

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (27)
apps/frontend-manage/src/pages/analytics/index.tsx (1)

6-7: Consider the trade-offs of relative vs. absolute imports

While relative imports work, consider the maintainability aspects:

  • Absolute imports (with path aliases) can make the codebase more maintainable during file relocations
  • Relative imports can make it harder to move files around without breaking imports

Consider using TypeScript path aliases (e.g., @/components/...) for a balance between explicit paths and maintainability.

packages/graphql/src/ops.ts (1)

49-56: LGTM! Consider adding JSDoc comments.

The new type definitions are well-structured and consistent. The feedback types appropriately use float values for vote rates, allowing for percentage calculations.

Consider adding JSDoc comments to document the purpose of each type and any important constraints on the rate values. Example:

+/**
+ * Represents feedback data for a course activity
+ * @property upvoteRate - The rate of upvotes (0.0 to 1.0)
+ * @property downvoteRate - The rate of downvotes (0.0 to 1.0)
+ */
 export type ActivityFeedback = {
   __typename?: 'ActivityFeedback';
   activityName: Scalars['String']['output'];
   // ...
 };

Also applies to: 379-385, 892-900

apps/frontend-manage/src/pages/analytics/[courseId]/quizzes.tsx (5)

1-2: Use smaller or direct imports from your GraphQL library.

While importing from "@klicker-uzh/graphql/dist/ops" might be correct for your setup, consider whether you can import the generated documents more directly (e.g., via "@klicker-uzh/graphql") to avoid potential path-churn if the library folder structure evolves.


26-34: Consider letting the router handle the route-based gating.

Currently, your loading state covers both the "loading" variable from Apollo and the absence of "courseId". This is fine, but an alternative is to check the route param in a higher-level route guard. That might simplify your component logic if you have repeated patterns across other pages.


36-44: Improve error handling messages or actions.

You display a generic error view, which is helpful. You could provide more details or a retry option, especially if the error might be intermittent or user-correctable.


50-59: Separate layout styling from data.

The display of the course name and total participants is clear and well styled. Consider placing them in a small, dedicated component if you plan to reuse this info or layout in other analytics pages.


61-64: Encapsulate feedback logic if it expands later.

The “ActivityInstanceFeedbacksPlot” is directly injected here. If you plan to extend the feedback logic for quizzes further, you might want a dedicated wrapper or container for all feedback components under a “quiz feedback” umbrella.

apps/frontend-manage/src/components/analytics/quiz/ActivityInstanceFeedbacksPlot.tsx (5)

1-2: Reduce import from FontAwesome.

You are importing icons from the FontAwesome package. If the bundle size is of any concern, consider using tree-shaking or selectively importing icons from a narrower source (e.g., '@fortawesome/free-solid-svg-icons/faX') to cut down on potential overhead.


34-39: Consolidate default filters.

You are defining “defaultFilters” as a constant with multiple fields. This is fine, but ensuring each enumerated filter type is typed or tested (especially if new filters appear) will help reduce future mistakes.


42-55: Potential duplication of state.

You have separate search terms for activity (“activitySearchTerm”) and instance (“instanceSearchTerm”). Since the type ('activity' | 'instance') is already tracked, you might consolidate them into a single searchTerm with conditional usage, or into a dictionary keyed by type. This can reduce code repetition.


78-104: Avoid inline styles in className if you can.

Your usage of “py-0.25” is presumably from a tailwind or system library. Double-check that fractional units do behave consistently across browsers. Also, consider a design token approach if used widely.


130-174: Provide more descriptive user messages or a link.

When no entries are present, you notify the user to manage filters. Directly linking them to documentation or providing a brief explanation of typical filter usage might further improve user experience.

apps/frontend-manage/src/components/analytics/StackedBarChartLabel.tsx (1)

1-27: Add input validation and improve accessibility

While the implementation is solid, consider these improvements:

  1. Add validation for the value prop to handle negative numbers or NaN
  2. Add aria-label for better accessibility
 function StackedBarChartLabel({
   value,
   x,
   width,
   y,
   height,
 }: {
   value: number
   x: number
   y: number
   width: number
   height: number
 }) {
+  const displayValue = Math.max(0, Number.isFinite(value) ? value : 0)
   return (
     <text
       x={x + width / 2}
       y={y + height / 2 + 1}
       fill="white"
       fontSize={14}
       textAnchor="middle"
       dominantBaseline="middle"
       className="font-bold"
+      aria-label={`${displayValue}%`}
     >
-      {value} %
+      {displayValue} %
     </text>
   )
 }
apps/frontend-manage/src/components/analytics/quiz/QuizAnalyticsNavigation.tsx (1)

8-11: Consider extracting URL paths to constants

The hardcoded URL paths could be moved to constants or a routing configuration for better maintainability.

+const ANALYTICS_ROUTES = {
+  PERFORMANCE: (courseId: string) => `/analytics/${courseId}/performance`,
+  ACTIVITY: (courseId: string) => `/analytics/${courseId}/activity`,
+} as const

 function QuizAnalyticsNavigation({ courseId }: { courseId: string }) {
   return (
     <AnalyticsNavigation
-      hrefLeft={`/analytics/${courseId}/performance`}
+      hrefLeft={ANALYTICS_ROUTES.PERFORMANCE(courseId)}
       labelLeft={<PerformanceDashboardLabel />}
-      hrefRight={`/analytics/${courseId}/activity`}
+      hrefRight={ANALYTICS_ROUTES.ACTIVITY(courseId)}
       labelRight={<ActivityDashboardLabel />}
     />
   )
 }
apps/frontend-manage/src/components/analytics/quiz/useElementFeedbackSearch.ts (1)

15-25: Add error handling and consider performance optimizations

The search implementation could be improved with:

  1. Validation for empty or invalid feedback arrays
  2. Configuration options for search settings (e.g., exact match, case sensitivity)
  3. Pagination or virtualization for large datasets
 return useMemo(() => {
+  // Validate input arrays
+  if (!Array.isArray(activityFeedbacks) || !Array.isArray(instanceFeedbacks)) {
+    throw new Error('Invalid feedback arrays provided')
+  }

   const activitySearch = new JsSearch.Search('activityName')
+  // Configure search settings
+  activitySearch.searchIndex = new JsSearch.UnorderedSearchIndex()
+  activitySearch.tokenizer = new JsSearch.StopWordsTokenizer(
+    new JsSearch.SimpleTokenizer()
+  )
   activitySearch.addIndex('activityName')
   activitySearch.addDocuments(activityFeedbacks)

   const instanceSearch = new JsSearch.Search('elementName')
+  instanceSearch.searchIndex = new JsSearch.UnorderedSearchIndex()
+  instanceSearch.tokenizer = new JsSearch.StopWordsTokenizer(
+    new JsSearch.SimpleTokenizer()
+  )
   instanceSearch.addIndex('elementName')
   instanceSearch.addDocuments(instanceFeedbacks)

   return { activitySearch, instanceSearch }
 }, [activityFeedbacks, instanceFeedbacks])
apps/frontend-manage/src/components/analytics/ActivityTypeFilter.tsx (1)

Line range hint 6-35: Consider extracting activity type options to a constant.

The implementation looks good, but the activity type options could be extracted to improve maintainability.

Consider refactoring the items array:

+ const ACTIVITY_TYPE_OPTIONS = [
+   { value: 'all', label: 'manage.analytics.allActivityTypes' },
+   { value: ActivityType.PracticeQuiz, label: 'shared.generic.practiceQuizzes' },
+   { value: ActivityType.MicroLearning, label: 'shared.generic.microlearnings' },
+ ] as const

function ActivityTypeFilter({...}) {
  const t = useTranslations()
  return (
    <SelectField
      label={t('manage.analytics.activityType')}
-     items={[
-       { value: 'all', label: t('manage.analytics.allActivityTypes') },
-       { value: ActivityType.PracticeQuiz, label: t('shared.generic.practiceQuizzes') },
-       { value: ActivityType.MicroLearning, label: t('shared.generic.microlearnings') },
-     ]}
+     items={ACTIVITY_TYPE_OPTIONS.map(({ value, label }) => ({
+       value,
+       label: t(label)
+     }))}
      ...
    />
  )
}
apps/frontend-manage/src/components/analytics/quiz/ElementFeedbackBarChart.tsx (1)

18-19: Consider extracting percentage calculations.

These calculations could be reused elsewhere in the analytics module.

Consider creating a utility function:

export const calculatePercentage = (rate: number): number => Math.round(rate * 100)
apps/frontend-manage/src/components/analytics/performance/PerformanceRatesBarChart.tsx (1)

Line range hint 31-64: Consider memoizing the bar components for better performance.

Since the bar components are conditionally rendered based on rate values, consider memoizing them to prevent unnecessary re-renders.

+ import { useMemo } from 'react'

function PerformanceRatesBarChart({
  title,
  rates,
  colors,
}: PerformanceRatesBarChartProps) {
  const roundedErrorRate = Math.round(rates.incorrectRate * 100)
  const roundedPartialRate = Math.round(rates.partialRate * 100)
  const roundedCorrectRate = 100 - roundedErrorRate - roundedPartialRate

+ const errorBar = useMemo(() => 
+   roundedErrorRate > 0 && (
+     <Bar
+       dataKey="incorrectRate"
+       stackId="1"
+       fill={colors.incorrect}
+       label={(props) => (
+         <StackedBarChartLabel {...props} value={roundedErrorRate} />
+       )}
+     />
+   ),
+ [roundedErrorRate, colors.incorrect])

// Similar memoization for partial and correct bars
apps/frontend-manage/src/components/analytics/quiz/useElementFeedbackFilters.ts (3)

31-48: Consider adding error handling for search operations.

The search operation could potentially throw errors if the search index is not properly initialized or if the data structure is unexpected.

- return activitySearch.search(sanitizedSearchTerm) as ActivityFeedback[]
+ try {
+   const searchResults = activitySearch.search(sanitizedSearchTerm)
+   return searchResults as ActivityFeedback[]
+ } catch (error) {
+   console.error('Search operation failed:', error)
+   return typeFiltered
+ }

10-30: Improve type safety with discriminated unions.

Consider using a discriminated union type for better type safety and to avoid type assertions.

type FilterParams = 
  | {
      type: 'activity'
      activityFeedbacks: ActivityFeedback[]
      activityType: ActivityType | 'all'
      activitySearchTerm: string
      activitySearch: Search
    }
  | {
      type: 'instance'
      instanceFeedbacks: InstanceFeedback[]
      elementType: ElementType | 'all'
      instanceSearchTerm: string
      instanceSearch: Search
    }

35-42: Consider memoizing filter predicates.

The filter predicates could be memoized to prevent recreating functions on each render.

+ const typeFilterPredicate = useMemo(() => 
+   (feedback: ActivityFeedback) => {
+     if (activityType === 'all') return true
+     return feedback.activityType === activityType
+   },
+ [activityType])

- const typeFiltered = rawData.filter((feedback) => {
-   if (activityType === 'all') {
-     return true
-   }
-   return feedback.activityType === activityType
- })
+ const typeFiltered = rawData.filter(typeFilterPredicate)
apps/frontend-manage/src/pages/analytics/[courseId]/performance.tsx (1)

Line range hint 41-46: Consider more granular error handling.

The error state could be more specific about the type of error encountered (network error, data not found, etc.).

- if (course === null || typeof course === 'undefined' || error) {
+ if (error) {
+   return (
+     <AnalyticsErrorView
+       title={t('manage.analytics.performanceDashboard')}
+       navigation={navigation}
+       error={error}
+       message={t('manage.analytics.errors.query')}
+     />
+   )
+ }
+ 
+ if (course === null || typeof course === 'undefined') {
    return (
      <AnalyticsErrorView
        title={t('manage.analytics.performanceDashboard')}
        navigation={navigation}
+       message={t('manage.analytics.errors.noData')}
      />
    )
  }
apps/frontend-pwa/src/pages/course/[courseId]/microlearning/[id]/[ix].tsx (1)

14-14: Consider using path alias instead of deep relative path

Deep relative paths (../../../../../) are harder to maintain and more prone to break during refactoring. Consider maintaining the alias import:

-import PreviewMessage from '../../../../../components/common/PreviewMessage'
+import PreviewMessage from '~/components/common/PreviewMessage'
packages/types/src/index.ts (2)

408-415: Consider adding validation constraints and documentation for feedback rates.

The InstanceFeedback type looks good but could benefit from additional constraints and documentation:

  • Add JSDoc comments explaining how upvote/downvote rates are calculated
  • Consider adding min/max constraints (e.g., 0-1 or 0-100) for rate fields
  • Consider adding a timestamp field for temporal analysis
+/** Represents feedback metrics for a specific instance
+ * @property upvoteRate - Rate of upvotes (range: 0-1)
+ * @property downvoteRate - Rate of downvotes (range: 0-1)
+ */
 export type InstanceFeedback = {
   id: number
   activityType: ActivityType
   instanceName: string
   instanceType: ElementType
   upvoteRate: number
   downvoteRate: number
+  createdAt?: Date
 }

417-423: Ensure consistent type usage between InstanceFeedback and ActivityFeedback.

The ActivityFeedback type uses string for ID while InstanceFeedback uses number. Consider using consistent ID types unless there's a specific reason for the difference.

 export type ActivityFeedback = {
-  id: string
+  id: number
   activityType: ActivityType
   activityName: string
   upvoteRate: number
   downvoteRate: number
 }
packages/graphql/src/schema/analytics.ts (2)

271-282: Add field descriptions for better GraphQL schema documentation.

Consider adding descriptions to the GraphQL fields to improve schema documentation and API usability.

 export const InstanceFeedback = builder.objectType(InstanceFeedbackRef, {
+  description: 'Feedback metrics for a specific instance',
   fields: (t) => ({
-    id: t.exposeInt('id'),
+    id: t.exposeInt('id', {
+      description: 'Unique identifier for the instance feedback'
+    }),
     activityType: t.expose('activityType', { type: ActivityType }),
     instanceName: t.exposeString('instanceName'),
     instanceType: t.expose('instanceType', { type: ElementType }),
-    upvoteRate: t.exposeFloat('upvoteRate'),
+    upvoteRate: t.exposeFloat('upvoteRate', {
+      description: 'Rate of upvotes (range: 0-1)'
+    }),
     downvoteRate: t.exposeFloat('downvoteRate'),
   }),
 })

296-316: Consider adding pagination support for feedback collections.

The CourseQuizAnalytics type returns arrays of feedbacks which might grow large. Consider implementing pagination for better performance and scalability.

Example implementation:

 interface ICourseQuizAnalytics {
   name: string
   totalParticipants: number
-  instanceFeedbacks: InstanceFeedbackType[]
-  activityFeedbacks: ActivityFeedbackType[]
+  instanceFeedbacks: {
+    nodes: InstanceFeedbackType[]
+    pageInfo: {
+      hasNextPage: boolean
+      endCursor: string
+    }
+  }
+  activityFeedbacks: {
+    nodes: ActivityFeedbackType[]
+    pageInfo: {
+      hasNextPage: boolean
+      endCursor: string
+    }
+  }
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5630619 and 81ce688.

📒 Files selected for processing (35)
  • apps/frontend-manage/src/components/analytics/ActivityTypeFilter.tsx (2 hunks)
  • apps/frontend-manage/src/components/analytics/AnalyticsErrorView.tsx (1 hunks)
  • apps/frontend-manage/src/components/analytics/AnalyticsLoadingView.tsx (1 hunks)
  • apps/frontend-manage/src/components/analytics/AnalyticsSearchField.tsx (2 hunks)
  • apps/frontend-manage/src/components/analytics/ElementTypeFilter.tsx (2 hunks)
  • apps/frontend-manage/src/components/analytics/StackedBarChartLabel.tsx (1 hunks)
  • apps/frontend-manage/src/components/analytics/performance/PerformanceRates.tsx (4 hunks)
  • apps/frontend-manage/src/components/analytics/performance/PerformanceRatesBarChart.tsx (4 hunks)
  • apps/frontend-manage/src/components/analytics/quiz/ActivityInstanceFeedbacksPlot.tsx (1 hunks)
  • apps/frontend-manage/src/components/analytics/quiz/ElementFeedbackBarChart.tsx (1 hunks)
  • apps/frontend-manage/src/components/analytics/quiz/QuizAnalyticsNavigation.tsx (1 hunks)
  • apps/frontend-manage/src/components/analytics/quiz/useElementFeedbackFilters.ts (1 hunks)
  • apps/frontend-manage/src/components/analytics/quiz/useElementFeedbackSearch.ts (1 hunks)
  • apps/frontend-manage/src/pages/analytics/[courseId]/activity.tsx (1 hunks)
  • apps/frontend-manage/src/pages/analytics/[courseId]/performance.tsx (1 hunks)
  • apps/frontend-manage/src/pages/analytics/[courseId]/quizzes.tsx (1 hunks)
  • apps/frontend-manage/src/pages/analytics/index.tsx (1 hunks)
  • apps/frontend-pwa/src/pages/course/[courseId]/index.tsx (1 hunks)
  • apps/frontend-pwa/src/pages/course/[courseId]/microlearning/[id]/[ix].tsx (1 hunks)
  • apps/frontend-pwa/src/pages/course/[courseId]/microlearning/[id]/evaluation.tsx (1 hunks)
  • apps/frontend-pwa/src/pages/course/[courseId]/microlearning/[id]/index.tsx (1 hunks)
  • apps/frontend-pwa/src/pages/group/[groupId]/activity/[activityId].tsx (1 hunks)
  • apps/frontend-pwa/src/pages/index.tsx (1 hunks)
  • packages/graphql/src/graphql/ops/QGetCourseQuizAnalytics.graphql (1 hunks)
  • packages/graphql/src/ops.schema.json (4 hunks)
  • packages/graphql/src/ops.ts (7 hunks)
  • packages/graphql/src/public/client.json (1 hunks)
  • packages/graphql/src/public/schema.graphql (4 hunks)
  • packages/graphql/src/public/server.json (1 hunks)
  • packages/graphql/src/schema/analytics.ts (2 hunks)
  • packages/graphql/src/schema/query.ts (2 hunks)
  • packages/graphql/src/services/analytics.ts (2 hunks)
  • packages/i18n/messages/de.ts (1 hunks)
  • packages/i18n/messages/en.ts (1 hunks)
  • packages/types/src/index.ts (1 hunks)
🔥 Files not summarized due to errors (1)
  • packages/graphql/src/ops.ts: Error: Server error: no LLM provider could handle the message
✅ Files skipped from review due to trivial changes (7)
  • apps/frontend-manage/src/pages/analytics/[courseId]/activity.tsx
  • apps/frontend-manage/src/components/analytics/AnalyticsLoadingView.tsx
  • apps/frontend-manage/src/components/analytics/AnalyticsErrorView.tsx
  • apps/frontend-pwa/src/pages/index.tsx
  • apps/frontend-pwa/src/pages/course/[courseId]/index.tsx
  • apps/frontend-manage/src/components/analytics/AnalyticsSearchField.tsx
  • apps/frontend-pwa/src/pages/course/[courseId]/microlearning/[id]/evaluation.tsx
🔇 Additional comments (35)
apps/frontend-manage/src/pages/analytics/index.tsx (1)

Line range hint 9-32: LGTM! Clean and well-structured component

The component follows React best practices:

  • Proper data fetching with Apollo Client
  • Clean loading state handling
  • Consistent layout structure

Let's verify that the router removal didn't impact any navigation features:

✅ Verification successful

Navigation functionality is properly implemented and unaffected by router changes

The router usage in the analytics section is working as expected:

  • The main Analytics page correctly renders the course list without router dependency
  • Navigation is properly handled through the DashboardButtons component using Next.js router
  • All navigation paths (/analytics/${courseId}/[activity|performance|quizzes]) are correctly structured
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for any remaining router usage in analytics-related components
# that might have been affected by the router removal

# Search for router-related patterns in analytics components
rg -t tsx -t ts "useRouter|router\." "apps/frontend-manage/src/components/analytics"

# Check for any navigation-related onClick handlers that might need the router
ast-grep --pattern 'onClick={$_}' "apps/frontend-manage/src/components/analytics"

Length of output: 194


Script:

#!/bin/bash
# Let's fix the previous script and broaden the search

# Search for router-related patterns in analytics components and pages
rg "useRouter|router\." apps/frontend-manage/src/pages/analytics
rg "useRouter|router\." apps/frontend-manage/src/components

# Check for any navigation-related onClick handlers in analytics components
rg "onClick" apps/frontend-manage/src/pages/analytics
rg "onClick" apps/frontend-manage/src/components/analytics

# Check if CourseDashboardList has any navigation logic
rg -A 5 "CourseDashboardList" apps/frontend-manage/src

Length of output: 12068


Script:

#!/bin/bash
# Let's check the CourseDashboardList component implementation for any navigation logic
cat apps/frontend-manage/src/components/analytics/overview/CourseDashboardList.tsx

# Also check DashboardButtons implementation since it handles navigation
cat apps/frontend-manage/src/components/analytics/overview/DashboardButtons.tsx

Length of output: 2935

packages/graphql/src/ops.ts (2)

3714-3716: LGTM! Query variables are properly typed.

The query variables type is correctly defined with a required courseId parameter.


4231-4231: LGTM! Verify the query implementation.

The GraphQL document is well-structured and includes all necessary fields. The query follows the same patterns as other queries in the file.

Let's verify that the query implementation matches the schema:

✅ Verification successful

LGTM! The query implementation matches the schema definition.

The verification confirms that:

  • The getCourseQuizAnalytics field is properly defined in the schema with correct arguments and return type
  • The CourseQuizAnalytics type exists in the schema with all the required fields
  • The query implementation in ops.ts matches the schema definition
  • The resolver is properly implemented in schema/query.ts and services/analytics.ts
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check if the getCourseQuizAnalytics field exists in the schema
ast-grep --pattern 'type Query {
  $$$
  getCourseQuizAnalytics$_($_): CourseQuizAnalytics
  $$$
}'

# Verify the return type exists in the schema
ast-grep --pattern 'type CourseQuizAnalytics {
  $$$
  activityFeedbacks: [ActivityFeedback!]!
  instanceFeedbacks: [InstanceFeedback!]!
  $$$
}'

Length of output: 249


Script:

#!/bin/bash
# Let's try to find the schema file first
fd "schema.graphql|schema.gql"

# Search for type definitions in any GraphQL files
rg "type (Query|CourseQuizAnalytics|ActivityFeedback|InstanceFeedback)" -A 5 --type graphql

# Search for resolver implementation
rg "getCourseQuizAnalytics" -A 5 --type typescript

Length of output: 287


Script:

#!/bin/bash
# Now that we found the schema file, let's examine its contents
cat packages/graphql/src/public/schema.graphql | grep -A 10 "type Query" || true
cat packages/graphql/src/public/schema.graphql | grep -A 10 "type CourseQuizAnalytics" || true

# Let's also search for the resolver implementation without file type restriction
rg "getCourseQuizAnalytics" -A 5

Length of output: 27599

apps/frontend-manage/src/pages/analytics/[courseId]/quizzes.tsx (2)

7-10: Check updated import paths.

You switched to relative paths for these components, which aligns them with the updated directory structure. Ensure consistency across your codebase so all references to analytics components match these relative paths.


16-20: Validate that courseId is always defined before making the query.

Using "skip: !courseId" avoids extraneous fetch calls, which is good. Just double-check that no code attempts to render data that might be undefined if the skip logic is triggered.

apps/frontend-manage/src/components/analytics/quiz/ActivityInstanceFeedbacksPlot.tsx (1)

11-12: Re-check your Recharts usage for zero-state or error states.

While you do handle empty results (no entries), ensure that any Recharts usage also gracefully handles unexpected feedback data (e.g., null or undefined). This is often addressed with fallback logic or optional chaining.

packages/graphql/src/services/analytics.ts (2)

282-339: Check for concurrency in feedback aggregation.

The aggregateInstanceFeedbacks function looks correct for synchronous collection. However, if there's any chance that data about stacks or elements might be handled in parallel in the future (e.g., Promise.all for performance), be mindful that reassigning accumulators or mutating shared objects might cause race conditions. For now, it’s fine if everything is synchronous.


390-482: Ensure participant privacy.

When returning feedback info at the activity or instance level, confirm that no personally identifiable data (like user IDs) are included. Everything looks aggregated, but keep an eye on additional expansions or fields that might leak PII.

packages/graphql/src/graphql/ops/QGetCourseQuizAnalytics.graphql (1)

1-23: GraphQL query shape looks good.

You’ve introduced a clear query with typed variables. Ensure the schema definitions in your GraphQL server (for getCourseQuizAnalytics) are kept consistent. Also, verify you’re returning only what the frontend needs to limit overfetching.

apps/frontend-manage/src/components/analytics/StackedBarChartLabel.tsx (1)

1-29: LGTM! Well-structured component

The component is well-implemented with proper typing and positioning calculations.

apps/frontend-manage/src/components/analytics/quiz/QuizAnalyticsNavigation.tsx (1)

1-16: LGTM! Clean navigation implementation

The component is well-structured and properly integrates with the analytics navigation system.

apps/frontend-manage/src/components/analytics/quiz/useElementFeedbackSearch.ts (1)

1-28: LGTM! Well-implemented search hook

The hook effectively implements search functionality with proper memoization and type safety.

apps/frontend-manage/src/components/analytics/ElementTypeFilter.tsx (1)

Line range hint 6-31: LGTM! Verify component renaming impact.

The component renaming and implementation look good. The code is well-structured with proper typing and translations.

Let's verify all usages of the old component name have been updated:

✅ Verification successful

Component renaming is properly implemented

The component renaming from PerformanceElementTypeFilter to ElementTypeFilter has been correctly implemented. All imports and usages in the codebase are using the new name consistently. The component is properly used in both PerformanceRates.tsx and ActivityInstanceFeedbacksPlot.tsx with correct props.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for any remaining references to the old component name
rg "PerformanceElementTypeFilter"

# Find all imports of ElementTypeFilter to verify correct usage
rg "import.*ElementTypeFilter"

Length of output: 336


Script:

#!/bin/bash
# Check the content of files importing ElementTypeFilter to verify correct usage
rg -A 5 "ElementTypeFilter" apps/frontend-manage/src/components/analytics/performance/PerformanceRates.tsx apps/frontend-manage/src/components/analytics/quiz/ActivityInstanceFeedbacksPlot.tsx

Length of output: 3089

apps/frontend-manage/src/components/analytics/quiz/ElementFeedbackBarChart.tsx (2)

4-11: LGTM! Props interface is well-defined.

The interface clearly defines the expected props with appropriate types.


27-55: Verify chart responsiveness.

The chart uses ResponsiveContainer which is good, but we should ensure it behaves well at different viewport sizes.

Let's check for any related responsive layout issues:

✅ Verification successful

Chart responsiveness appears to be properly implemented

The implementation shows good responsive design practices:

  • The chart is wrapped in a ResponsiveContainer with width="100%" and fixed height={35}
  • The parent component uses a flex layout with proper constraints (max-h-[13rem] and overflow-y-scroll)
  • Similar responsive chart patterns are consistently used across other analytics components
  • The chart is vertically compact (35px height) and horizontally fluid, which is appropriate for its use case as a list item
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for related responsive container usages and potential issues
rg -A 3 "ResponsiveContainer.*width=\"100%\""

# Look for any reported issues with similar chart implementations
rg -l "BarChart.*layout=\"vertical\"" | xargs rg "TODO|FIXME|BUG"

Length of output: 3729


Script:

#!/bin/bash
# Let's check for any media queries or responsive styles applied to these charts
rg -A 5 "ElementFeedbackBarChart|StackedProgress|PerformanceRatesBarChart" "apps/frontend-manage/src/**/*.{css,scss,styled.ts,styles.ts}"

# Check for any container components or wrappers that might affect the chart's responsiveness
ast-grep --pattern 'const $_ = styled.$_`
  $$$
  @media $$ {
    $$$
  }
  $$$
`'

# Look for any test files that might verify responsive behavior
fd -e test.tsx -e spec.tsx | xargs rg "ElementFeedbackBarChart|StackedProgress|PerformanceRatesBarChart"

Length of output: 2522


Script:

#!/bin/bash
# Let's check the parent components to understand the layout context
rg -B 5 -A 10 "<ElementFeedbackBarChart" 

# Check if there are any width/height constraints from parent components
ast-grep --pattern 'const $_ = styled.div`
  $$$
  width: $_
  $$$
`' apps/frontend-manage/src/components/analytics/quiz/

# Look for any flex or grid container usage that might affect the chart
rg "display: (flex|grid)" apps/frontend-manage/src/components/analytics/quiz/

Length of output: 2300

apps/frontend-manage/src/components/analytics/performance/PerformanceRatesBarChart.tsx (1)

2-2: LGTM! Good refactoring of the label component.

The extraction of StackedBarChartLabel improves code reusability and maintainability.

Also applies to: 39-39, 49-49, 59-59

apps/frontend-manage/src/pages/analytics/[courseId]/performance.tsx (1)

7-13: LGTM! Import paths are consistently updated.

The relative import paths are correctly updated and maintain consistency with the project structure.

apps/frontend-manage/src/components/analytics/performance/PerformanceRates.tsx (3)

15-18: LGTM! Component renames improve reusability

The renaming of components from specific prefixes (e.g., Performance) to more generic names improves their reusability across different contexts while maintaining their functionality.


82-84: LGTM! Button padding adjustment

The padding adjustment improves the button's visual consistency.


110-117: LGTM! Consistent usage of renamed components

The implementation correctly uses the renamed components (ActivityTypeFilter, AnalyticsSearchField, ElementTypeFilter) with consistent props.

Also applies to: 126-134

apps/frontend-pwa/src/pages/group/[groupId]/activity/[activityId].tsx (1)

25-25: LGTM: Import path update.

The change from alias path to relative path is consistent with standard practices.

packages/graphql/src/public/client.json (1)

123-123: LGTM!

The hash entry for GetCourseQuizAnalytics is correctly added and follows the established pattern.

packages/graphql/src/schema/query.ts (2)

17-17: LGTM!

The CourseQuizAnalytics type is correctly imported alongside other analytics types.


785-794: LGTM!

The getCourseQuizAnalytics query field is well-structured:

  • Properly secured with user authentication
  • Follows the established pattern of other analytics queries
  • Correctly delegates to the analytics service
packages/graphql/src/public/schema.graphql (4)

27-33: LGTM!

The ActivityFeedback type is well-defined with appropriate fields for tracking feedback metrics at the activity level.


331-336: LGTM!

The CourseQuizAnalytics type is well-structured:

  • Includes both activity and instance level feedbacks
  • Uses non-null lists to ensure data consistency
  • Provides course name and total participants for context

811-818: LGTM!

The InstanceFeedback type is well-defined:

  • Captures both instance metadata and feedback metrics
  • Uses appropriate enums for activity and element types

1348-1348: LGTM!

The query field definition matches its implementation in query.ts and follows GraphQL naming conventions.

packages/i18n/messages/en.ts (1)

1877-1879: LGTM! New translation keys added for analytics features.

The new translation keys are properly structured and follow consistent naming patterns for feedback-related metrics.

packages/graphql/src/public/server.json (1)

123-123: LGTM! New GraphQL query added for course quiz analytics.

The query is well-structured with proper parameter validation and returns comprehensive feedback metrics at both instance and activity levels.

packages/i18n/messages/de.ts (1)

1891-1893: LGTM! The translations are accurate and consistent.

The new German translations for feedback analytics are well-structured and maintain consistency with the existing translations.

Let's verify the consistency with the English translations:

✅ Verification successful

German translations are accurate and consistent with English counterparts

The verification confirms that:

  • The English translations exist in en.ts with matching keys
  • German translations accurately convey the meaning:
    • "Negative Feedback Overview" → "Übersicht Negative Bewertungen"
    • "Upvotes" → "Positive Bewertungen"
    • "Downvotes" → "Negative Bewertungen"
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check if the English translations exist and match the structure
# Look for the corresponding keys in the English translation file
rg "negativeFeedbackOverview|upvotes|downvotes" packages/i18n/messages/en.ts

Length of output: 196

packages/graphql/src/ops.schema.json (4)

249-340: Well-structured ActivityFeedback type definition!

The ActivityFeedback type is well-designed with:

  • Clear and descriptive field names
  • Appropriate use of non-null constraints
  • Good type choices for the fields (String for id, Float for rates)

3862-3953: Well-designed CourseQuizAnalytics type!

The CourseQuizAnalytics type effectively aggregates quiz analytics with:

  • Comprehensive feedback arrays (activityFeedbacks and instanceFeedbacks)
  • Proper non-null constraints
  • Clear field naming

9025-9132: Well-structured InstanceFeedback type definition!

The InstanceFeedback type is well-designed with:

  • Appropriate fields for instance-level feedback
  • Consistent use of non-null constraints
  • Clear separation from ActivityFeedback type

19873-19901: Well-defined getCourseQuizAnalytics query!

The query is properly structured with:

  • Required courseId parameter
  • Appropriate return type (CourseQuizAnalytics)
  • Consistent with GraphQL best practices

packages/graphql/src/services/analytics.ts Outdated Show resolved Hide resolved
Comment on lines +30 to +32
import Layout from '../../../../../components/Layout'
import PreviewMessage from '../../../../../components/common/PreviewMessage'
import MicroLearningSubscriber from '../../../../../components/microLearning/MicroLearningSubscriber'
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 using path aliases instead of deep relative paths

Multiple deep relative paths (../../../../../) make the code harder to maintain. Consider using path aliases for all these imports:

-import Layout from '../../../../../components/Layout'
-import PreviewMessage from '../../../../../components/common/PreviewMessage'
-import MicroLearningSubscriber from '../../../../../components/microLearning/MicroLearningSubscriber'
+import Layout from '~/components/Layout'
+import PreviewMessage from '~/components/common/PreviewMessage'
+import MicroLearningSubscriber from '~/components/microLearning/MicroLearningSubscriber'

Would you like me to create a GitHub issue to track the systematic implementation of path aliases across the codebase?

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
packages/graphql/src/services/analytics.ts (1)

341-388: 🛠️ Refactor suggestion

Consider weighted averaging and simplify the implementation.

The current implementation uses unweighted averaging, which might not represent the true feedback distribution. Additionally, the implementation could be simplified.

Consider this refactoring:

 function aggregateActivityFeedbacks({
   instanceFeedbacks,
   activityType,
   activityId,
   activityName,
 }: {
   instanceFeedbacks: InstanceFeedback[]
   activityType: ActivityType
   activityId: string
   activityName: string
 }) {
   if (instanceFeedbacks.length === 0) {
     return undefined
   }

+  // Calculate total votes across all instances for weighted averaging
+  const totalVotes = instanceFeedbacks.reduce((sum, feedback) => sum + feedback.totalVotes, 0)
+
+  // Skip if no votes
+  if (totalVotes === 0) return undefined
+
+  // Calculate weighted averages
+  const { upvoteRate, downvoteRate } = instanceFeedbacks.reduce(
+    (acc, feedback) => ({
+      upvoteRate: acc.upvoteRate + (feedback.upvoteRate * feedback.totalVotes / totalVotes),
+      downvoteRate: acc.downvoteRate + (feedback.downvoteRate * feedback.totalVotes / totalVotes),
+    }),
+    { upvoteRate: 0, downvoteRate: 0 }
+  )

   return {
     id: activityId,
     activityType,
     activityName,
     upvoteRate,
     downvoteRate,
-    count: instanceFeedbacks.length,
   }
🧹 Nitpick comments (2)
packages/graphql/src/services/analytics.ts (2)

282-339: Consider improving readability and robustness of the feedback aggregation.

The function could benefit from the following improvements:

  1. Add input validation for null/undefined stacks
  2. Simplify the nested data transformations
  3. Make the division by zero handling more explicit

Consider this refactoring:

 function aggregateInstanceFeedbacks({
   stacks,
   activityType,
 }: {
   stacks: (ElementStack & {
     elements: (ElementInstance & { feedbacks: ElementFeedback[] })[]
   })[]
   activityType: ActivityType
 }): InstanceFeedback[] {
+  if (!stacks?.length) return []
+
+  // First, flatten the structure to get all elements with their feedbacks
+  const elements = stacks.flatMap(stack => stack.elements)
+
+  // Then, process each element's feedbacks
+  return elements
+    .map(element => {
+      const votes = element.feedbacks.reduce(
+        (acc, feedback) => ({
+          upvotes: acc.upvotes + (feedback.upvote ? 1 : 0),
+          downvotes: acc.downvotes + (feedback.downvote ? 1 : 0),
+        }),
+        { upvotes: 0, downvotes: 0 }
+      )
+
+      const totalVotes = votes.upvotes + votes.downvotes
+      
+      // Skip elements with no votes
+      if (totalVotes === 0) return null
+
+      return {
+        id: element.id,
+        instanceName: element.elementData.name,
+        instanceType: element.elementData.type,
+        activityType,
+        ...votes,
+        totalVotes,
+        upvoteRate: votes.upvotes / totalVotes,
+        downvoteRate: votes.downvotes / totalVotes,
+      }
+    })
+    .filter((feedback): feedback is InstanceFeedback => feedback !== null)

390-484: Consider optimizing the implementation and adding error handling.

The function could benefit from the following improvements:

  1. Add error handling for the prisma query
  2. Abstract the repeated logic for quizzes and micro learnings
  3. Consider optimizing the prisma query by selecting only needed fields

Consider this refactoring:

 export async function getCourseQuizAnalytics(
   { courseId }: { courseId: string },
   ctx: ContextWithUser
 ) {
+  try {
     const course = await ctx.prisma.course.findUnique({
       where: { id: courseId, ownerId: ctx.user.sub },
       include: {
         participations: {
+          select: { id: true }, // Only need count
+        },
         practiceQuizzes: {
           include: {
             stacks: {
               include: {
                 elements: {
                   include: {
                     feedbacks: true,
                   },
                 },
               },
             },
           },
           orderBy: { createdAt: 'desc' },
         },
         // ... similar optimization for microLearnings
       },
     })

     if (!course) {
       return null
     }

+    // Abstract the feedback aggregation logic
+    const aggregateFeedbacks = (
+      activities: typeof course.practiceQuizzes | typeof course.microLearnings,
+      activityType: ActivityType
+    ) => {
+      const instanceFeedbacks: InstanceFeedback[] = []
+      const activityFeedbacks: ActivityFeedback[] = []
+
+      activities.forEach((activity) => {
+        const activityInstanceFeedbacks = aggregateInstanceFeedbacks({
+          stacks: activity.stacks,
+          activityType,
+        })
+        instanceFeedbacks.push(...activityInstanceFeedbacks)
+
+        const activityFeedback = aggregateActivityFeedbacks({
+          instanceFeedbacks: activityInstanceFeedbacks,
+          activityType,
+          activityId: activity.id,
+          activityName: activity.name,
+        })
+
+        if (activityFeedback) {
+          activityFeedbacks.push(activityFeedback)
+        }
+      })
+
+      return { instanceFeedbacks, activityFeedbacks }
+    }

+    // Aggregate feedbacks for both types of activities
+    const quizFeedbacks = aggregateFeedbacks(
+      course.practiceQuizzes,
+      ActivityType.PRACTICE_QUIZ
+    )
+    const microFeedbacks = aggregateFeedbacks(
+      course.microLearnings,
+      ActivityType.MICRO_LEARNING
+    )

     return {
       name: course.name,
       totalParticipants: course.participations.length,
-      instanceFeedbacks,
-      activityFeedbacks,
+      instanceFeedbacks: [
+        ...quizFeedbacks.instanceFeedbacks,
+        ...microFeedbacks.instanceFeedbacks,
+      ],
+      activityFeedbacks: [
+        ...quizFeedbacks.activityFeedbacks,
+        ...microFeedbacks.activityFeedbacks,
+      ],
     }
+  } catch (error) {
+    console.error('Error in getCourseQuizAnalytics:', error)
+    throw error
+  }
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 81ce688 and 0d795b8.

📒 Files selected for processing (1)
  • packages/graphql/src/services/analytics.ts (2 hunks)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (4)
packages/graphql/src/ops.ts (1)

4212-4212: Consider using GraphQL fragments for better maintainability.

The GetCoursePerformanceAnalyticsDocument query is quite large. Consider extracting common field selections into reusable fragments, particularly for the feedback-related fields and performance metrics. This would improve maintainability and reduce duplication.

Example structure:

fragment FeedbackFields on ActivityFeedback {
  __typename
  id
  activityType
  upvoteRate
  downvoteRate
  feedbackCount
}

# Then in the query:
activityFeedbacks {
  ...FeedbackFields
  activityName
}
apps/frontend-manage/src/components/analytics/performance/ActivityInstanceFeedbacksPlot.tsx (1)

150-159: Consider extracting the feedback title resolution into a helper function.
Currently, you're switching on __typename in-line to pick between activityName and instanceName. Extracting this logic into a helper function or a small mapping could simplify readability and provide flexibility if new feedback types are introduced in the future.

apps/frontend-manage/src/components/analytics/performance/useElementFeedbackFilters.ts (2)

10-30: Consider adding explicit return type annotation.

The function's return type should be explicitly defined for better type safety and documentation.

-function useElementFeedbackFilters({
+function useElementFeedbackFilters({
   type,
   activityFeedbacks,
   instanceFeedbacks,
   activityType,
   elementType,
   activitySearchTerm,
   instanceSearchTerm,
   activitySearch,
   instanceSearch,
-}: {
+}: {
   type: 'activity' | 'instance'
   activityFeedbacks: ActivityFeedback[]
   instanceFeedbacks: InstanceFeedback[]
   activityType: ActivityType | 'all'
   elementType: ElementType | 'all'
   activitySearchTerm: string
   instanceSearchTerm: string
   activitySearch: Search
   instanceSearch: Search
-}) {
+}): ActivityFeedback[] | InstanceFeedback[] {

52-70: Extract search term sanitization to a utility function.

The search term sanitization logic is duplicated. Consider extracting it to a reusable function.

+const sanitizeSearchTerm = (term: string): string => term?.trim().toLowerCase() ?? ''
+
 } else {
+  if (!instanceFeedbacks?.length) return []
   const rawData = instanceFeedbacks
 
   // filter by element type
   const typeFiltered = rawData.filter((feedback) => {
     if (elementType === 'all') {
       return true
     }
 
     return feedback.instanceType === elementType
   })
 
   // filter by search word
-  const sanitizedSearchTerm = instanceSearchTerm.trim().toLowerCase()
-  if (sanitizedSearchTerm) {
-    return instanceSearch.search(sanitizedSearchTerm) as InstanceFeedback[]
+  const sanitizedSearchTerm = sanitizeSearchTerm(instanceSearchTerm)
+  if (sanitizedSearchTerm && instanceSearch) {
+    const searchResults = instanceSearch.search(sanitizedSearchTerm)
+    return searchResults.length ? searchResults as InstanceFeedback[] : typeFiltered
   }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0d795b8 and b91ca12.

📒 Files selected for processing (15)
  • apps/frontend-manage/src/components/analytics/performance/ActivityInstanceFeedbacksPlot.tsx (1 hunks)
  • apps/frontend-manage/src/components/analytics/performance/ElementFeedbackBarChart.tsx (1 hunks)
  • apps/frontend-manage/src/components/analytics/performance/useElementFeedbackFilters.ts (1 hunks)
  • apps/frontend-manage/src/components/analytics/performance/useElementFeedbackSearch.ts (1 hunks)
  • apps/frontend-manage/src/pages/analytics/[courseId]/performance.tsx (2 hunks)
  • apps/frontend-manage/src/pages/analytics/[courseId]/quizzes.tsx (1 hunks)
  • packages/graphql/src/graphql/ops/QGetCoursePerformanceAnalytics.graphql (1 hunks)
  • packages/graphql/src/ops.schema.json (4 hunks)
  • packages/graphql/src/ops.ts (5 hunks)
  • packages/graphql/src/public/client.json (1 hunks)
  • packages/graphql/src/public/schema.graphql (3 hunks)
  • packages/graphql/src/public/server.json (1 hunks)
  • packages/graphql/src/schema/analytics.ts (3 hunks)
  • packages/graphql/src/services/analytics.ts (4 hunks)
  • packages/types/src/index.ts (1 hunks)
🔥 Files not summarized due to errors (1)
  • packages/graphql/src/ops.ts: Error: Server error: no LLM provider could handle the message
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/graphql/src/public/client.json
  • apps/frontend-manage/src/pages/analytics/[courseId]/performance.tsx
  • apps/frontend-manage/src/pages/analytics/[courseId]/quizzes.tsx
  • packages/graphql/src/public/server.json
  • packages/graphql/src/ops.schema.json
🔇 Additional comments (10)
packages/graphql/src/ops.ts (2)

49-57: LGTM! Well-structured feedback type definitions.

The ActivityFeedback and InstanceFeedback types are well-defined with consistent naming and proper type annotations for all fields.

Also applies to: 887-896


372-375: LGTM! Consistent type extensions.

The new feedback-related fields are properly integrated into the CoursePerformanceAnalytics type, maintaining consistency with the existing structure.

packages/graphql/src/graphql/ops/QGetCoursePerformanceAnalytics.graphql (1)

56-74: Verify usage of instanceType in downstream code.
If "instanceType" is not used anywhere else, consider removing it from the query to reduce payload size. Otherwise, ensure you are leveraging it downstream.

apps/frontend-manage/src/components/analytics/performance/ElementFeedbackBarChart.tsx (1)

22-23: Verify that feedback rate is indeed a ratio instead of a percentage.
Rounding “downvoteRate” and “upvoteRate” after multiplying by 100 is correct for fractional rates. Ensure the source data is indeed 0–1 to avoid incorrect percentage displays.

packages/types/src/index.ts (1)

408-425: LGTM! Well-structured type definitions.

The new types InstanceFeedback and ActivityFeedback are well-defined and consistent with the existing type patterns in the codebase.

packages/graphql/src/schema/analytics.ts (1)

236-261: LGTM! Well-structured GraphQL schema changes.

The new GraphQL types and interface updates are consistent with the type definitions and follow the established patterns in the codebase.

Also applies to: 270-271, 293-298

packages/graphql/src/services/analytics.ts (3)

116-174: Consider weighted averaging for more accurate feedback representation.

The implementation correctly aggregates instance feedbacks, but consider weighing the feedback rates based on the number of votes to give more weight to instances with higher participation.


Line range hint 426-499: LGTM! Well-structured implementation.

The function properly handles error cases and efficiently combines both performance and feedback analytics.


204-209: ⚠️ Potential issue

Fix weighted average calculation.

The current implementation of weighted average is incorrect. When combining rates, it's using the count of instances instead of the total votes.

Apply this fix:

-      acc.upvoteRate =
-        (acc.upvoteRate * acc.feedbackCount + instanceFeedback.upvoteRate) /
-        (acc.feedbackCount + 1)
-      acc.downvoteRate =
-        (acc.downvoteRate * acc.feedbackCount + instanceFeedback.downvoteRate) /
-        (acc.feedbackCount + 1)
+      const totalVotesSoFar = acc.feedbackCount * instanceFeedback.feedbackCount
+      const newTotalVotes = totalVotesSoFar + instanceFeedback.feedbackCount
+      acc.upvoteRate = 
+        (acc.upvoteRate * totalVotesSoFar + 
+         instanceFeedback.upvoteRate * instanceFeedback.feedbackCount) / 
+        newTotalVotes
+      acc.downvoteRate = 
+        (acc.downvoteRate * totalVotesSoFar + 
+         instanceFeedback.downvoteRate * instanceFeedback.feedbackCount) / 
+        newTotalVotes

Likely invalid or redundant comment.

packages/graphql/src/public/schema.graphql (1)

27-34: LGTM! Well-defined types.

The new ActivityFeedback and InstanceFeedback types are well-structured and align perfectly with their implementation.

Also applies to: 807-815

@sjschlapbach sjschlapbach merged commit 990cd6f into v3-analytics Dec 20, 2024
8 of 12 checks passed
@sjschlapbach sjschlapbach deleted the instance-activity-ratings branch December 20, 2024 11:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

1 participant