diff --git a/scripts/langindex.json b/scripts/langindex.json index a0e8aad4b..523dd673a 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -104,10 +104,12 @@ "addon.calendar.currentmonth": "local_moodlemobileapp", "addon.calendar.daynext": "calendar", "addon.calendar.dayprev": "calendar", + "addon.calendar.dayviewtitle": "calendar", "addon.calendar.defaultnotificationtime": "local_moodlemobileapp", "addon.calendar.deleteallevents": "calendar", "addon.calendar.deleteevent": "calendar", "addon.calendar.deleteoneevent": "calendar", + "addon.calendar.detailedmonthviewtitle": "calendar", "addon.calendar.durationminutes": "calendar", "addon.calendar.durationnone": "calendar", "addon.calendar.durationuntil": "calendar", @@ -369,6 +371,7 @@ "addon.mod_assign.gradelocked": "assign", "addon.mod_assign.gradenotsynced": "local_moodlemobileapp", "addon.mod_assign.gradeoutof": "assign", + "addon.mod_assign.grading": "assign", "addon.mod_assign.gradingstatus": "assign", "addon.mod_assign.groupsubmissionsettings": "assign", "addon.mod_assign.hiddenuser": "assign", @@ -425,6 +428,7 @@ "addon.mod_assign.submittedlate": "assign", "addon.mod_assign.submittedovertime": "assign", "addon.mod_assign.submittedundertime": "assign", + "addon.mod_assign.subpagetitle": "assign", "addon.mod_assign.syncblockedusercomponent": "local_moodlemobileapp", "addon.mod_assign.timelimit": "assign", "addon.mod_assign.timemodified": "assign", @@ -2235,6 +2239,7 @@ "core.play": "local_moodlemobileapp", "core.previous": "moodle", "core.proceed": "moodle", + "core.publicprofile": "moodle", "core.pulltorefresh": "local_moodlemobileapp", "core.qrscanner": "local_moodlemobileapp", "core.question.answer": "question", @@ -2347,9 +2352,9 @@ "core.settings.disabled": "lesson", "core.settings.disallowed": "message", "core.settings.displayformat": "local_moodlemobileapp", + "core.settings.enableanalytics": "local_moodlemobileapp", + "core.settings.enableanalyticsdescription": "local_moodlemobileapp", "core.settings.enabledownloadsection": "local_moodlemobileapp", - "core.settings.enablefirebaseanalytics": "local_moodlemobileapp", - "core.settings.enablefirebaseanalyticsdescription": "local_moodlemobileapp", "core.settings.enablerichtexteditor": "local_moodlemobileapp", "core.settings.enablerichtexteditordescription": "local_moodlemobileapp", "core.settings.encryptedpushsupported": "local_moodlemobileapp", diff --git a/src/addons/badges/pages/issued-badge/issued-badge.ts b/src/addons/badges/pages/issued-badge/issued-badge.ts index 890ccbfe0..1440e7f02 100644 --- a/src/addons/badges/pages/issued-badge/issued-badge.ts +++ b/src/addons/badges/pages/issued-badge/issued-badge.ts @@ -26,6 +26,8 @@ import { ActivatedRoute } from '@angular/router'; import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; import { AddonBadgesUserBadgesSource } from '@addons/badges/classes/user-badges-source'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreTime } from '@singletons/time'; /** * Page that displays the list of calendar events. @@ -38,6 +40,7 @@ export class AddonBadgesIssuedBadgePage implements OnInit, OnDestroy { protected badgeHash = ''; protected userId!: number; + protected logView: (badge: AddonBadgesUserBadge) => void; courseId = 0; user?: CoreUserProfile; @@ -58,6 +61,16 @@ export class AddonBadgesIssuedBadgePage implements OnInit, OnDestroy { ); this.badges = new CoreSwipeNavigationItemsManager(source); + + this.logView = CoreTime.once((badge) => { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'core_badges_view_user_badges', + name: badge.name, + data: { id: badge.uniquehash, category: 'badges' }, + url: `/badges/badge.php?hash=${badge.uniquehash}`, + }); + }); } /** @@ -105,6 +118,8 @@ export class AddonBadgesIssuedBadgePage implements OnInit, OnDestroy { this.course = undefined; } } + + this.logView(badge); } catch (message) { CoreDomUtils.showErrorModalDefault(message, 'Error getting badge data.'); } diff --git a/src/addons/badges/pages/user-badges/user-badges.ts b/src/addons/badges/pages/user-badges/user-badges.ts index 31ba89d84..9d02a2c5f 100644 --- a/src/addons/badges/pages/user-badges/user-badges.ts +++ b/src/addons/badges/pages/user-badges/user-badges.ts @@ -24,6 +24,9 @@ import { CoreNavigator } from '@services/navigator'; import { CoreListItemsManager } from '@classes/items-management/list-items-manager'; import { AddonBadgesUserBadgesSource } from '@addons/badges/classes/user-badges-source'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreTime } from '@singletons/time'; +import { Translate } from '@singletons'; /** * Page that displays the list of calendar events. @@ -39,6 +42,8 @@ export class AddonBadgesUserBadgesPage implements AfterViewInit, OnDestroy { @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; + protected logView: () => void; + constructor() { let courseId = CoreNavigator.getRouteNumberParam('courseId') ?? 0; // Use 0 for site badges. const userId = CoreNavigator.getRouteNumberParam('userId') ?? CoreSites.getCurrentSiteUserId(); @@ -52,6 +57,16 @@ export class AddonBadgesUserBadgesPage implements AfterViewInit, OnDestroy { CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(AddonBadgesUserBadgesSource, [courseId, userId]), AddonBadgesUserBadgesPage, ); + + this.logView = CoreTime.once(() => { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_badges_view_user_badges', + name: Translate.instant('addon.badges.badges'), + data: { courseId: this.badges.getSource().COURSE_ID, category: 'badges' }, + url: '/badges/mybadges.php', + }); + }); } /** @@ -95,6 +110,8 @@ export class AddonBadgesUserBadgesPage implements AfterViewInit, OnDestroy { try { await this.badges.reload(); + + this.logView(); } catch (message) { CoreDomUtils.showErrorModalDefault(message, 'Error loading badges'); diff --git a/src/addons/blog/pages/entries/entries.ts b/src/addons/blog/pages/entries/entries.ts index 11d2b32c6..b5ee60335 100644 --- a/src/addons/blog/pages/entries/entries.ts +++ b/src/addons/blog/pages/entries/entries.ts @@ -20,11 +20,14 @@ import { CoreMainMenuDeepLinkManager } from '@features/mainmenu/classes/deep-lin import { CoreTag } from '@features/tag/services/tag'; import { CoreUser, CoreUserProfile } from '@features/user/services/user'; import { IonRefresher } from '@ionic/angular'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; +import { CoreUrlUtils } from '@services/utils/url'; import { CoreUtils } from '@services/utils/utils'; +import { CoreTime } from '@singletons/time'; /** * Page that displays the list of blog entries. @@ -43,7 +46,7 @@ export class AddonBlogEntriesPage implements OnInit { protected canLoadMoreEntries = false; protected canLoadMoreUserEntries = true; protected siteHomeId: number; - protected fetchSuccess = false; + protected logView: () => void; loaded = false; canLoadMore = false; @@ -61,6 +64,25 @@ export class AddonBlogEntriesPage implements OnInit { constructor() { this.currentUserId = CoreSites.getCurrentSiteUserId(); this.siteHomeId = CoreSites.getCurrentSiteHomeId(); + + this.logView = CoreTime.once(async () => { + await CoreUtils.ignoreErrors(AddonBlog.logView(this.filter)); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_blog_view_entries', + name: this.title, + data: { + ...this.filter, + category: 'blog', + }, + url: CoreUrlUtils.addParamsToUrl('/blog/index.php', { + ...this.filter, + modid: this.filter.cmid, + cmid: undefined, + }), + }); + }); } /** @@ -200,10 +222,7 @@ export class AddonBlogEntriesPage implements OnInit { await Promise.all(promises); - if (!this.fetchSuccess) { - this.fetchSuccess = true; - CoreUtils.ignoreErrors(AddonBlog.logView(this.filter)); - } + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.blog.errorloadentries', true); this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading. diff --git a/src/addons/blog/services/blog.ts b/src/addons/blog/services/blog.ts index 83e588deb..c3491951e 100644 --- a/src/addons/blog/services/blog.ts +++ b/src/addons/blog/services/blog.ts @@ -14,7 +14,6 @@ import { Injectable } from '@angular/core'; import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; -import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; import { CoreTagItem } from '@features/tag/services/tag'; import { CoreSites } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; @@ -104,8 +103,6 @@ export class AddonBlogProvider { * @returns Promise to be resolved when done. */ async logView(filter: AddonBlogFilter = {}, siteId?: string): Promise { - CorePushNotifications.logViewListEvent('blog', 'core_blog_view_entries', filter, siteId); - const site = await CoreSites.getSite(siteId); const data: AddonBlogViewEntriesWSParams = { diff --git a/src/addons/calendar/components/calendar/calendar.ts b/src/addons/calendar/components/calendar/calendar.ts index 0295e9d15..1d9c2eea9 100644 --- a/src/addons/calendar/components/calendar/calendar.ts +++ b/src/addons/calendar/components/calendar/calendar.ts @@ -49,6 +49,10 @@ import { } from '@classes/items-management/swipe-slides-dynamic-items-manager-source'; import { CoreSwipeSlidesDynamicItemsManager } from '@classes/items-management/swipe-slides-dynamic-items-manager'; import moment from 'moment-timezone'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreUrlUtils } from '@services/utils/url'; +import { CoreTime } from '@singletons/time'; +import { Translate } from '@singletons'; /** * Component that displays a calendar. @@ -81,6 +85,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro // Observers and listeners. protected undeleteEventObserver: CoreEventObserver; protected managerUnsubscribe?: () => void; + protected logView: () => void; constructor(differs: KeyValueDiffers) { this.currentSiteId = CoreSites.getCurrentSiteId(); @@ -107,6 +112,29 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro this.hiddenDiffer = this.hidden; this.filterDiffer = differs.find(this.filter ?? {}).create(); + + this.logView = CoreTime.once(() => { + const month = this.manager?.getSelectedItem(); + if (!month) { + return; + } + + const params = { + course: this.filter?.courseId, + time: month.moment.unix(), + }; + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_calendar_get_calendar_monthly_view', + name: Translate.instant('addon.calendar.detailedmonthviewtitle', { $a: this.periodName }), + data: { + ...params, + category: 'calendar', + }, + url: CoreUrlUtils.addParamsToUrl('/calendar/view.php?view=month', params), + }); + }); } @HostBinding('attr.hidden') get hiddenAttribute(): string | null { @@ -124,7 +152,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro const source = new AddonCalendarMonthSlidesItemsManagerSource(this, moment({ year: this.initialYear, month: this.initialMonth ? this.initialMonth - 1 : undefined, - })); + }).startOf('month')); this.manager = new CoreSwipeSlidesDynamicItemsManager(source); this.managerUnsubscribe = this.manager.addListener({ onSelectedItemUpdated: (item) => { @@ -176,6 +204,8 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro await this.manager?.getSource().fetchData(); await this.manager?.getSource().load(this.manager?.getSelectedItem()); + + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); } diff --git a/src/addons/calendar/components/upcoming-events/upcoming-events.ts b/src/addons/calendar/components/upcoming-events/upcoming-events.ts index 4586fd259..909169ad8 100644 --- a/src/addons/calendar/components/upcoming-events/upcoming-events.ts +++ b/src/addons/calendar/components/upcoming-events/upcoming-events.ts @@ -25,6 +25,10 @@ import { AddonCalendarHelper, AddonCalendarFilter } from '../../services/calenda import { AddonCalendarOffline } from '../../services/calendar-offline'; import { CoreCategoryData, CoreCourses } from '@features/courses/services/courses'; import { CoreConstants } from '@/core/constants'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreUrlUtils } from '@services/utils/url'; +import { CoreTime } from '@singletons/time'; +import { Translate } from '@singletons'; /** * Component that displays upcoming events. @@ -54,6 +58,7 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, DoCheck, On protected lookAhead = 0; protected timeFormat?: string; protected differ: KeyValueDiffer; // To detect changes in the data input. + protected logView: () => void; // Observers. protected undeleteEventObserver: CoreEventObserver; @@ -84,6 +89,23 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, DoCheck, On ); this.differ = differs.find([]).create(); + + this.logView = CoreTime.once(() => { + const params = { + course: this.filter?.courseId, + }; + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_calendar_get_calendar_upcoming_view', + name: Translate.instant('addon.calendar.upcomingevents'), + data: { + ...params, + category: 'calendar', + }, + url: CoreUrlUtils.addParamsToUrl('/calendar/view.php?view=upcoming', params), + }); + }); } /** @@ -148,8 +170,9 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, DoCheck, On try { await Promise.all(promises); - this.fetchEvents(); + await this.fetchEvents(); + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); } diff --git a/src/addons/calendar/lang.json b/src/addons/calendar/lang.json index c448ad2db..8820fd715 100644 --- a/src/addons/calendar/lang.json +++ b/src/addons/calendar/lang.json @@ -11,10 +11,12 @@ "currentmonth": "Current Month", "daynext": "Next day", "dayprev": "Previous day", + "dayviewtitle": "Day view: {{$a}}", "defaultnotificationtime": "Default notification time", "deleteallevents": "Delete all events", "deleteevent": "Delete event", "deleteoneevent": "Delete this event", + "detailedmonthviewtitle": "Detailed month view: {{$a}}", "durationminutes": "Duration in minutes", "durationnone": "Without duration", "durationuntil": "Until", diff --git a/src/addons/calendar/pages/day/day.ts b/src/addons/calendar/pages/day/day.ts index 4ad52ea63..1f539005c 100644 --- a/src/addons/calendar/pages/day/day.ts +++ b/src/addons/calendar/pages/day/day.ts @@ -33,7 +33,7 @@ import { CoreCategoryData, CoreCourses, CoreEnrolledCourseData } from '@features import { CoreCoursesHelper } from '@features/courses/services/courses-helper'; import { AddonCalendarFilterComponent } from '../../components/filter/filter'; import moment from 'moment-timezone'; -import { NgZone } from '@singletons'; +import { NgZone, Translate } from '@singletons'; import { CoreNavigator } from '@services/navigator'; import { Params } from '@angular/router'; import { Subscription } from 'rxjs'; @@ -47,6 +47,9 @@ import { } from '@classes/items-management/swipe-slides-dynamic-items-manager-source'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; import { AddonCalendarEventsSource } from '@addons/calendar/classes/events-source'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreUrlUtils } from '@services/utils/url'; +import { CoreTime } from '@singletons/time'; /** * Page that displays the calendar events for a certain day. @@ -73,6 +76,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { protected onlineObserver: Subscription; protected filterChangedObserver: CoreEventObserver; protected managerUnsubscribe?: () => void; + protected logView: () => void; periodName?: string; manager?: CoreSwipeSlidesDynamicItemsManager; @@ -186,6 +190,28 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { this.isOnline = CoreNetwork.isOnline(); }); }); + + this.logView = CoreTime.once(() => { + const day = this.manager?.getSelectedItem(); + if (!day) { + return; + } + const params = { + course: this.filter.courseId, + time: day.moment.unix(), + }; + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_calendar_get_calendar_day_view', + name: Translate.instant('addon.calendar.dayviewtitle', { $a: this.periodName }), + data: { + ...params, + category: 'calendar', + }, + url: CoreUrlUtils.addParamsToUrl('/calendar/view.php?view=day', params), + }); + }); } /** @@ -209,7 +235,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { year: CoreNavigator.getRouteNumberParam('year'), month: month ? month - 1 : undefined, date: CoreNavigator.getRouteNumberParam('day'), - })); + }).startOf('day')); this.manager = new CoreSwipeSlidesDynamicItemsManager(source); this.managerUnsubscribe = this.manager.addListener({ onSelectedItemUpdated: (item) => { @@ -246,6 +272,8 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { await this.manager?.getSource().fetchData(this.filter.courseId); await this.manager?.getSource().load(this.manager?.getSelectedItem()); + + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); } @@ -500,6 +528,7 @@ class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicIte canCreate = false; protected dayPage: AddonCalendarDayPage; + protected sendLog = true; constructor(page: AddonCalendarDayPage, initialMoment: moment.Moment) { super({ moment: initialMoment }); @@ -780,6 +809,7 @@ class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicIte promises.push(AddonCalendar.invalidateTimeFormat()); this.categories = undefined; // Get categories again. + this.sendLog = true; if (selectedDay) { selectedDay.dirty = true; diff --git a/src/addons/competency/pages/competencies/competencies.page.ts b/src/addons/competency/pages/competencies/competencies.page.ts index 16eb9076a..e2c01de02 100644 --- a/src/addons/competency/pages/competencies/competencies.page.ts +++ b/src/addons/competency/pages/competencies/competencies.page.ts @@ -27,6 +27,9 @@ import { AddonCompetencyPlanCompetenciesSource } from '@addons/competency/classe import { AddonCompetencyCourseCompetenciesSource } from '@addons/competency/classes/competency-course-competencies-source'; import { CoreListItemsManager } from '@classes/items-management/list-items-manager'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreSites } from '@services/sites'; +import { CoreTime } from '@singletons/time'; /** * Page that displays the list of competencies of a learning plan. @@ -46,8 +49,11 @@ export class AddonCompetencyCompetenciesPage implements AfterViewInit, OnDestroy title = ''; + protected logView: () => void; + constructor() { const planId = CoreNavigator.getRouteNumberParam('planId'); + this.logView = CoreTime.once(() => this.performLogView()); if (!planId) { const courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); @@ -96,6 +102,8 @@ export class AddonCompetencyCompetenciesPage implements AfterViewInit, OnDestroy } else { this.title = Translate.instant('addon.competency.coursecompetencies'); } + + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error getting competencies data.'); } @@ -122,4 +130,42 @@ export class AddonCompetencyCompetenciesPage implements AfterViewInit, OnDestroy this.competencies.destroy(); } + /** + * Log view. + */ + protected performLogView(): void { + const source = this.competencies.getSource(); + + if (source instanceof AddonCompetencyPlanCompetenciesSource) { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'tool_lp_data_for_plan_page', + name: this.title, + data: { + category: 'competency', + planid: source.PLAN_ID, + }, + url: `/admin/tool/lp/plan.php?id=${source.PLAN_ID}`, + }); + + return; + } + + if (source.USER_ID && source.USER_ID !== CoreSites.getCurrentSiteUserId()) { + // Only log event when viewing own competencies. In LMS viewing students competencies uses a different view. + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'tool_lp_data_for_course_competencies_page', + name: this.title, + data: { + category: 'competency', + courseid: source.COURSE_ID, + }, + url: `/admin/tool/lp/coursecompetencies.php?courseid=${source.COURSE_ID}`, + }); + } + } diff --git a/src/addons/competency/pages/competency/competency.page.ts b/src/addons/competency/pages/competency/competency.page.ts index d3a9f825b..bf14914ac 100644 --- a/src/addons/competency/pages/competency/competency.page.ts +++ b/src/addons/competency/pages/competency/competency.page.ts @@ -27,6 +27,7 @@ import { AddonCompetency, AddonCompetencyDataForPlanPageCompetency, AddonCompetencyDataForCourseCompetenciesPageCompetency, + AddonCompetencyProvider, } from '@addons/competency/services/competency'; import { CoreNavigator } from '@services/navigator'; import { IonRefresher } from '@ionic/angular'; @@ -38,6 +39,9 @@ import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/ import { AddonCompetencyPlanCompetenciesSource } from '@addons/competency/classes/competency-plan-competencies-source'; import { ActivatedRouteSnapshot } from '@angular/router'; import { AddonCompetencyCourseCompetenciesSource } from '@addons/competency/classes/competency-course-competencies-source'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreUrlUtils } from '@services/utils/url'; /** * Page that displays the competency information. @@ -58,9 +62,11 @@ export class AddonCompetencyCompetencyPage implements OnInit, OnDestroy { contextLevel?: string; contextInstanceId?: number; - protected fetchSuccess = false; + protected logView: () => void; constructor() { + this.logView = CoreTime.once(() => this.performLogView()); + try { const planId = CoreNavigator.getRouteNumberParam('planId'); @@ -156,31 +162,7 @@ export class AddonCompetencyCompetencyPage implements OnInit, OnDestroy { } }); - if (!this.fetchSuccess) { - this.fetchSuccess = true; - const name = this.competency.competency.competency.shortname; - - if (source instanceof AddonCompetencyPlanCompetenciesSource) { - this.planStatus && await CoreUtils.ignoreErrors( - AddonCompetency.logCompetencyInPlanView( - source.PLAN_ID, - this.requireCompetencyId(), - this.planStatus, - name, - source.user?.id, - ), - ); - } else { - await CoreUtils.ignoreErrors( - AddonCompetency.logCompetencyInCourseView( - source.COURSE_ID, - this.requireCompetencyId(), - name, - source.USER_ID, - ), - ); - } - } + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error getting competency data.'); } @@ -288,6 +270,73 @@ export class AddonCompetencyCompetencyPage implements OnInit, OnDestroy { return competency.usercompetencysummary; } + /** + * Log view. + */ + protected async performLogView(): Promise { + if (!this.competency) { + return; + } + + const source = this.competencies.getSource(); + const compId = this.requireCompetencyId(); + const name = this.competency.competency.competency.shortname; + const userId = source.user?.id; + + if (source instanceof AddonCompetencyPlanCompetenciesSource) { + if (!this.planStatus) { + return; + } + + await CoreUtils.ignoreErrors( + AddonCompetency.logCompetencyInPlanView(source.PLAN_ID, compId, this.planStatus, name, userId), + ); + + const wsName = this.planStatus === AddonCompetencyProvider.STATUS_COMPLETE + ? 'core_competency_user_competency_plan_viewed' + : 'core_competency_user_competency_viewed_in_plan'; + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: wsName, + name, + data: { + id: compId, + category: 'competency', + planid: source.PLAN_ID, + planstatus: this.planStatus, + userid: userId, + }, + url: CoreUrlUtils.addParamsToUrl('/admin/tool/lp/user_competency_in_plan.php', { + planid: source.PLAN_ID, + userid: userId, + competencyid: compId, + }), + }); + + return; + } + + await CoreUtils.ignoreErrors(AddonCompetency.logCompetencyInCourseView(source.COURSE_ID, compId, name, source.USER_ID)); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'core_competency_user_competency_viewed_in_course', + name, + data: { + id: compId, + category: 'competency', + courseid: source.COURSE_ID, + userid: userId, + }, + url: CoreUrlUtils.addParamsToUrl('/admin/tool/lp/user_competency_in_course.php', { + courseid: source.COURSE_ID, + competencyid: compId, + userid: userId, + }), + }); + } + } /** diff --git a/src/addons/competency/pages/competencysummary/competencysummary.page.ts b/src/addons/competency/pages/competencysummary/competencysummary.page.ts index 28554cdb7..1495a9e6e 100644 --- a/src/addons/competency/pages/competencysummary/competencysummary.page.ts +++ b/src/addons/competency/pages/competencysummary/competencysummary.page.ts @@ -20,6 +20,8 @@ import { CoreNavigator } from '@services/navigator'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; import { ADDON_COMPETENCY_SUMMARY_PAGE } from '@addons/competency/competency.module'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays the competency summary. @@ -36,7 +38,30 @@ export class AddonCompetencyCompetencySummaryPage implements OnInit { contextLevel?: ContextLevel; contextInstanceId?: number; - protected fetchSuccess = false; // Whether a fetch was finished successfully. + protected logView: () => void; + + constructor() { + this.logView = CoreTime.once(async () => { + if (!this.competency) { + return; + } + + await CoreUtils.ignoreErrors( + AddonCompetency.logCompetencyView(this.competencyId, this.competency.competency.shortname), + ); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_competency_competency_viewed', + name: this.competency.competency.shortname, + data: { + competencyId: this.competencyId, + category: 'competency', + }, + url: `/admin/tool/lp/user_competency.php?id=${this.competencyId}`, + }); + }); + } /** * @inheritdoc @@ -77,10 +102,7 @@ export class AddonCompetencyCompetencySummaryPage implements OnInit { this.competency = result.competency; - if (!this.fetchSuccess) { - this.fetchSuccess = true; - CoreUtils.ignoreErrors(AddonCompetency.logCompetencyView(this.competencyId, this.competency.competency.shortname)); - } + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error getting competency summary data.'); } diff --git a/src/addons/competency/pages/coursecompetencies/coursecompetencies.page.ts b/src/addons/competency/pages/coursecompetencies/coursecompetencies.page.ts index d091bd470..cf780f27d 100644 --- a/src/addons/competency/pages/coursecompetencies/coursecompetencies.page.ts +++ b/src/addons/competency/pages/coursecompetencies/coursecompetencies.page.ts @@ -26,6 +26,10 @@ import { ADDON_COMPETENCY_SUMMARY_PAGE } from '@addons/competency/competency.mod import { CoreListItemsManager } from '@classes/items-management/list-items-manager'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; import { AddonCompetencyCourseCompetenciesSource } from '@addons/competency/classes/competency-course-competencies-source'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreSites } from '@services/sites'; +import { Translate } from '@singletons'; /** * Page that displays the list of competencies of a course. @@ -41,7 +45,11 @@ export class AddonCompetencyCourseCompetenciesPage implements OnInit, OnDestroy AddonCompetencyCourseCompetenciesSource >; + protected logView: () => void; + constructor() { + this.logView = CoreTime.once(() => this.performLogView()); + try { const courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); const userId = CoreNavigator.getRouteNumberParam('userId'); @@ -53,7 +61,6 @@ export class AddonCompetencyCourseCompetenciesPage implements OnInit, OnDestroy this.competencies = new CoreListItemsManager(source, AddonCompetencyCourseCompetenciesPage); } catch (error) { CoreDomUtils.showErrorModal(error); - CoreNavigator.back(); return; @@ -112,6 +119,8 @@ export class AddonCompetencyCourseCompetenciesPage implements OnInit, OnDestroy protected async fetchCourseCompetencies(): Promise { try { await this.competencies.getSource().reload(); + + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error getting course competencies data.'); } @@ -147,4 +156,26 @@ export class AddonCompetencyCourseCompetenciesPage implements OnInit, OnDestroy }); } + /** + * Log view. + */ + protected performLogView(): void { + const source = this.competencies.getSource(); + if (source.USER_ID && source.USER_ID !== CoreSites.getCurrentSiteUserId()) { + // Only log event when viewing own competencies. In LMS viewing students competencies uses a different view. + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'tool_lp_data_for_course_competencies_page', + name: Translate.instant('addon.competency.coursecompetencies'), + data: { + category: 'competency', + courseid: source.COURSE_ID, + }, + url: `/admin/tool/lp/coursecompetencies.php?courseid=${source.COURSE_ID}`, + }); + } + } diff --git a/src/addons/competency/pages/plan/plan.ts b/src/addons/competency/pages/plan/plan.ts index 85d06ca30..5c57a659b 100644 --- a/src/addons/competency/pages/plan/plan.ts +++ b/src/addons/competency/pages/plan/plan.ts @@ -23,6 +23,8 @@ import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/ import { AddonCompetencyPlansSource } from '@addons/competency/classes/competency-plans-source'; import { CoreListItemsManager } from '@classes/items-management/list-items-manager'; import { AddonCompetencyPlanCompetenciesSource } from '@addons/competency/classes/competency-plan-competencies-source'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreTime } from '@singletons/time'; /** * Page that displays a learning plan. @@ -36,7 +38,11 @@ export class AddonCompetencyPlanPage implements OnInit, OnDestroy { plans!: CoreSwipeNavigationItemsManager; competencies!: CoreListItemsManager; + protected logView: () => void; + constructor() { + this.logView = CoreTime.once(() => this.performLogView()); + try { const planId = CoreNavigator.getRequiredRouteNumberParam('planId'); const userId = CoreNavigator.getRouteNumberParam('userId'); @@ -93,6 +99,8 @@ export class AddonCompetencyPlanPage implements OnInit, OnDestroy { protected async fetchLearningPlan(): Promise { try { await this.competencies.getSource().reload(); + + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error getting learning plan data.'); } @@ -111,4 +119,26 @@ export class AddonCompetencyPlanPage implements OnInit, OnDestroy { }); } + /** + * Log view. + */ + protected performLogView(): void { + if (!this.plan) { + return; + } + + const planId = this.competencies.getSource().PLAN_ID; + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'tool_lp_data_for_plan_page', + name: this.plan.plan.name, + data: { + category: 'competency', + planid: planId, + }, + url: `/admin/tool/lp/coursecompetencies.php?id=${planId}`, + }); + } + } diff --git a/src/addons/competency/pages/planlist/planlist.ts b/src/addons/competency/pages/planlist/planlist.ts index eb314632a..991aa9690 100644 --- a/src/addons/competency/pages/planlist/planlist.ts +++ b/src/addons/competency/pages/planlist/planlist.ts @@ -20,6 +20,10 @@ import { CoreNavigator } from '@services/navigator'; import { AddonCompetencyPlanFormatted, AddonCompetencyPlansSource } from '@addons/competency/classes/competency-plans-source'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; import { CoreListItemsManager } from '@classes/items-management/list-items-manager'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreSites } from '@services/sites'; +import { Translate } from '@singletons'; /** * Page that displays the list of learning plans. @@ -34,11 +38,25 @@ export class AddonCompetencyPlanListPage implements AfterViewInit, OnDestroy { plans: CoreListItemsManager; + protected logView: () => void; + constructor() { const userId = CoreNavigator.getRouteNumberParam('userId'); const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(AddonCompetencyPlansSource, [userId]); this.plans = new CoreListItemsManager(source, AddonCompetencyPlanListPage); + + this.logView = CoreTime.once(async () => { + const userId = source.USER_ID ?? CoreSites.getCurrentSiteId(); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'tool_lp_data_for_plans_page', + name: Translate.instant('addon.competency.userplans'), + data: { userid: userId }, + url: `/admin/tool/lp/plans.php?userid=${userId}`, + }); + }); } /** @@ -58,6 +76,8 @@ export class AddonCompetencyPlanListPage implements AfterViewInit, OnDestroy { protected async fetchLearningPlans(): Promise { try { await this.plans.load(); + + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error getting learning plans data.'); } diff --git a/src/addons/competency/services/competency.ts b/src/addons/competency/services/competency.ts index 4991f034f..7f83cc0c2 100644 --- a/src/addons/competency/services/competency.ts +++ b/src/addons/competency/services/competency.ts @@ -16,7 +16,6 @@ import { Injectable } from '@angular/core'; import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; import { CoreCommentsArea } from '@features/comments/services/comments'; import { CoreCourseSummary, CoreCourseModuleSummary } from '@features/course/services/course'; -import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; import { CoreUserSummary } from '@features/user/services/user'; import { CoreSites } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; @@ -495,7 +494,7 @@ export class AddonCompetencyProvider { * @param planId ID of the plan. * @param competencyId ID of the competency. * @param planStatus Current plan Status to decide what action should be logged. - * @param name Name of the competency. + * @param name Deprecated, not used anymore. * @param userId User ID. If not defined, current user. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. @@ -525,12 +524,6 @@ export class AddonCompetencyProvider { ? 'core_competency_user_competency_plan_viewed' : 'core_competency_user_competency_viewed_in_plan'; - CorePushNotifications.logViewEvent(competencyId, name, 'competency', wsName, { - planid: planId, - planstatus: planStatus, - userid: userId, - }, siteId); - await site.write(wsName, params, preSets); } @@ -539,7 +532,7 @@ export class AddonCompetencyProvider { * * @param courseId ID of the course. * @param competencyId ID of the competency. - * @param name Name of the competency. + * @param name Deprecated, not used anymore. * @param userId User ID. If not defined, current user. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. @@ -564,14 +557,7 @@ export class AddonCompetencyProvider { typeExpected: 'boolean', }; - const wsName = 'core_competency_user_competency_viewed_in_course'; - - CorePushNotifications.logViewEvent(competencyId, name, 'competency', 'wsName', { - courseid: courseId, - userid: userId, - }, siteId); - - await site.write(wsName, params, preSets); + await site.write('core_competency_user_competency_viewed_in_course', params, preSets); } /** @@ -593,10 +579,7 @@ export class AddonCompetencyProvider { typeExpected: 'boolean', }; - const wsName = 'core_competency_competency_viewed'; - CorePushNotifications.logViewEvent(competencyId, name, 'competency', wsName, {}, siteId); - - await site.write(wsName, params, preSets); + await site.write('core_competency_competency_viewed', params, preSets); } } diff --git a/src/addons/coursecompletion/pages/report/report.ts b/src/addons/coursecompletion/pages/report/report.ts index b95bb88b5..b58921aa3 100644 --- a/src/addons/coursecompletion/pages/report/report.ts +++ b/src/addons/coursecompletion/pages/report/report.ts @@ -19,9 +19,12 @@ import { import { Component, OnInit } from '@angular/core'; import { CoreUser, CoreUserProfile } from '@features/user/services/user'; import { IonRefresher } from '@ionic/angular'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; +import { Translate } from '@singletons'; +import { CoreTime } from '@singletons/time'; /** * Page that displays the course completion report. @@ -33,6 +36,7 @@ import { CoreDomUtils } from '@services/utils/dom'; export class AddonCourseCompletionReportPage implements OnInit { protected userId!: number; + protected logView: () => void; courseId!: number; completionLoaded = false; @@ -42,6 +46,21 @@ export class AddonCourseCompletionReportPage implements OnInit { statusText?: string; user?: CoreUserProfile; + constructor() { + this.logView = CoreTime.once(() => { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'core_completion_get_course_completion_status', + name: Translate.instant('addon.coursecompletion.coursecompletion'), + data: { + course: this.courseId, + user: this.userId, + }, + url: `/blocks/completionstatus/details.php?course=${this.courseId}&user=${this.userId}`, + }); + }); + } + /** * @inheritdoc */ @@ -77,6 +96,7 @@ export class AddonCourseCompletionReportPage implements OnInit { this.showSelfComplete = AddonCourseCompletion.canMarkSelfCompleted(this.userId, this.completion); this.tracked = true; + this.logView(); } catch (error) { if (error && error.errorcode == 'notenroled') { // Not enrolled error, probably a teacher. diff --git a/src/addons/mod/assign/components/index/index.ts b/src/addons/mod/assign/components/index/index.ts index e63a1f1a5..638ba2215 100644 --- a/src/addons/mod/assign/components/index/index.ts +++ b/src/addons/mod/assign/components/index/index.ts @@ -57,7 +57,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo @ViewChild(AddonModAssignSubmissionComponent) submissionComponent?: AddonModAssignSubmissionComponent; component = AddonModAssignProvider.COMPONENT; - moduleName = 'assign'; + pluginName = 'assign'; assign?: AddonModAssignAssign; // The assign object. canViewAllSubmissions = false; // Whether the user can view all submissions. @@ -230,14 +230,20 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo return; // Shouldn't happen. } - await AddonModAssign.logView(this.assign.id, this.assign.name); + await CoreUtils.ignoreErrors(AddonModAssign.logView(this.assign.id)); + + this.analyticsLogEvent('mod_assign_view_assign'); if (this.canViewAllSubmissions) { // User can see all submissions, log grading view. - CoreUtils.ignoreErrors(AddonModAssign.logGradingView(this.assign.id, this.assign.name)); + await CoreUtils.ignoreErrors(AddonModAssign.logGradingView(this.assign.id)); + + this.analyticsLogEvent('mod_assign_view_grading_table', { sendUrl: false }); } else if (this.canViewOwnSubmission) { // User can only see their own submission, log view the user submission. - CoreUtils.ignoreErrors(AddonModAssign.logSubmissionView(this.assign.id, this.assign.name)); + await CoreUtils.ignoreErrors(AddonModAssign.logSubmissionView(this.assign.id)); + + this.analyticsLogEvent('mod_assign_view_submission_status', { sendUrl: false }); } } diff --git a/src/addons/mod/assign/lang.json b/src/addons/mod/assign/lang.json index de0b0cc9d..116924c0b 100644 --- a/src/addons/mod/assign/lang.json +++ b/src/addons/mod/assign/lang.json @@ -45,6 +45,7 @@ "gradelocked": "This grade is locked or overridden in the gradebook.", "gradenotsynced": "Grade not synced", "gradeoutof": "Grade out of {{$a}}", + "grading": "Grading", "gradingstatus": "Grading status", "groupsubmissionsettings": "Group submission settings", "hiddenuser": "Participant", @@ -101,6 +102,7 @@ "submittedlate": "Assignment was submitted {{$a}} late", "submittedovertime": "Assignment was submitted {{$a}} over the time limit", "submittedundertime": "Assignment was submitted {{$a}} under the time limit", + "subpagetitle": "{{$a.contextname}} - {{$a.subpage}}", "syncblockedusercomponent": "user grade", "timelimit": "Time limit", "timemodified": "Last modified", diff --git a/src/addons/mod/assign/pages/edit/edit.ts b/src/addons/mod/assign/pages/edit/edit.ts index 634d79773..b7677e451 100644 --- a/src/addons/mod/assign/pages/edit/edit.ts +++ b/src/addons/mod/assign/pages/edit/edit.ts @@ -39,6 +39,7 @@ import { AddonModAssignOffline } from '../../services/assign-offline'; import { AddonModAssignSync } from '../../services/assign-sync'; import { CoreUtils } from '@services/utils/utils'; import { CoreWSExternalFile } from '@services/ws'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that allows adding or editing an assigment submission. @@ -226,6 +227,17 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave { // No offline data found. this.hasOffline = false; } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_assign_save_submission', + name: Translate.instant('addon.mod_assign.subpagetitle', { + contextname: this.assign.name, + subpage: Translate.instant('addon.mod_assign.editsubmission'), + }), + data: { id: this.assign.id, category: 'assign' }, + url: `/mod/assign/view.php?action=editsubmission&id=${this.moduleId}`, + }); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error getting assigment data.'); diff --git a/src/addons/mod/assign/pages/submission-list/submission-list.ts b/src/addons/mod/assign/pages/submission-list/submission-list.ts index 1670f1dec..d0a59e2e0 100644 --- a/src/addons/mod/assign/pages/submission-list/submission-list.ts +++ b/src/addons/mod/assign/pages/submission-list/submission-list.ts @@ -34,6 +34,7 @@ import { AddonModAssignManualSyncData, AddonModAssignAutoSyncData, } from '../../services/assign-sync'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays a list of submissions of an assignment. @@ -168,6 +169,21 @@ export class AddonModAssignSubmissionListPage implements AfterViewInit, OnDestro protected async fetchAssignment(sync = false): Promise { try { await this.submissions.getSource().loadAssignment(sync); + + if (!this.assign) { + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'mod_assign_get_submissions', + name: Translate.instant('addon.mod_assign.subpagetitle', { + contextname: this.assign.name, + subpage: Translate.instant('addon.mod_assign.grading'), + }), + data: { assignid: this.assign.id, category: 'assign' }, + url: `/mod/assign/view.php?id=${this.assign.cmid}&action=grading`, + }); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error getting assigment data.'); } diff --git a/src/addons/mod/assign/pages/submission-review/submission-review.ts b/src/addons/mod/assign/pages/submission-review/submission-review.ts index 847d3502d..6749d9f1b 100644 --- a/src/addons/mod/assign/pages/submission-review/submission-review.ts +++ b/src/addons/mod/assign/pages/submission-review/submission-review.ts @@ -25,6 +25,9 @@ import { CoreDomUtils } from '@services/utils/dom'; import { AddonModAssignListFilterName, AddonModAssignSubmissionsSource } from '../../classes/submissions-source'; import { AddonModAssignSubmissionComponent } from '../../components/submission/submission'; import { AddonModAssign, AddonModAssignAssign } from '../../services/assign'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { Translate } from '@singletons'; /** * Page that displays a submission. @@ -49,8 +52,29 @@ export class AddonModAssignSubmissionReviewPage implements OnInit, OnDestroy, Ca protected assign?: AddonModAssignAssign; // The assignment the submission belongs to. protected blindMarking = false; // Whether it uses blind marking. protected forceLeave = false; // To allow leaving the page without checking for changes. + protected logView: () => void; - constructor(protected route: ActivatedRoute) { } + constructor(protected route: ActivatedRoute) { + this.logView = CoreTime.once(() => { + if (!this.assign) { + return; + } + + const id = this.blindMarking ? this.blindId : this.submitId; + const paramName = this.blindMarking ? 'blindid' : 'userid'; + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_assign_get_submission_status', + name: Translate.instant('addon.mod_assign.subpagetitle', { + contextname: this.assign.name, + subpage: Translate.instant('addon.mod_assign.grading'), + }), + data: { id, assignid: this.assign.id, category: 'assign' }, + url: `/mod/assign/view.php?id=${this.assign.cmid}&action=grader&${paramName}=${id}`, + }); + }); + } /** * @inheritdoc @@ -84,6 +108,7 @@ export class AddonModAssignSubmissionReviewPage implements OnInit, OnDestroy, Ca } this.fetchSubmission().finally(() => { + this.logView(); this.loaded = true; }); }); diff --git a/src/addons/mod/assign/services/assign.ts b/src/addons/mod/assign/services/assign.ts index c213cb8eb..ce34faf77 100644 --- a/src/addons/mod/assign/services/assign.ts +++ b/src/addons/mod/assign/services/assign.ts @@ -878,23 +878,19 @@ export class AddonModAssignProvider { * Report an assignment submission as being viewed. * * @param assignid Assignment ID. - * @param name Name of the assign. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - async logSubmissionView(assignid: number, name?: string, siteId?: string): Promise { + async logSubmissionView(assignid: number, siteId?: string): Promise { const params: AddonModAssignViewSubmissionStatusWSParams = { assignid, }; - await CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.log( 'mod_assign_view_submission_status', params, AddonModAssignProvider.COMPONENT, assignid, - name, - 'assign', - {}, siteId, ); } @@ -903,23 +899,19 @@ export class AddonModAssignProvider { * Report an assignment grading table is being viewed. * * @param assignid Assignment ID. - * @param name Name of the assign. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - async logGradingView(assignid: number, name?: string, siteId?: string): Promise { + async logGradingView(assignid: number, siteId?: string): Promise { const params: AddonModAssignViewGradingTableWSParams = { assignid, }; - await CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.log( 'mod_assign_view_grading_table', params, AddonModAssignProvider.COMPONENT, assignid, - name, - 'assign', - {}, siteId, ); } @@ -928,23 +920,19 @@ export class AddonModAssignProvider { * Report an assign as being viewed. * * @param assignid Assignment ID. - * @param name Name of the assign. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - async logView(assignid: number, name?: string, siteId?: string): Promise { + async logView(assignid: number, siteId?: string): Promise { const params: AddonModAssignViewAssignWSParams = { assignid, }; - await CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.log( 'mod_assign_view_assign', params, AddonModAssignProvider.COMPONENT, assignid, - name, - 'assign', - {}, siteId, ); } diff --git a/src/addons/mod/bigbluebuttonbn/components/index/index.ts b/src/addons/mod/bigbluebuttonbn/components/index/index.ts index 72ad86990..92914b8ae 100644 --- a/src/addons/mod/bigbluebuttonbn/components/index/index.ts +++ b/src/addons/mod/bigbluebuttonbn/components/index/index.ts @@ -44,7 +44,7 @@ import { export class AddonModBBBIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit { component = AddonModBBBService.COMPONENT; - moduleName = 'bigbluebuttonbn'; + pluginName = 'bigbluebuttonbn'; bbb?: AddonModBBBData; groupInfo?: CoreGroupInfo; groupId = 0; @@ -226,7 +226,9 @@ export class AddonModBBBIndexComponent extends CoreCourseModuleMainActivityCompo return; // Shouldn't happen. } - await AddonModBBB.logView(this.bbb.id, this.bbb.name); + await CoreUtils.ignoreErrors(AddonModBBB.logView(this.bbb.id)); + + this.analyticsLogEvent('mod_bigbluebuttonbn_view_bigbluebuttonbn'); } /** diff --git a/src/addons/mod/bigbluebuttonbn/services/bigbluebuttonbn.ts b/src/addons/mod/bigbluebuttonbn/services/bigbluebuttonbn.ts index a59218853..dd1e40225 100644 --- a/src/addons/mod/bigbluebuttonbn/services/bigbluebuttonbn.ts +++ b/src/addons/mod/bigbluebuttonbn/services/bigbluebuttonbn.ts @@ -276,23 +276,19 @@ export class AddonModBBBService { * Report a BBB as being viewed. * * @param id BBB instance ID. - * @param name Name of the BBB. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - async logView(id: number, name?: string, siteId?: string): Promise { + async logView(id: number, siteId?: string): Promise { const params: AddonModBBBViewBigBlueButtonBNWSParams = { bigbluebuttonbnid: id, }; - await CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.log( 'mod_bigbluebuttonbn_view_bigbluebuttonbn', params, AddonModBBBService.COMPONENT, id, - name, - 'bigbluebuttonbn', - {}, siteId, ); } diff --git a/src/addons/mod/book/components/index/index.ts b/src/addons/mod/book/components/index/index.ts index 93c64972f..aa77f2d33 100644 --- a/src/addons/mod/book/components/index/index.ts +++ b/src/addons/mod/book/components/index/index.ts @@ -19,6 +19,7 @@ import { CoreCourseContentsPage } from '@features/course/pages/contents/contents import { CoreCourse } from '@features/course/services/course'; import { CoreNavigator } from '@services/navigator'; import { AddonModBookModuleHandlerService } from '../../services/handlers/module'; +import { CoreUtils } from '@services/utils/utils'; /** * Component that displays a book entry page. @@ -29,6 +30,7 @@ import { AddonModBookModuleHandlerService } from '../../services/handlers/module }) export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy { + pluginName = 'book'; showNumbers = true; addPadding = true; showBullets = false; @@ -102,7 +104,9 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp * @inheritdoc */ protected async logActivity(): Promise { - AddonModBook.logView(this.module.instance, undefined, this.module.name); + await CoreUtils.ignoreErrors(AddonModBook.logView(this.module.instance)); + + this.analyticsLogEvent('mod_book_view_book'); } /** diff --git a/src/addons/mod/book/pages/contents/contents.ts b/src/addons/mod/book/pages/contents/contents.ts index 2bd8a6cbe..3d22145cf 100644 --- a/src/addons/mod/book/pages/contents/contents.ts +++ b/src/addons/mod/book/pages/contents/contents.ts @@ -40,6 +40,8 @@ import { AddonModBookProvider, AddonModBookTocChapter, } from '../../services/book'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreUrlUtils } from '@services/utils/url'; /** * Page that displays a book contents. @@ -286,7 +288,15 @@ export class AddonModBookContentsPage implements OnInit, OnDestroy { } // Chapter loaded, log view. - await CoreUtils.ignoreErrors(AddonModBook.logView(this.module.instance, chapterId, this.module.name)); + await CoreUtils.ignoreErrors(AddonModBook.logView(this.module.instance, chapterId)); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_book_view_book', + name: this.module.name, + data: { id: this.module.instance, category: 'book', chapterid: chapterId }, + url: CoreUrlUtils.addParamsToUrl(`/mod/book/view.php?id=${this.module.id}`, { chapterid: chapterId }), + }); const currentChapterIndex = this.chapters.findIndex((chapter) => chapter.id == chapterId); const isLastChapter = currentChapterIndex < 0 || this.chapters[currentChapterIndex + 1] === undefined; diff --git a/src/addons/mod/book/services/book.ts b/src/addons/mod/book/services/book.ts index 2365c5291..19a86c643 100644 --- a/src/addons/mod/book/services/book.ts +++ b/src/addons/mod/book/services/book.ts @@ -359,24 +359,20 @@ export class AddonModBookProvider { * * @param id Module ID. * @param chapterId Chapter ID. - * @param name Name of the book. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - async logView(id: number, chapterId?: number, name?: string, siteId?: string): Promise { + async logView(id: number, chapterId?: number, siteId?: string): Promise { const params: AddonModBookViewBookWSParams = { bookid: id, chapterid: chapterId, }; - await CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.log( 'mod_book_view_book', params, AddonModBookProvider.COMPONENT, id, - name, - 'book', - { chapterid: chapterId }, siteId, ); } diff --git a/src/addons/mod/chat/components/index/index.ts b/src/addons/mod/chat/components/index/index.ts index 7315bdf70..b298143c0 100644 --- a/src/addons/mod/chat/components/index/index.ts +++ b/src/addons/mod/chat/components/index/index.ts @@ -32,7 +32,7 @@ import { AddonModChatModuleHandlerService } from '../../services/handlers/module export class AddonModChatIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit { component = AddonModChatProvider.COMPONENT; - moduleName = 'chat'; + pluginName = 'chat'; chat?: AddonModChatChat; chatInfo?: { date: string; @@ -85,7 +85,9 @@ export class AddonModChatIndexComponent extends CoreCourseModuleMainActivityComp return; // Shouldn't happen. } - await AddonModChat.logView(this.chat.id, this.chat.name); + await AddonModChat.logView(this.chat.id); + + this.analyticsLogEvent('mod_chat_view_chat'); } /** diff --git a/src/addons/mod/chat/pages/chat/chat.ts b/src/addons/mod/chat/pages/chat/chat.ts index 8c13924cf..e1c168c10 100644 --- a/src/addons/mod/chat/pages/chat/chat.ts +++ b/src/addons/mod/chat/pages/chat/chat.ts @@ -28,6 +28,8 @@ import { Subscription } from 'rxjs'; import { AddonModChatUsersModalComponent, AddonModChatUsersModalResult } from '../../components/users-modal/users-modal'; import { AddonModChat, AddonModChatProvider, AddonModChatUser } from '../../services/chat'; import { AddonModChatFormattedMessage, AddonModChatHelper } from '../../services/chat-helper'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays a chat session. @@ -61,6 +63,7 @@ export class AddonModChatChatPage implements OnInit, OnDestroy, CanLeave { protected viewDestroyed = false; protected pollingRunning = false; protected users: AddonModChatUser[] = []; + protected logView: () => void; constructor() { this.currentUserId = CoreSites.getCurrentSiteUserId(); @@ -71,6 +74,16 @@ export class AddonModChatChatPage implements OnInit, OnDestroy, CanLeave { this.isOnline = CoreNetwork.isOnline(); }); }); + + this.logView = CoreTime.once(() => { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'mod_chat_get_chat_latest_messages', + name: this.title, + data: { chatid: this.chatId, category: 'chat' }, + url: `/mod/chat/gui_ajax/index.php?id=${this.chatId}`, + }); + }); } /** @@ -88,6 +101,7 @@ export class AddonModChatChatPage implements OnInit, OnDestroy, CanLeave { await this.fetchMessages(); this.startPolling(); + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.mod_chat.errorwhileconnecting', true); CoreNavigator.back(); diff --git a/src/addons/mod/chat/pages/session-messages/session-messages.ts b/src/addons/mod/chat/pages/session-messages/session-messages.ts index c59e8af91..0e98536de 100644 --- a/src/addons/mod/chat/pages/session-messages/session-messages.ts +++ b/src/addons/mod/chat/pages/session-messages/session-messages.ts @@ -21,6 +21,9 @@ import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; import { AddonModChat } from '../../services/chat'; import { AddonModChatFormattedSessionMessage, AddonModChatHelper } from '../../services/chat-helper'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { Translate } from '@singletons'; +import { CoreTime } from '@singletons/time'; /** * Page that displays list of chat session messages. @@ -42,6 +45,19 @@ export class AddonModChatSessionMessagesPage implements OnInit { protected sessionStart!: number; protected sessionEnd!: number; protected groupId!: number; + protected logView: () => void; + + constructor() { + this.logView = CoreTime.once(() => { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'mod_chat_view_sessions', + name: Translate.instant('addon.mod_chat.messages'), + data: { chatid: this.chatId, category: 'chat' }, + url: `/mod/chat/report.php?id=${this.cmId}&start=${this.sessionStart}&end=${this.sessionEnd}`, + }); + }); + } /** * @inheritdoc diff --git a/src/addons/mod/chat/pages/sessions/sessions.ts b/src/addons/mod/chat/pages/sessions/sessions.ts index 9f78793fd..32f91b9fa 100644 --- a/src/addons/mod/chat/pages/sessions/sessions.ts +++ b/src/addons/mod/chat/pages/sessions/sessions.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core'; +import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { CoreListItemsManager } from '@classes/items-management/list-items-manager'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; @@ -21,6 +21,9 @@ import { CoreGroupInfo } from '@services/groups'; import { CoreNavigator } from '@services/navigator'; import { CoreDomUtils } from '@services/utils/dom'; import { AddonModChatSessionFormatted, AddonModChatSessionsSource } from '../../classes/chat-sessions-source'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreTime } from '@singletons/time'; +import { Translate } from '@singletons'; /** * Page that displays list of chat sessions. @@ -29,14 +32,32 @@ import { AddonModChatSessionFormatted, AddonModChatSessionsSource } from '../../ selector: 'page-addon-mod-chat-sessions', templateUrl: 'sessions.html', }) -export class AddonModChatSessionsPage implements AfterViewInit, OnDestroy { +export class AddonModChatSessionsPage implements OnInit, AfterViewInit, OnDestroy { @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; sessions!: CoreListItemsManager; courseId?: number; + protected logView: () => void; constructor() { + this.logView = CoreTime.once(() => { + const source = this.sessions.getSource(); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'mod_chat_view_sessions', + name: Translate.instant('addon.mod_chat.chatreport'), + data: { chatid: source.CHAT_ID, category: 'chat' }, + url: `/mod/chat/report.php?id=${source.CM_ID}`, + }); + }); + } + + /** + * @inheritdoc + */ + ngOnInit(): void { try { this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); const chatId = CoreNavigator.getRequiredRouteNumberParam('chatId'); @@ -91,6 +112,8 @@ export class AddonModChatSessionsPage implements AfterViewInit, OnDestroy { async fetchSessions(): Promise { try { await this.sessions.load(); + + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'core.errorloadingcontent', true); } diff --git a/src/addons/mod/chat/services/chat.ts b/src/addons/mod/chat/services/chat.ts index 4b508958f..2c631423a 100644 --- a/src/addons/mod/chat/services/chat.ts +++ b/src/addons/mod/chat/services/chat.ts @@ -87,23 +87,19 @@ export class AddonModChatProvider { * Report a chat as being viewed. * * @param id Chat instance ID. - * @param name Name of the chat. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - async logView(id: number, name?: string, siteId?: string): Promise { + async logView(id: number, siteId?: string): Promise { const params: AddonModChatViewChatWSParams = { chatid: id, }; - await CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.log( 'mod_chat_view_chat', params, AddonModChatProvider.COMPONENT, id, - name, - 'chat', - {}, siteId, ); } diff --git a/src/addons/mod/choice/components/index/index.ts b/src/addons/mod/choice/components/index/index.ts index d4f7e039c..e119299fa 100644 --- a/src/addons/mod/choice/components/index/index.ts +++ b/src/addons/mod/choice/components/index/index.ts @@ -48,7 +48,7 @@ import { AddonModChoicePrefetchHandler } from '../../services/handlers/prefetch' export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit { component = AddonModChoiceProvider.COMPONENT; - moduleName = 'choice'; + pluginName = 'choice'; choice?: AddonModChoiceChoice; options: AddonModChoiceOption[] = []; @@ -321,7 +321,9 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo return; // Shouldn't happen. } - await AddonModChoice.logView(this.choice.id, this.choice.name); + await AddonModChoice.logView(this.choice.id); + + this.analyticsLogEvent('mod_choice_view_choice'); } /** @@ -386,6 +388,8 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo this.checkCompletion(); } + this.analyticsLogEvent('mod_choice_view_choice', { data: { notify: 'choicesaved' } }); + await this.dataUpdated(online); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.mod_choice.cannotsubmit', true); @@ -412,6 +416,8 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo this.content?.scrollToTop(); + this.analyticsLogEvent('mod_choice_view_choice', { data: { action: 'delchoice' } }); + // Refresh the data. Don't call dataUpdated because deleting an answer doesn't mark the choice as outdated. await this.refreshContent(false); } catch (error) { diff --git a/src/addons/mod/choice/services/choice.ts b/src/addons/mod/choice/services/choice.ts index 57e002fea..055b7affb 100644 --- a/src/addons/mod/choice/services/choice.ts +++ b/src/addons/mod/choice/services/choice.ts @@ -365,23 +365,19 @@ export class AddonModChoiceProvider { * Report the choice as being viewed. * * @param id Choice ID. - * @param name Name of the choice. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - logView(id: number, name?: string, siteId?: string): Promise { + logView(id: number, siteId?: string): Promise { const params: AddonModChoiceViewChoiceWSParams = { choiceid: id, }; - return CoreCourseLogHelper.logSingle( + return CoreCourseLogHelper.log( 'mod_choice_view_choice', params, AddonModChoiceProvider.COMPONENT, id, - name, - 'choice', - {}, siteId, ); } diff --git a/src/addons/mod/data/components/index/index.ts b/src/addons/mod/data/components/index/index.ts index 64e0ca22e..3b6c939d6 100644 --- a/src/addons/mod/data/components/index/index.ts +++ b/src/addons/mod/data/components/index/index.ts @@ -45,6 +45,8 @@ import { AddonModDataModuleHandlerService } from '../../services/handlers/module import { AddonModDataPrefetchHandler } from '../../services/handlers/prefetch'; import { AddonModDataComponentsCompileModule } from '../components-compile.module'; import { AddonModDataSearchComponent } from '../search/search'; +import { CoreUrlUtils } from '@services/utils/url'; +import { CoreTime } from '@singletons/time'; const contentToken = ''; @@ -59,7 +61,7 @@ const contentToken = ''; export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit, OnDestroy { component = AddonModDataProvider.COMPONENT; - moduleName = 'data'; + pluginName = 'data'; access?: AddonModDataGetDataAccessInformationWSResponse; database?: AddonModDataData; @@ -114,6 +116,7 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp protected entryChangedObserver?: CoreEventObserver; protected ratingOfflineObserver?: CoreEventObserver; protected ratingSyncObserver?: CoreEventObserver; + protected logSearch?: () => void; constructor( protected content?: IonContent, @@ -404,6 +407,7 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp // Add data to search object. if (modalData) { this.search = modalData; + this.logSearch = CoreTime.once(() => this.performLogSearch()); this.searchEntries(0); } } @@ -420,8 +424,8 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp try { await this.fetchEntriesData(); - // Log activity view for coherence with Moodle web. - await this.logActivity(); + + this.logSearch?.(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true); } finally { @@ -470,9 +474,6 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp try { await this.fetchEntriesData(); - - // Log activity view for coherence with Moodle web. - return this.logActivity(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true); } @@ -535,7 +536,34 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp return; } - await AddonModData.logView(this.database.id, this.database.name); + await AddonModData.logView(this.database.id); + + this.analyticsLogEvent('mod_data_view_database'); + } + + /** + * Log search. + */ + protected async performLogSearch(): Promise { + if (!this.database || !this.search.searching) { + return; + } + + const params: Record = { + perpage: AddonModDataProvider.PER_PAGE, + search: !this.search.searchingAdvanced ? this.search.text : '', + sort: this.search.sortBy, + order: this.search.sortDirection, + advanced: this.search.searchingAdvanced ? 1 : 0, + filter: 1, + }; + + // @todo: Add advanced search parameters. Leave them empty if not using advanced search. + + this.analyticsLogEvent('mod_data_search_entries', { + data: params, + url: CoreUrlUtils.addParamsToUrl(`/mod/data/view.php?d=${this.database.id}`, params), + }); } /** diff --git a/src/addons/mod/data/pages/edit/edit.ts b/src/addons/mod/data/pages/edit/edit.ts index e6452eab5..4946a6800 100644 --- a/src/addons/mod/data/pages/edit/edit.ts +++ b/src/addons/mod/data/pages/edit/edit.ts @@ -43,6 +43,8 @@ import { AddonModDataHelper } from '../../services/data-helper'; import { CoreDom } from '@singletons/dom'; import { AddonModDataEntryFieldInitialized } from '../../classes/base-field-plugin-component'; import { CoreTextUtils } from '@services/utils/text'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays the view edit page. @@ -65,6 +67,7 @@ export class AddonModDataEditPage implements OnInit { protected initialSelectedGroup?: number; protected isEditing = false; protected originalData: AddonModDataEntryFields = {}; + protected logView: () => void; entry?: AddonModDataEntry; fields: Record = {}; @@ -94,6 +97,20 @@ export class AddonModDataEditPage implements OnInit { constructor() { this.siteId = CoreSites.getCurrentSiteId(); this.editForm = new FormGroup({}); + + this.logView = CoreTime.once(() => { + if (!this.database) { + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: this.isEditing ? 'mod_data_update_entry' : 'mod_data_add_entry', + name: this.title, + data: { databaseid: this.database.id, category: 'data' }, + url: '/mod/data/edit.php?' + (this.isEditing ? `d=${this.database.id}&rid=${this.entryId}` : `id=${this.moduleId}`), + }); + }); } /** @@ -230,6 +247,7 @@ export class AddonModDataEditPage implements OnInit { } this.editFormRender = this.displayEditFields(); + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true); } diff --git a/src/addons/mod/data/pages/entry/entry.ts b/src/addons/mod/data/pages/entry/entry.ts index 681ffbda0..0fff2c131 100644 --- a/src/addons/mod/data/pages/entry/entry.ts +++ b/src/addons/mod/data/pages/entry/entry.ts @@ -36,6 +36,8 @@ import { AddonModDataProvider, } from '../../services/data'; import { AddonModDataHelper } from '../../services/data-helper'; import { AddonModDataSyncProvider } from '../../services/data-sync'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreTime } from '@singletons/time'; /** * Page that displays the view entry page. @@ -55,9 +57,9 @@ export class AddonModDataEntryPage implements OnInit, OnDestroy { protected entryChangedObserver: CoreEventObserver; // It will observe the changed entry event. protected fields: Record = {}; protected fieldsArray: AddonModDataField[] = []; - protected logAfterFetch = true; protected sortBy = 0; protected sortDirection = 'DESC'; + protected logView: () => void; moduleId = 0; courseId!: number; @@ -129,6 +131,8 @@ export class AddonModDataEntryPage implements OnInit, OnDestroy { } } }, this.siteId); + + this.logView = CoreTime.once(() => this.performLogView()); } /** @@ -219,13 +223,7 @@ export class AddonModDataEntryPage implements OnInit, OnDestroy { access: this.access, }; - if (this.logAfterFetch) { - this.logAfterFetch = false; - await CoreUtils.ignoreErrors(AddonModData.logView(this.database.id, this.database.name)); - - // Store module viewed because this page also updates recent accessed items block. - CoreCourse.storeModuleViewed(this.courseId, this.moduleId); - } + this.logView(); } catch (error) { if (!refresh) { // Some call failed, retry without using cache since it might be a new activity. @@ -250,7 +248,7 @@ export class AddonModDataEntryPage implements OnInit, OnDestroy { this.entryId = undefined; this.entry = undefined; this.entryLoaded = false; - this.logAfterFetch = true; + this.logView = CoreTime.once(() => this.performLogView()); // Log again after loading data. await this.fetchEntryData(); } @@ -310,7 +308,6 @@ export class AddonModDataEntryPage implements OnInit, OnDestroy { this.entry = undefined; this.entryId = undefined; this.entryLoaded = false; - this.logAfterFetch = true; await this.fetchEntryData(); } @@ -422,6 +419,28 @@ export class AddonModDataEntryPage implements OnInit, OnDestroy { AddonModData.invalidateEntryData(this.database!.id, this.entryId!); } + /** + * Log view. + */ + protected async performLogView(): Promise { + if (!this.database) { + return; + } + + await CoreUtils.ignoreErrors(AddonModData.logView(this.database.id)); + + // Store module viewed because this page also updates recent accessed items block. + CoreCourse.storeModuleViewed(this.courseId, this.moduleId); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_data_view_database', + name: this.database.name, + data: { id: this.entryId, databaseid: this.database.id, category: 'data' }, + url: `/mod/data/view.php?d=${this.database.id}&rid=${this.entryId}`, + }); + } + /** * @inheritdoc */ diff --git a/src/addons/mod/data/services/data.ts b/src/addons/mod/data/services/data.ts index 0ff632a17..a433b1bc0 100644 --- a/src/addons/mod/data/services/data.ts +++ b/src/addons/mod/data/services/data.ts @@ -955,23 +955,19 @@ export class AddonModDataProvider { * Report the database as being viewed. * * @param id Module ID. - * @param name Name of the data. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - async logView(id: number, name?: string, siteId?: string): Promise { + async logView(id: number, siteId?: string): Promise { const params: AddonModDataViewDatabaseWSParams = { databaseid: id, }; - await CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.log( 'mod_data_view_database', params, AddonModDataProvider.COMPONENT, id, - name, - 'data', - {}, siteId, ); } diff --git a/src/addons/mod/feedback/classes/feedback-attempts-source.ts b/src/addons/mod/feedback/classes/feedback-attempts-source.ts index fe3c6f74d..e08133127 100644 --- a/src/addons/mod/feedback/classes/feedback-attempts-source.ts +++ b/src/addons/mod/feedback/classes/feedback-attempts-source.ts @@ -37,8 +37,7 @@ export class AddonModFeedbackAttemptsSource extends CoreRoutedItemsManagerSource anonymous?: AddonModFeedbackWSAnonAttempt[]; anonymousTotal?: number; groupInfo?: CoreGroupInfo; - - protected feedback?: AddonModFeedbackWSFeedback; + feedback?: AddonModFeedbackWSFeedback; constructor(courseId: number, cmId: number) { super(); diff --git a/src/addons/mod/feedback/components/index/index.ts b/src/addons/mod/feedback/components/index/index.ts index e9e4defce..7980f3640 100644 --- a/src/addons/mod/feedback/components/index/index.ts +++ b/src/addons/mod/feedback/components/index/index.ts @@ -56,7 +56,7 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity @Input() group = 0; component = AddonModFeedbackProvider.COMPONENT; - moduleName = 'feedback'; + pluginName = 'feedback'; feedback?: AddonModFeedbackWSFeedback; goPage?: number; items: AddonModFeedbackItem[] = []; @@ -140,7 +140,18 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity return; // Shouldn't happen. } - await AddonModFeedback.logView(this.feedback.id, this.feedback.name); + await AddonModFeedback.logView(this.feedback.id); + + this.callAnalyticsLogEvent(); + } + + /** + * Call analytics. + */ + protected callAnalyticsLogEvent(): void { + this.analyticsLogEvent('mod_feedback_view_feedback', { + url: this.tab === 'analysis' ? `/mod/feedback/analysis.php?id=${this.module.id}` : undefined, + }); } /** @@ -429,11 +440,16 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity * @param tabName New tab name. */ tabChanged(tabName: string): void { + const tabHasChanged = this.tab !== undefined && this.tab !== tabName; this.tab = tabName; if (!this.tabsLoaded[this.tab]) { this.loadContent(false, false, true); } + + if (tabHasChanged) { + this.callAnalyticsLogEvent(); + } } /** diff --git a/src/addons/mod/feedback/pages/attempt/attempt.ts b/src/addons/mod/feedback/pages/attempt/attempt.ts index e2db40423..b80e42b79 100644 --- a/src/addons/mod/feedback/pages/attempt/attempt.ts +++ b/src/addons/mod/feedback/pages/attempt/attempt.ts @@ -27,6 +27,8 @@ import { AddonModFeedbackWSFeedback, } from '../../services/feedback'; import { AddonModFeedbackAttempt, AddonModFeedbackFormItem, AddonModFeedbackHelper } from '../../services/feedback-helper'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays a feedback attempt review. @@ -48,6 +50,7 @@ export class AddonModFeedbackAttemptPage implements OnInit, OnDestroy { loaded = false; protected attemptId: number; + protected logView: () => void; constructor() { this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId'); @@ -60,6 +63,21 @@ export class AddonModFeedbackAttemptPage implements OnInit, OnDestroy { ); this.attempts = new AddonModFeedbackAttemptsSwipeManager(source); + + this.logView = CoreTime.once(() => { + if (!this.feedback) { + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_feedback_get_responses_analysis', + name: this.feedback.name, + data: { id: this.attemptId, feedbackid: this.feedback.id, category: 'feedback' }, + url: `/mod/feedback/show_entries.php?id=${this.cmId}` + + (this.attempt ? `userid=${this.attempt.userid}` : '' ) + `&showcompleted=${this.attemptId}`, + }); + }); } /** @@ -129,6 +147,8 @@ export class AddonModFeedbackAttemptPage implements OnInit, OnDestroy { return attemptItem; }).filter((itemData) => itemData); // Filter items with errors. + + this.logView(); } catch (message) { // Some call failed on fetch, go back. CoreDomUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true); diff --git a/src/addons/mod/feedback/pages/attempts/attempts.ts b/src/addons/mod/feedback/pages/attempts/attempts.ts index 3799bb66e..817fd2500 100644 --- a/src/addons/mod/feedback/pages/attempts/attempts.ts +++ b/src/addons/mod/feedback/pages/attempts/attempts.ts @@ -25,6 +25,8 @@ import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; import { AddonModFeedbackAttemptItem, AddonModFeedbackAttemptsSource } from '../../classes/feedback-attempts-source'; import { AddonModFeedbackWSAnonAttempt, AddonModFeedbackWSAttempt } from '../../services/feedback'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays feedback attempts. @@ -41,8 +43,25 @@ export class AddonModFeedbackAttemptsPage implements AfterViewInit, OnDestroy { fetchFailed = false; courseId?: number; + protected logView: () => void; + constructor(protected route: ActivatedRoute) { this.promisedAttempts = new CorePromisedValue(); + + this.logView = CoreTime.once(() => { + const source = this.attempts?.getSource(); + if (!source || !source.feedback) { + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'mod_feedback_get_responses_analysis', + name: source.feedback.name, + data: { feedbackid: source.feedback.id, category: 'feedback' }, + url: `/mod/feedback/show_entries.php?id=${source.CM_ID}`, + }); + }); } get attempts(): AddonModFeedbackAttemptsManager | null { @@ -112,6 +131,8 @@ export class AddonModFeedbackAttemptsPage implements AfterViewInit, OnDestroy { await attempts.getSource().loadFeedback(); await attempts.load(); + + this.logView(); } catch (error) { this.fetchFailed = true; diff --git a/src/addons/mod/feedback/pages/form/form.ts b/src/addons/mod/feedback/pages/form/form.ts index ad96737b0..0c45198a1 100644 --- a/src/addons/mod/feedback/pages/form/form.ts +++ b/src/addons/mod/feedback/pages/form/form.ts @@ -38,6 +38,7 @@ import { import { AddonModFeedbackFormItem, AddonModFeedbackHelper } from '../../services/feedback-helper'; import { AddonModFeedbackSync } from '../../services/feedback-sync'; import { AddonModFeedbackModuleHandlerService } from '../../services/handlers/module'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays feedback form. @@ -122,7 +123,7 @@ export class AddonModFeedbackFormPage implements OnInit, OnDestroy, CanLeave { } try { - await AddonModFeedback.logView(this.feedback.id, this.feedback.name, true); + await AddonModFeedback.logView(this.feedback.id, true); CoreCourse.checkModuleCompletion(this.courseId, this.module!.completiondata); } catch { @@ -263,6 +264,8 @@ export class AddonModFeedbackFormPage implements OnInit, OnDestroy, CanLeave { const itemsCopy = CoreUtils.clone(this.items); // Copy the array to avoid modifications. this.originalData = AddonModFeedbackHelper.getPageItemsResponses(itemsCopy); } + + this.analyticsLogEvent(); } /** @@ -435,6 +438,40 @@ export class AddonModFeedbackFormPage implements OnInit, OnDestroy, CanLeave { } } + /** + * Log event in analytics. + */ + protected analyticsLogEvent(): void { + if (!this.feedback) { + return; + } + + if (this.preview) { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_feedback_get_items', + name: this.feedback.name, + data: { id: this.feedback.id, category: 'feedback' }, + url: `/mod/feedback/print.php?id=${this.cmId}&courseid=${this.courseId}`, + }); + + return; + } + + let url = '/mod/feedback/complete.php'; + if (!this.completed) { + url += `?id=${this.cmId}` + (this.currentPage ? `&gopage=${this.currentPage}` : '') + `&courseid=${this.courseId}`; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: this.completed ? 'mod_feedback_get_feedback_access_information' : 'mod_feedback_get_page_items', + name: this.feedback.name, + data: { id: this.feedback.id, category: 'feedback', page: this.currentPage }, + url, + }); + } + /** * @inheritdoc */ diff --git a/src/addons/mod/feedback/pages/nonrespondents/nonrespondents.ts b/src/addons/mod/feedback/pages/nonrespondents/nonrespondents.ts index 6917682ba..2046106ff 100644 --- a/src/addons/mod/feedback/pages/nonrespondents/nonrespondents.ts +++ b/src/addons/mod/feedback/pages/nonrespondents/nonrespondents.ts @@ -20,6 +20,8 @@ import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; import { AddonModFeedback, AddonModFeedbackWSFeedback } from '../../services/feedback'; import { AddonModFeedbackHelper, AddonModFeedbackNonRespondent } from '../../services/feedback-helper'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays feedback non respondents. @@ -33,6 +35,7 @@ export class AddonModFeedbackNonRespondentsPage implements OnInit { protected cmId!: number; protected feedback?: AddonModFeedbackWSFeedback; protected page = 0; + protected logView: () => void; courseId!: number; selectedGroup!: number; @@ -43,6 +46,22 @@ export class AddonModFeedbackNonRespondentsPage implements OnInit { loaded = false; loadMoreError = false; + constructor() { + this.logView = CoreTime.once(() => { + if (!this.feedback) { + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'mod_feedback_get_non_respondents', + name: this.feedback.name, + data: { feedbackid: this.feedback.id, category: 'feedback' }, + url: `/mod/feedback/show_nonrespondents.php?id=${this.cmId}&courseid=${this.courseId}`, + }); + }); + } + /** * @inheritdoc */ @@ -81,6 +100,8 @@ export class AddonModFeedbackNonRespondentsPage implements OnInit { this.selectedGroup = CoreGroups.validateGroupId(this.selectedGroup, this.groupInfo); await this.loadGroupUsers(this.selectedGroup); + + this.logView(); } catch (message) { CoreDomUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true); diff --git a/src/addons/mod/feedback/services/feedback.ts b/src/addons/mod/feedback/services/feedback.ts index c38e62ff8..8dd3fcf9d 100644 --- a/src/addons/mod/feedback/services/feedback.ts +++ b/src/addons/mod/feedback/services/feedback.ts @@ -1093,25 +1093,21 @@ export class AddonModFeedbackProvider { * Report the feedback as being viewed. * * @param id Module ID. - * @param name Name of the feedback. * @param formViewed True if form was viewed. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - async logView(id: number, name?: string, formViewed: boolean = false, siteId?: string): Promise { + async logView(id: number, formViewed: boolean = false, siteId?: string): Promise { const params: AddonModFeedbackViewFeedbackWSParams = { feedbackid: id, moduleviewed: formViewed, }; - await CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.log( 'mod_feedback_view_feedback', params, AddonModFeedbackProvider.COMPONENT, id, - name, - 'feedback', - { moduleviewed: params.moduleviewed }, siteId, ); } diff --git a/src/addons/mod/folder/components/index/index.ts b/src/addons/mod/folder/components/index/index.ts index 709d92815..40daf4693 100644 --- a/src/addons/mod/folder/components/index/index.ts +++ b/src/addons/mod/folder/components/index/index.ts @@ -22,6 +22,7 @@ import { Md5 } from 'ts-md5'; import { AddonModFolder, AddonModFolderFolder, AddonModFolderProvider } from '../../services/folder'; import { AddonModFolderFolderFormattedData, AddonModFolderHelper } from '../../services/folder-helper'; import { AddonModFolderModuleHandlerService } from '../../services/handlers/module'; +import { CoreUtils } from '@services/utils/utils'; /** * Component that displays a folder. @@ -39,6 +40,7 @@ export class AddonModFolderIndexComponent extends CoreCourseModuleMainResourceCo @Input() subfolder?: AddonModFolderFolderFormattedData; // Subfolder to show. component = AddonModFolderProvider.COMPONENT; + pluginName = 'folder'; contents?: AddonModFolderFolderFormattedData; constructor(@Optional() courseContentsPage?: CoreCourseContentsPage) { @@ -119,7 +121,9 @@ export class AddonModFolderIndexComponent extends CoreCourseModuleMainResourceCo * @inheritdoc */ protected async logActivity(): Promise { - await AddonModFolder.logView(this.module.instance, this.module.name); + await CoreUtils.ignoreErrors(AddonModFolder.logView(this.module.instance)); + + this.analyticsLogEvent('mod_folder_view_folder'); } /** diff --git a/src/addons/mod/folder/services/folder.ts b/src/addons/mod/folder/services/folder.ts index 701e0f56d..ab4f8f450 100644 --- a/src/addons/mod/folder/services/folder.ts +++ b/src/addons/mod/folder/services/folder.ts @@ -126,23 +126,19 @@ export class AddonModFolderProvider { * Report a folder as being viewed. * * @param id Module ID. - * @param name Name of the folder. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - async logView(id: number, name?: string, siteId?: string): Promise { + async logView(id: number, siteId?: string): Promise { const params: AddonModFolderViewFolderWSParams = { folderid: id, }; - await CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.log( 'mod_folder_view_folder', params, AddonModFolderProvider.COMPONENT, id, - name, - 'folder', - {}, siteId, ); } diff --git a/src/addons/mod/forum/components/index/index.ts b/src/addons/mod/forum/components/index/index.ts index bb604e3f2..3283edc33 100644 --- a/src/addons/mod/forum/components/index/index.ts +++ b/src/addons/mod/forum/components/index/index.ts @@ -71,7 +71,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; component = AddonModForumProvider.COMPONENT; - moduleName = 'forum'; + pluginName = 'forum'; descriptionNote?: string; promisedDiscussions: CorePromisedValue; discussionsItems: AddonModForumDiscussionItem[] = []; @@ -708,12 +708,14 @@ class AddonModForumDiscussionsManager extends CoreListItemsManager { + // Log analytics even if the user cancels for consistency with LMS. + this.analyticsLogEvent('mod_forum_delete_post', `/mod/forum/post.php?delete=${this.post.id}`); + try { await CoreDomUtils.showDeleteConfirm('addon.mod_forum.deletesure'); @@ -290,6 +294,8 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges } this.scrollToForm(); + + this.analyticsLogEvent('mod_forum_add_discussion_post', `/mod/forum/post.php?reply=${this.post.id}`); } /** @@ -314,6 +320,8 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges ); this.scrollToForm(); + + this.analyticsLogEvent('mod_forum_update_discussion_post', `/mod/forum/post.php?edit=${this.post.id}`); } catch { // Cancelled. } @@ -554,4 +562,24 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges ); } + /** + * Log analytics event. + * + * @param wsName WS name. + * @param url URL. + */ + protected analyticsLogEvent(wsName: string, url: string): void { + if (this.post.id <= 0) { + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: wsName, + name: this.post.subject, + data: { id: this.post.id, forumid: this.forum.id, category: 'forum' }, + url, + }); + } + } diff --git a/src/addons/mod/forum/pages/discussion/discussion.ts b/src/addons/mod/forum/pages/discussion/discussion.ts index bb06dcce1..60117a03f 100644 --- a/src/addons/mod/forum/pages/discussion/discussion.ts +++ b/src/addons/mod/forum/pages/discussion/discussion.ts @@ -51,6 +51,7 @@ import { import { AddonModForumHelper } from '../../services/forum-helper'; import { AddonModForumOffline } from '../../services/forum-offline'; import { AddonModForumSync, AddonModForumSyncProvider } from '../../services/forum-sync'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; type SortType = 'flat-newest' | 'flat-oldest' | 'nested'; @@ -562,19 +563,7 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes if (forceMarkAsRead || (hasUnreadPosts && this.trackPosts)) { // Add log in Moodle and mark unread posts as readed. - AddonModForum.logDiscussionView(this.discussionId, this.forumId || -1, this.forum.name).catch(() => { - // Ignore errors. - }).finally(() => { - if (!this.courseId || !this.cmId) { - return; - } - - // Trigger mark read posts. - CoreEvents.trigger(AddonModForumProvider.MARK_READ_EVENT, { - courseId: this.courseId, - moduleId: this.cmId, - }, CoreSites.getCurrentSiteId()); - }); + this.logDiscussionView(forceMarkAsRead); } } } @@ -854,6 +843,35 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes return posts; } + /** + * Log discussion as viewed. This will also mark the posts as read. + * + * @param logAnalytics Whether to log analytics too or not. + */ + protected async logDiscussionView(logAnalytics = false): Promise { + await CoreUtils.ignoreErrors(AddonModForum.logDiscussionView(this.discussionId, this.forumId || -1)); + + if (logAnalytics) { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_forum_view_forum_discussion', + name: this.startingPost?.subject ?? this.forum.name ?? '', + data: { id: this.discussionId, forumid: this.forumId, category: 'forum' }, + url: `/mod/forum/discuss.php?d=${this.discussionId}` + (this.postId ? `#p${this.postId}` : ''), + }); + } + + if (!this.courseId || !this.cmId) { + return; + } + + // Trigger mark read posts. + CoreEvents.trigger(AddonModForumProvider.MARK_READ_EVENT, { + courseId: this.courseId, + moduleId: this.cmId, + }, CoreSites.getCurrentSiteId()); + } + } /** diff --git a/src/addons/mod/forum/pages/new-discussion/new-discussion.ts b/src/addons/mod/forum/pages/new-discussion/new-discussion.ts index cdb7772e9..6978faa24 100644 --- a/src/addons/mod/forum/pages/new-discussion/new-discussion.ts +++ b/src/addons/mod/forum/pages/new-discussion/new-discussion.ts @@ -44,6 +44,8 @@ import { AddonModForumDiscussionsSwipeManager } from '../../classes/forum-discus import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router'; import { AddonModForumDiscussionsSource } from '../../classes/forum-discussions-source'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; type NewDiscussionData = { subject: string; @@ -105,8 +107,19 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea protected originalData?: Partial; protected forceLeave = false; protected initialGroupId?: number; + protected logView: () => void; - constructor(protected route: ActivatedRoute, @Optional() protected splitView: CoreSplitViewComponent) {} + constructor(protected route: ActivatedRoute, @Optional() protected splitView: CoreSplitViewComponent) { + this.logView = CoreTime.once(() => { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_forum_add_discussion', + name: Translate.instant('addon.mod_forum.addanewdiscussion'), + data: { id: this.forumId, category: 'forum' }, + url: '/mod/forum/post.php', + }); + }); + } /** * @inheritdoc @@ -309,6 +322,8 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea } this.showForm = true; + + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.mod_forum.errorgetgroups', true); diff --git a/src/addons/mod/forum/services/forum.ts b/src/addons/mod/forum/services/forum.ts index d20f88efc..650dbefcc 100644 --- a/src/addons/mod/forum/services/forum.ts +++ b/src/addons/mod/forum/services/forum.ts @@ -996,23 +996,19 @@ export class AddonModForumProvider { * Report a forum as being viewed. * * @param id Module ID. - * @param name Name of the forum. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - logView(id: number, name?: string, siteId?: string): Promise { + logView(id: number, siteId?: string): Promise { const params = { forumid: id, }; - return CoreCourseLogHelper.logSingle( + return CoreCourseLogHelper.log( 'mod_forum_view_forum', params, AddonModForumProvider.COMPONENT, id, - name, - 'forum', - {}, siteId, ); } @@ -1022,23 +1018,19 @@ export class AddonModForumProvider { * * @param id Discussion ID. * @param forumId Forum ID. - * @param name Name of the forum. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - logDiscussionView(id: number, forumId: number, name?: string, siteId?: string): Promise { + logDiscussionView(id: number, forumId: number, siteId?: string): Promise { const params = { discussionid: id, }; - return CoreCourseLogHelper.logSingle( + return CoreCourseLogHelper.log( 'mod_forum_view_forum_discussion', params, AddonModForumProvider.COMPONENT, forumId, - name, - 'forum', - params, siteId, ); } diff --git a/src/addons/mod/glossary/components/index/index.ts b/src/addons/mod/glossary/components/index/index.ts index 3ca1498ca..56f5c4b87 100644 --- a/src/addons/mod/glossary/components/index/index.ts +++ b/src/addons/mod/glossary/components/index/index.ts @@ -56,6 +56,7 @@ import { import { AddonModGlossaryModuleHandlerService } from '../../services/handlers/module'; import { AddonModGlossaryPrefetchHandler } from '../../services/handlers/prefetch'; import { AddonModGlossaryModePickerPopoverComponent } from '../mode-picker/mode-picker'; +import { CoreTime } from '@singletons/time'; /** * Component that displays a glossary entry page. @@ -71,7 +72,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; component = AddonModGlossaryProvider.COMPONENT; - moduleName = 'glossary'; + pluginName = 'glossary'; canAdd = false; loadMoreError = false; @@ -86,6 +87,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity protected sourceUnsubscribe?: () => void; protected observers?: CoreEventObserver[]; protected checkCompletionAfterLog = false; // Use CoreListItemsManager log system instead. + protected logSearch?: () => void; getDivider?: (entry: AddonModGlossaryEntry) => string; showDivider: (entry: AddonModGlossaryEntry, previous?: AddonModGlossaryEntry) => boolean = () => false; @@ -226,6 +228,10 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity this.hasOfflineRatings = hasOfflineRatings; this.hasOffline = this.hasOfflineEntries || this.hasOfflineRatings; + + if (this.isSearch && this.logSearch) { + this.logSearch(); + } } /** @@ -424,11 +430,23 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity search(query: string): void { this.loadingMessage = Translate.instant('core.searching'); this.showLoading = true; + this.logSearch = CoreTime.once(() => this.performLogSearch(query)); this.entries?.getSource().search(query); this.loadContent(); } + /** + * Log search. + * + * @param query Text entered on the search box. + */ + protected async performLogSearch(query: string): Promise { + this.analyticsLogEvent('mod_glossary_get_entries_by_search', { + data: { mode: 'search', hook: query, fullsearch: 1 }, + }); + } + /** * @inheritdoc */ @@ -482,12 +500,14 @@ class AddonModGlossaryEntriesManager extends CoreListItemsManager void; + + constructor(@Optional() protected splitView: CoreSplitViewComponent, protected route: ActivatedRoute) { + this.logView = CoreTime.once(async () => { + if (!this.onlineEntry || !this.glossary || !this.componentId) { + return; + } + + await CoreUtils.ignoreErrors(AddonModGlossary.logEntryView(this.onlineEntry.id, this.componentId)); + + this.analyticsLogEvent('mod_glossary_get_entry_by_id', `/mod/glossary/showentry.php?eid=${this.onlineEntry.id}`); + }); + } get entry(): AddonModGlossaryEntry | AddonModGlossaryOfflineEntry | undefined { return this.onlineEntry ?? this.offlineEntry; @@ -128,12 +142,6 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy { try { if (onlineEntryId) { await this.loadOnlineEntry(onlineEntryId); - - if (!this.glossary || !this.componentId) { - return; - } - - await CoreUtils.ignoreErrors(AddonModGlossary.logEntryView(onlineEntryId, this.componentId, this.glossary?.name)); } else if (offlineEntryTimeCreated) { await this.loadOfflineEntry(offlineEntryTimeCreated); } @@ -161,6 +169,12 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy { * Delete entry. */ async deleteEntry(): Promise { + // Log analytics even if the user cancels for consistency with LMS. + this.analyticsLogEvent( + 'mod_glossary_delete_entry', + `/mod/glossary/deleteentry.php?id=${this.glossary?.id}&mode=delete&entry=${this.onlineEntry?.id}`, + ); + const glossaryId = this.glossary?.id; const cancelled = await CoreUtils.promiseFails( CoreDomUtils.showConfirm(Translate.instant('addon.mod_glossary.areyousuredelete')), @@ -250,6 +264,8 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy { this.canEdit = canUpdateEntries && !!result.permissions?.canupdate; await this.loadGlossary(); + + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentry', true); } @@ -321,6 +337,26 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy { AddonModGlossary.invalidateEntry(this.onlineEntry.id); } + /** + * Log analytics event. + * + * @param wsName WS name. + * @param url URL. + */ + protected analyticsLogEvent(wsName: string, url: string): void { + if (!this.onlineEntry || !this.glossary) { + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: wsName, + name: this.onlineEntry.concept, + data: { id: this.onlineEntry.id, glossaryid: this.glossary.id, category: 'glossary' }, + url, + }); + } + } /** diff --git a/src/addons/mod/glossary/services/glossary.ts b/src/addons/mod/glossary/services/glossary.ts index 487544639..fef56bab5 100644 --- a/src/addons/mod/glossary/services/glossary.ts +++ b/src/addons/mod/glossary/services/glossary.ts @@ -1020,23 +1020,19 @@ export class AddonModGlossaryProvider { * * @param glossaryId Glossary ID. * @param mode The mode in which the glossary was viewed. - * @param name Name of the glossary. * @param siteId Site ID. If not defined, current site. */ - async logView(glossaryId: number, mode: string, name?: string, siteId?: string): Promise { + async logView(glossaryId: number, mode: string, siteId?: string): Promise { const params: AddonModGlossaryViewGlossaryWSParams = { id: glossaryId, mode: mode, }; - await CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.log( 'mod_glossary_view_glossary', params, AddonModGlossaryProvider.COMPONENT, glossaryId, - name, - 'glossary', - { mode }, siteId, ); } @@ -1046,22 +1042,18 @@ export class AddonModGlossaryProvider { * * @param entryId Entry ID. * @param glossaryId Glossary ID. - * @param name Name of the glossary. * @param siteId Site ID. If not defined, current site. */ - async logEntryView(entryId: number, glossaryId: number, name?: string, siteId?: string): Promise { + async logEntryView(entryId: number, glossaryId: number, siteId?: string): Promise { const params: AddonModGlossaryViewEntryWSParams = { id: entryId, }; - await CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.log( 'mod_glossary_view_entry', params, AddonModGlossaryProvider.COMPONENT, glossaryId, - name, - 'glossary', - { entryid: entryId }, siteId, ); } diff --git a/src/addons/mod/h5pactivity/components/index/index.ts b/src/addons/mod/h5pactivity/components/index/index.ts index b84fc38bd..7af252102 100644 --- a/src/addons/mod/h5pactivity/components/index/index.ts +++ b/src/addons/mod/h5pactivity/components/index/index.ts @@ -63,7 +63,7 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv @Output() onActivityFinish = new EventEmitter(); component = AddonModH5PActivityProvider.COMPONENT; - moduleName = 'h5pactivity'; + pluginName = 'h5pactivity'; h5pActivity?: AddonModH5PActivityData; // The H5P activity object. accessInfo?: AddonModH5PActivityAccessInfo; // Info about the user capabilities. @@ -441,9 +441,11 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv this.playing = true; // Mark the activity as viewed. - await AddonModH5PActivity.logView(this.h5pActivity.id, this.h5pActivity.name, this.siteId); + await AddonModH5PActivity.logView(this.h5pActivity.id, this.siteId); this.checkCompletion(); + + this.analyticsLogEvent('mod_h5pactivity_view_h5pactivity'); } /** diff --git a/src/addons/mod/h5pactivity/pages/attempt-results/attempt-results.ts b/src/addons/mod/h5pactivity/pages/attempt-results/attempt-results.ts index e54f4017e..3142c9637 100644 --- a/src/addons/mod/h5pactivity/pages/attempt-results/attempt-results.ts +++ b/src/addons/mod/h5pactivity/pages/attempt-results/attempt-results.ts @@ -25,6 +25,8 @@ import { AddonModH5PActivityData, AddonModH5PActivityAttemptResults, } from '../../services/h5pactivity'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays results of an attempt. @@ -45,7 +47,28 @@ export class AddonModH5PActivityAttemptResultsPage implements OnInit { cmId!: number; protected attemptId!: number; - protected fetchSuccess = false; + protected logView: () => void; + + constructor() { + this.logView = CoreTime.once(async () => { + if (!this.h5pActivity) { + return; + } + + await CoreUtils.ignoreErrors(AddonModH5PActivity.logViewReport( + this.h5pActivity.id, + { attemptId: this.attemptId }, + )); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_h5pactivity_log_report_viewed', + name: this.h5pActivity.name, + data: { id: this.h5pActivity.id, attemptid: this.attemptId, category: 'h5pactivity' }, + url: `/mod/h5pactivity/report.php?a=${this.h5pActivity.id}&attemptid=${this.attemptId}`, + }); + }); + } /** * @inheritdoc @@ -92,14 +115,7 @@ export class AddonModH5PActivityAttemptResultsPage implements OnInit { await this.fetchUserProfile(); - if (!this.fetchSuccess) { - this.fetchSuccess = true; - CoreUtils.ignoreErrors(AddonModH5PActivity.logViewReport( - this.h5pActivity.id, - this.h5pActivity.name, - { attemptId: this.attemptId }, - )); - } + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error loading attempt.'); } finally { diff --git a/src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.ts b/src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.ts index e91c957bd..f0c2c9faf 100644 --- a/src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.ts +++ b/src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.ts @@ -26,6 +26,8 @@ import { AddonModH5PActivityData, AddonModH5PActivityUserAttempts, } from '../../services/h5pactivity'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays user attempts of a certain user. @@ -46,7 +48,28 @@ export class AddonModH5PActivityUserAttemptsPage implements OnInit { isCurrentUser = false; protected userId!: number; - protected fetchSuccess = false; + protected logView: () => void; + + constructor() { + this.logView = CoreTime.once(async () => { + if (!this.h5pActivity) { + return; + } + + await CoreUtils.ignoreErrors(AddonModH5PActivity.logViewReport( + this.h5pActivity.id, + { userId: this.userId }, + )); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_h5pactivity_log_report_viewed', + name: this.h5pActivity.name, + data: { id: this.h5pActivity.id, userid: this.userId, category: 'h5pactivity' }, + url: `/mod/h5pactivity/report.php?a=${this.h5pActivity.id}&userid=${this.userId}`, + }); + }); + } /** * @inheritdoc @@ -94,14 +117,7 @@ export class AddonModH5PActivityUserAttemptsPage implements OnInit { this.fetchUserProfile(), ]); - if (!this.fetchSuccess) { - this.fetchSuccess = true; - CoreUtils.ignoreErrors(AddonModH5PActivity.logViewReport( - this.h5pActivity.id, - this.h5pActivity.name, - { userId: this.userId }, - )); - } + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error loading attempts.'); } finally { diff --git a/src/addons/mod/h5pactivity/pages/users-attempts/users-attempts.ts b/src/addons/mod/h5pactivity/pages/users-attempts/users-attempts.ts index cf977abbf..f5eff6eb1 100644 --- a/src/addons/mod/h5pactivity/pages/users-attempts/users-attempts.ts +++ b/src/addons/mod/h5pactivity/pages/users-attempts/users-attempts.ts @@ -25,6 +25,8 @@ import { AddonModH5PActivityProvider, AddonModH5PActivityUserAttempts, } from '../../services/h5pactivity'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays all users that can attempt an H5P activity. @@ -45,7 +47,25 @@ export class AddonModH5PActivityUsersAttemptsPage implements OnInit { canLoadMore = false; protected page = 0; - protected fetchSuccess = false; + protected logView: () => void; + + constructor() { + this.logView = CoreTime.once(async () => { + if (!this.h5pActivity) { + return; + } + + await CoreUtils.ignoreErrors(AddonModH5PActivity.logViewReport(this.h5pActivity.id)); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'mod_h5pactivity_log_report_viewed', + name: this.h5pActivity.name, + data: { id: this.h5pActivity.id, category: 'h5pactivity' }, + url: `/mod/h5pactivity/report.php?a=${this.h5pActivity.id}`, + }); + }); + } /** * @inheritdoc @@ -90,10 +110,7 @@ export class AddonModH5PActivityUsersAttemptsPage implements OnInit { this.fetchUsers(refresh), ]); - if (!this.fetchSuccess) { - this.fetchSuccess = true; - CoreUtils.ignoreErrors(AddonModH5PActivity.logViewReport(this.h5pActivity.id, this.h5pActivity.name)); - } + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error loading attempts.'); } finally { diff --git a/src/addons/mod/h5pactivity/services/h5pactivity.ts b/src/addons/mod/h5pactivity/services/h5pactivity.ts index 594e0d201..5a23aa738 100644 --- a/src/addons/mod/h5pactivity/services/h5pactivity.ts +++ b/src/addons/mod/h5pactivity/services/h5pactivity.ts @@ -776,23 +776,19 @@ export class AddonModH5PActivityProvider { * Report an H5P activity as being viewed. * * @param id H5P activity ID. - * @param name Name of the activity. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - logView(id: number, name?: string, siteId?: string): Promise { + logView(id: number, siteId?: string): Promise { const params: AddonModH5PActivityViewH5pactivityWSParams = { h5pactivityid: id, }; - return CoreCourseLogHelper.logSingle( + return CoreCourseLogHelper.log( 'mod_h5pactivity_view_h5pactivity', params, AddonModH5PActivityProvider.COMPONENT, id, - name, - 'h5pactivity', - {}, siteId, ); } @@ -801,11 +797,10 @@ export class AddonModH5PActivityProvider { * Report an H5P activity report as being viewed. * * @param id H5P activity ID. - * @param name Name of the activity. * @param options Options. * @returns Promise resolved when the WS call is successful. */ - async logViewReport(id: number, name?: string, options: AddonModH5PActivityViewReportOptions = {}): Promise { + async logViewReport(id: number, options: AddonModH5PActivityViewReportOptions = {}): Promise { const site = await CoreSites.getSite(options.siteId); if (!site.wsAvailable('mod_h5pactivity_log_report_viewed')) { @@ -819,14 +814,11 @@ export class AddonModH5PActivityProvider { attemptid: options.attemptId, }; - return CoreCourseLogHelper.logSingle( + return CoreCourseLogHelper.log( 'mod_h5pactivity_log_report_viewed', params, AddonModH5PActivityProvider.COMPONENT, id, - name, - 'h5pactivity', - {}, site.getId(), ); } diff --git a/src/addons/mod/imscp/components/index/index.ts b/src/addons/mod/imscp/components/index/index.ts index bde54ea69..29e111c8a 100644 --- a/src/addons/mod/imscp/components/index/index.ts +++ b/src/addons/mod/imscp/components/index/index.ts @@ -18,6 +18,7 @@ import { CoreCourseContentsPage } from '@features/course/pages/contents/contents import { CoreCourse } from '@features/course/services/course'; import { CoreNavigator } from '@services/navigator'; import { AddonModImscpProvider, AddonModImscp, AddonModImscpTocItem } from '../../services/imscp'; +import { CoreUtils } from '@services/utils/utils'; /** * Component that displays a IMSCP. @@ -30,6 +31,7 @@ import { AddonModImscpProvider, AddonModImscp, AddonModImscpTocItem } from '../. export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit { component = AddonModImscpProvider.COMPONENT; + pluginName = 'imscp'; items: AddonModImscpTocItem[] = []; hasStarted = false; @@ -100,7 +102,9 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom * @inheritdoc */ protected async logActivity(): Promise { - await AddonModImscp.logView(this.module.instance, this.module.name); + await CoreUtils.ignoreErrors(AddonModImscp.logView(this.module.instance)); + + this.analyticsLogEvent('mod_imscp_view_imscp'); } /** diff --git a/src/addons/mod/imscp/services/imscp.ts b/src/addons/mod/imscp/services/imscp.ts index 4c388cf78..f71cf1193 100644 --- a/src/addons/mod/imscp/services/imscp.ts +++ b/src/addons/mod/imscp/services/imscp.ts @@ -271,7 +271,6 @@ export class AddonModImscpProvider { * Report a IMSCP as being viewed. * * @param id Module ID. - * @param name Name of the imscp. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ @@ -280,14 +279,11 @@ export class AddonModImscpProvider { imscpid: id, }; - await CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.log( 'mod_imscp_view_imscp', params, AddonModImscpProvider.COMPONENT, id, - name, - 'imscp', - {}, siteId, ); } diff --git a/src/addons/mod/lesson/components/index/index.ts b/src/addons/mod/lesson/components/index/index.ts index 7d89c2004..b4b6bcc98 100644 --- a/src/addons/mod/lesson/components/index/index.ts +++ b/src/addons/mod/lesson/components/index/index.ts @@ -66,7 +66,7 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo @Input() action?: string; // The "action" to display first. component = AddonModLessonProvider.COMPONENT; - moduleName = 'lesson'; + pluginName = 'lesson'; lesson?: AddonModLessonLessonWSData; // The lesson. selectedTab?: number; // The initial selected tab. @@ -372,7 +372,16 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo return; } - await AddonModLesson.logViewLesson(this.lesson.id, this.password, this.lesson.name); + await CoreUtils.ignoreErrors(AddonModLesson.logViewLesson(this.lesson.id, this.password)); + } + + /** + * Call analytics. + */ + protected callAnalyticsLogEvent(): void { + this.analyticsLogEvent('mod_lesson_view_lesson', { + url: this.selectedTab === 1 ? `/mod/lesson/report.php?id=${this.module.id}&action=reportoverview` : undefined, + }); } /** @@ -435,13 +444,19 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo * First tab selected. */ indexSelected(): void { + const tabHasChanged = this.selectedTab !== 0; this.selectedTab = 0; + + if (tabHasChanged) { + this.callAnalyticsLogEvent(); + } } /** * Reports tab selected. */ reportsSelected(): void { + const tabHasChanged = this.selectedTab !== 1; this.selectedTab = 1; if (!this.groupInfo) { @@ -449,6 +464,10 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo CoreDomUtils.showErrorModalDefault(error, 'Error getting report.'); }); } + + if (tabHasChanged) { + this.callAnalyticsLogEvent(); + } } /** diff --git a/src/addons/mod/lesson/pages/player/player.ts b/src/addons/mod/lesson/pages/player/player.ts index 2edc3daf7..99ca572a2 100644 --- a/src/addons/mod/lesson/pages/player/player.ts +++ b/src/addons/mod/lesson/pages/player/player.ts @@ -54,6 +54,7 @@ import { import { AddonModLessonOffline } from '../../services/lesson-offline'; import { AddonModLessonSync } from '../../services/lesson-sync'; import { CoreFormFields, CoreForms } from '@singletons/form'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that allows attempting and reviewing a lesson. @@ -446,6 +447,8 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy, CanLeave { this.reviewPageId = Number(params.pageid); } } + + this.logPageLoaded(AddonModLessonProvider.LESSON_EOL, Translate.instant('addon.mod_lesson.congratulations')); } /** @@ -615,6 +618,44 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy, CanLeave { } else { this.showRetake = false; } + + this.logPageLoaded(pageId, data.page?.title ?? ''); + } + + /** + * Log page loaded. + * + * @param pageId Page ID. + */ + protected logPageLoaded(pageId: number, title: string): void { + if (!this.lesson) { + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_lesson_get_page_data', + name: this.lesson.name + ': ' + title, + data: { id: this.lesson.id, pageid: pageId, category: 'lesson' }, + url: `/mod/lesson/view.php?id=${this.lesson.id}&pageid=${pageId}`, + }); + } + + /** + * Log continue page. + */ + protected logContinuePageLoaded(): void { + if (!this.lesson) { + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_lesson_process_page', + name: this.lesson.name + ': ' + Translate.instant('addon.mod_lesson.continue'), + data: { id: this.lesson.id, category: 'lesson' }, + url: '/mod/lesson/continue.php', + }); } /** @@ -715,6 +756,8 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy, CanLeave { pageId: result.newpageid, }); } + + this.logContinuePageLoaded(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error processing page'); } finally { diff --git a/src/addons/mod/lesson/pages/user-retake/user-retake.ts b/src/addons/mod/lesson/pages/user-retake/user-retake.ts index 792c6477d..7ea3706a7 100644 --- a/src/addons/mod/lesson/pages/user-retake/user-retake.ts +++ b/src/addons/mod/lesson/pages/user-retake/user-retake.ts @@ -35,6 +35,7 @@ import { } from '../../services/lesson'; import { AddonModLessonAnswerData, AddonModLessonHelper } from '../../services/lesson-helper'; import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays a retake made by a certain user. @@ -59,6 +60,11 @@ export class AddonModLessonUserRetakePage implements OnInit { protected userId?: number; // User ID to see the retakes. protected retakeNumber?: number; // Number of the initial retake to see. protected previousSelectedRetake?: number; // To be able to detect the previous selected retake when it has changed. + protected logView: () => void; + + constructor() { + this.logView = CoreTime.once(() => this.performLogView()); + } /** * @inheritdoc @@ -93,6 +99,8 @@ export class AddonModLessonUserRetakePage implements OnInit { try { await this.setRetake(retakeNumber); + + this.performLogView(); } catch (error) { this.selectedRetake = this.previousSelectedRetake ?? this.selectedRetake; CoreDomUtils.showErrorModal(CoreUtils.addDataNotDownloadedError(error, 'Error getting attempt.')); @@ -160,6 +168,8 @@ export class AddonModLessonUserRetakePage implements OnInit { this.student.profileimageurl = user?.profileimageurl; await this.setRetake(this.selectedRetake); + + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error getting data.', true); } @@ -243,6 +253,23 @@ export class AddonModLessonUserRetakePage implements OnInit { return formattedData; } + /** + * Log view. + */ + protected performLogView(): void { + if (!this.lesson) { + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_lesson_get_user_attempt', + name: this.lesson.name + ': ' + Translate.instant('addon.mod_lesson.detailedstats'), + data: { id: this.lesson.id, userid: this.userId, try: this.selectedRetake, category: 'lesson' }, + url: `/mod/lesson/report.php?id=${this.cmId}&action=reportdetail&userid=${this.userId}&try=${this.selectedRetake}`, + }); + } + } /** diff --git a/src/addons/mod/lesson/services/lesson.ts b/src/addons/mod/lesson/services/lesson.ts index 27e780f13..af0b49451 100644 --- a/src/addons/mod/lesson/services/lesson.ts +++ b/src/addons/mod/lesson/services/lesson.ts @@ -2951,11 +2951,10 @@ export class AddonModLessonProvider { * * @param id Module ID. * @param password Lesson password (if any). - * @param name Name of the assign. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - async logViewLesson(id: number, password?: string, name?: string, siteId?: string): Promise { + async logViewLesson(id: number, password?: string, siteId?: string): Promise { const params: AddonModLessonViewLessonWSParams = { lessonid: id, }; @@ -2964,14 +2963,11 @@ export class AddonModLessonProvider { params.password = password; } - await CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.log( 'mod_lesson_view_lesson', params, AddonModLessonProvider.COMPONENT, id, - name, - 'lesson', - {}, siteId, ); } diff --git a/src/addons/mod/lti/components/index/index.ts b/src/addons/mod/lti/components/index/index.ts index 333f0bb48..c1870a741 100644 --- a/src/addons/mod/lti/components/index/index.ts +++ b/src/addons/mod/lti/components/index/index.ts @@ -30,7 +30,7 @@ import { AddonModLtiHelper } from '../../services/lti-helper'; export class AddonModLtiIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit { component = AddonModLtiProvider.COMPONENT; - moduleName = 'lti'; + pluginName = 'lti'; displayDescription = false; lti?: AddonModLtiLti; // The LTI object. diff --git a/src/addons/mod/lti/services/lti-helper.ts b/src/addons/mod/lti/services/lti-helper.ts index 2d5aaa00b..9b0ab35e7 100644 --- a/src/addons/mod/lti/services/lti-helper.ts +++ b/src/addons/mod/lti/services/lti-helper.ts @@ -22,6 +22,7 @@ import { CoreDomUtils } from '@services/utils/dom'; import { makeSingleton } from '@singletons'; import { CoreEvents } from '@singletons/events'; import { AddonModLti, AddonModLtiLti } from './lti'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Service that provides some helper functions for LTI. @@ -86,7 +87,7 @@ export class AddonModLtiHelperProvider { const launchData = await AddonModLti.getLtiLaunchData(lti.id); // "View" LTI without blocking the UI. - this.logViewAndCheckCompletion(courseId, module, lti.id, lti.name, siteId); + this.logViewAndCheckCompletion(courseId, module, lti.id, siteId); // Launch LTI. return AddonModLti.launch(launchData.endpoint, launchData.parameters); @@ -111,16 +112,23 @@ export class AddonModLtiHelperProvider { courseId: number, module: CoreCourseModuleData, ltiId: number, - name?: string, siteId?: string, ): Promise { try { - await AddonModLti.logView(ltiId, name, siteId); + await AddonModLti.logView(ltiId,siteId); CoreCourse.checkModuleCompletion(courseId, module.completiondata); } catch { // Ignore errors. } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_lti_view_lti', + name: module.name, + data: { id: module.instance, category: 'lti' }, + url: `/mod/lti/view.php?id=${module.id}`, + }); } } diff --git a/src/addons/mod/lti/services/lti.ts b/src/addons/mod/lti/services/lti.ts index 357510506..989432eab 100644 --- a/src/addons/mod/lti/services/lti.ts +++ b/src/addons/mod/lti/services/lti.ts @@ -272,14 +272,11 @@ export class AddonModLtiProvider { ltiid: id, }; - return CoreCourseLogHelper.logSingle( + return CoreCourseLogHelper.log( 'mod_lti_view_lti', params, AddonModLtiProvider.COMPONENT, id, - name, - 'lti', - {}, siteId, ); } diff --git a/src/addons/mod/page/components/index/index.ts b/src/addons/mod/page/components/index/index.ts index 957a67fe9..d139e0473 100644 --- a/src/addons/mod/page/components/index/index.ts +++ b/src/addons/mod/page/components/index/index.ts @@ -31,6 +31,7 @@ import { AddonModPageHelper } from '../../services/page-helper'; export class AddonModPageIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit { component = AddonModPageProvider.COMPONENT; + pluginName = 'page'; contents?: string; displayDescription = false; displayTimemodified = true; @@ -114,7 +115,9 @@ export class AddonModPageIndexComponent extends CoreCourseModuleMainResourceComp * @inheritdoc */ protected async logActivity(): Promise { - await AddonModPage.logView(this.module.instance, this.module.name); + await CoreUtils.ignoreErrors(AddonModPage.logView(this.module.instance)); + + this.analyticsLogEvent('mod_page_view_page'); } } diff --git a/src/addons/mod/page/services/page.ts b/src/addons/mod/page/services/page.ts index 6a266342c..19579a5e8 100644 --- a/src/addons/mod/page/services/page.ts +++ b/src/addons/mod/page/services/page.ts @@ -141,23 +141,19 @@ export class AddonModPageProvider { * Report a page as being viewed. * * @param pageid Module ID. - * @param name Name of the page. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - logView(pageid: number, name?: string, siteId?: string): Promise { + logView(pageid: number, siteId?: string): Promise { const params: AddonModPageViewPageWSParams = { pageid, }; - return CoreCourseLogHelper.logSingle( + return CoreCourseLogHelper.log( 'mod_page_view_page', params, AddonModPageProvider.COMPONENT, pageid, - name, - 'page', - {}, siteId, ); } diff --git a/src/addons/mod/quiz/components/index/index.ts b/src/addons/mod/quiz/components/index/index.ts index b9390ec91..cf5d867e5 100644 --- a/src/addons/mod/quiz/components/index/index.ts +++ b/src/addons/mod/quiz/components/index/index.ts @@ -57,7 +57,7 @@ import { export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit, OnDestroy { component = AddonModQuizProvider.COMPONENT; - moduleName = 'quiz'; + pluginName = 'quiz'; quiz?: AddonModQuizQuizData; // The quiz. now?: number; // Current time. syncTime?: string; // Last synchronization time. @@ -386,7 +386,9 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp return; // Shouldn't happen. } - await AddonModQuiz.logViewQuiz(this.quiz.id, this.quiz.name); + await CoreUtils.ignoreErrors(AddonModQuiz.logViewQuiz(this.quiz.id)); + + this.analyticsLogEvent('mod_quiz_view_quiz'); } /** diff --git a/src/addons/mod/quiz/pages/player/player.ts b/src/addons/mod/quiz/pages/player/player.ts index b70daf5f8..0ea6316c0 100644 --- a/src/addons/mod/quiz/pages/player/player.ts +++ b/src/addons/mod/quiz/pages/player/player.ts @@ -49,6 +49,7 @@ import { CoreDom } from '@singletons/dom'; import { CoreTime } from '@singletons/time'; import { CoreDirectivesRegistry } from '@singletons/directives-registry'; import { CoreWSError } from '@classes/errors/wserror'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that allows attempting a quiz. @@ -534,9 +535,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { // @todo MOBILE-4350: This is called before getting the attempt data in sequential quizzes as a workaround for a bug // in the LMS. Once the bug has been fixed, this should be reverted. if (this.isSequential) { - await CoreUtils.ignoreErrors( - AddonModQuiz.logViewAttempt(this.attempt.id, page, this.preflightData, this.offline, this.quiz), - ); + await this.logViewPage(page); } const data = await AddonModQuiz.getAttemptData(this.attempt.id, page, this.preflightData, { @@ -569,15 +568,55 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { // Mark the page as viewed. if (!this.isSequential) { // @todo MOBILE-4350: Undo workaround. - CoreUtils.ignoreErrors( - AddonModQuiz.logViewAttempt(this.attempt.id, page, this.preflightData, this.offline, this.quiz), - ); + await this.logViewPage(page); } // Start looking for changes. this.autoSave.startCheckChangesProcess(this.quiz, this.attempt, this.preflightData, this.offline); } + /** + * Log view a page. + * + * @param page Page viewed. + */ + protected async logViewPage(page: number): Promise { + if (!this.quiz || !this.attempt) { + return; + } + + await CoreUtils.ignoreErrors(AddonModQuiz.logViewAttempt(this.attempt.id, page, this.preflightData, this.offline)); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_quiz_view_attempt', + name: this.quiz.name, + data: { id: this.attempt.id, quizid: this.quiz.id, page, category: 'quiz' }, + url: `/mod/quiz/attempt.php?attempt=${this.attempt.id}&cmid=${this.cmId}` + (page > 0 ? `&page=${page}` : ''), + }); + } + + /** + * Log view summary. + */ + protected async logViewSummary(): Promise { + if (!this.quiz || !this.attempt) { + return; + } + + await CoreUtils.ignoreErrors( + AddonModQuiz.logViewAttemptSummary(this.attempt.id, this.preflightData, this.quiz.id), + ); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_quiz_view_attempt_summary', + name: this.quiz.name, + data: { id: this.attempt.id, quizid: this.quiz.id, category: 'quiz' }, + url: `/mod/quiz/summary.php?attempt=${this.attempt.id}&cmid=${this.cmId}`, + }); + } + /** * Refresh attempt data. */ @@ -618,10 +657,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { this.dueDateWarning = AddonModQuiz.getAttemptDueDateWarning(this.quiz, this.attempt); - // Log summary as viewed. - CoreUtils.ignoreErrors( - AddonModQuiz.logViewAttemptSummary(this.attempt.id, this.preflightData, this.quiz.id, this.quiz.name), - ); + this.logViewSummary(); } /** diff --git a/src/addons/mod/quiz/pages/review/review.ts b/src/addons/mod/quiz/pages/review/review.ts index a5c789a0c..63eeaf7d7 100644 --- a/src/addons/mod/quiz/pages/review/review.ts +++ b/src/addons/mod/quiz/pages/review/review.ts @@ -37,6 +37,7 @@ import { AddonModQuizWSAdditionalData, } from '../../services/quiz'; import { AddonModQuizHelper } from '../../services/quiz-helper'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that allows reviewing a quiz attempt. @@ -73,11 +74,15 @@ export class AddonModQuizReviewPage implements OnInit { protected attemptId!: number; // The attempt being reviewed. protected currentPage!: number; // The current page being reviewed. protected options?: AddonModQuizCombinedReviewOptions; // Review options. - protected fetchSuccess = false; + protected logView: () => void; constructor( protected elementRef: ElementRef, ) { + this.logView = CoreTime.once(() => this.performLogView(true, { + showAllDisabled: !this.showAll, + page: this.currentPage, + })); } /** @@ -127,6 +132,8 @@ export class AddonModQuizReviewPage implements OnInit { try { await this.loadPage(page); + + this.performLogView(false, { page }); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorgetquestions', true); } finally { @@ -156,12 +163,7 @@ export class AddonModQuizReviewPage implements OnInit { // Load questions. await this.loadPage(this.currentPage); - if (!this.fetchSuccess) { - this.fetchSuccess = true; - CoreUtils.ignoreErrors( - AddonModQuiz.logViewAttemptReview(this.attemptId, this.quiz.id, this.quiz.name), - ); - } + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorgetquiz', true); } @@ -325,11 +327,13 @@ export class AddonModQuizReviewPage implements OnInit { /** * Switch mode: all questions in same page OR one page at a time. */ - switchMode(): void { + async switchMode(): Promise { this.showAll = !this.showAll; // Load all questions or first page, depending on the mode. - this.loadPage(this.showAll ? -1 : 0); + await this.loadPage(this.showAll ? -1 : 0); + + this.performLogView(false, { showAllDisabled: !this.showAll }); } async openNavigation(): Promise { @@ -351,6 +355,37 @@ export class AddonModQuizReviewPage implements OnInit { this.changePage(modalData.page, modalData.slot); } + /** + * Perform log view. + * + * @param logInLMS Whether to log in LMS too or only in analytics. + * @param options Other options. + */ + protected async performLogView(logInLMS = false, options: LogViewOptions = {}): Promise { + if (!this.quiz) { + return; + } + + if (logInLMS) { + await CoreUtils.ignoreErrors(AddonModQuiz.logViewAttemptReview(this.attemptId, this.quiz.id)); + } + + let url = `/mod/quiz/review.php?attempt=${this.attemptId}&cmid=${this.cmId}`; + if (options.showAllDisabled) { + url += '&showall=0'; + } else if (options.page && options.page > 0) { + url += `&page=${ options.page}`; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_quiz_view_attempt_review', + name: this.quiz.name, + data: { id: this.attemptId, quizid: this.quiz.id, page: options.page, category: 'quiz' }, + url: url, + }); + } + } /** @@ -359,3 +394,8 @@ export class AddonModQuizReviewPage implements OnInit { type QuizQuestion = CoreQuestionQuestionParsed & { readableMark?: string; }; + +type LogViewOptions = { + page?: number; // Page being viewed (if viewing pages); + showAllDisabled?: boolean; // Whether the showAll option has just been disabled. +}; diff --git a/src/addons/mod/quiz/services/quiz-sync.ts b/src/addons/mod/quiz/services/quiz-sync.ts index 74871a22e..1dd59d96c 100644 --- a/src/addons/mod/quiz/services/quiz-sync.ts +++ b/src/addons/mod/quiz/services/quiz-sync.ts @@ -404,13 +404,11 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider if (!finish) { // Answers sent, now set the current page. - // Don't pass the quiz instance because we don't want to trigger a Firebase event in this case. await CoreUtils.ignoreErrors(AddonModQuiz.logViewAttempt( onlineAttempt.id, offlineAttempt.currentpage, preflightData, false, - undefined, siteId, )); } diff --git a/src/addons/mod/quiz/services/quiz.ts b/src/addons/mod/quiz/services/quiz.ts index 95fefe96c..3582a144a 100644 --- a/src/addons/mod/quiz/services/quiz.ts +++ b/src/addons/mod/quiz/services/quiz.ts @@ -21,7 +21,6 @@ import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreGradesFormattedItem, CoreGradesHelper } from '@features/grades/services/grades-helper'; -import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; import { CoreQuestion, CoreQuestionQuestionParsed, @@ -1535,7 +1534,6 @@ export class AddonModQuizProvider { * @param page Page number. * @param preflightData Preflight required data (like password). * @param offline Whether attempt is offline. - * @param quiz Quiz instance. If set, a Firebase event will be stored. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ @@ -1544,7 +1542,6 @@ export class AddonModQuizProvider { page: number = 0, preflightData: Record = {}, offline?: boolean, - quiz?: AddonModQuizQuizWSData, siteId?: string, ): Promise { const site = await CoreSites.getSite(siteId); @@ -1564,16 +1561,6 @@ export class AddonModQuizProvider { if (offline) { promises.push(AddonModQuizOffline.setAttemptCurrentPage(attemptId, page, site.getId())); } - if (quiz) { - CorePushNotifications.logViewEvent( - quiz.id, - quiz.name, - 'quiz', - 'mod_quiz_view_attempt', - { attemptid: attemptId, page }, - siteId, - ); - } await Promise.all(promises); } @@ -1583,23 +1570,19 @@ export class AddonModQuizProvider { * * @param attemptId Attempt ID. * @param quizId Quiz ID. - * @param name Name of the quiz. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - logViewAttemptReview(attemptId: number, quizId: number, name?: string, siteId?: string): Promise { + logViewAttemptReview(attemptId: number, quizId: number, siteId?: string): Promise { const params: AddonModQuizViewAttemptReviewWSParams = { attemptid: attemptId, }; - return CoreCourseLogHelper.logSingle( + return CoreCourseLogHelper.log( 'mod_quiz_view_attempt_review', params, AddonModQuizProvider.COMPONENT, quizId, - name, - 'quiz', - params, siteId, ); } @@ -1610,7 +1593,6 @@ export class AddonModQuizProvider { * @param attemptId Attempt ID. * @param preflightData Preflight required data (like password). * @param quizId Quiz ID. - * @param name Name of the quiz. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ @@ -1618,7 +1600,6 @@ export class AddonModQuizProvider { attemptId: number, preflightData: Record, quizId: number, - name?: string, siteId?: string, ): Promise { const params: AddonModQuizViewAttemptSummaryWSParams = { @@ -1630,14 +1611,11 @@ export class AddonModQuizProvider { ), }; - return CoreCourseLogHelper.logSingle( + return CoreCourseLogHelper.log( 'mod_quiz_view_attempt_summary', params, AddonModQuizProvider.COMPONENT, quizId, - name, - 'quiz', - { attemptid: attemptId }, siteId, ); } @@ -1646,23 +1624,19 @@ export class AddonModQuizProvider { * Report a quiz as being viewed. * * @param id Module ID. - * @param name Name of the quiz. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - logViewQuiz(id: number, name?: string, siteId?: string): Promise { + logViewQuiz(id: number, siteId?: string): Promise { const params: AddonModQuizViewQuizWSParams = { quizid: id, }; - return CoreCourseLogHelper.logSingle( + return CoreCourseLogHelper.log( 'mod_quiz_view_quiz', params, AddonModQuizProvider.COMPONENT, id, - name, - 'quiz', - {}, siteId, ); } diff --git a/src/addons/mod/resource/components/index/index.ts b/src/addons/mod/resource/components/index/index.ts index 3871a2798..0830bae6a 100644 --- a/src/addons/mod/resource/components/index/index.ts +++ b/src/addons/mod/resource/components/index/index.ts @@ -47,6 +47,7 @@ import { CorePlatform } from '@services/platform'; export class AddonModResourceIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy { component = AddonModResourceProvider.COMPONENT; + pluginName = 'resource'; mode = ''; src = ''; @@ -188,7 +189,9 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource * @inheritdoc */ protected async logActivity(): Promise { - await AddonModResource.logView(this.module.instance, this.module.name); + await CoreUtils.ignoreErrors(AddonModResource.logView(this.module.instance)); + + this.analyticsLogEvent('mod_resource_view_resource'); } /** diff --git a/src/addons/mod/resource/services/resource-helper.ts b/src/addons/mod/resource/services/resource-helper.ts index 11e75b041..e781f0b54 100644 --- a/src/addons/mod/resource/services/resource-helper.ts +++ b/src/addons/mod/resource/services/resource-helper.ts @@ -28,6 +28,7 @@ import { CoreUtilsOpenFileOptions } from '@services/utils/utils'; import { makeSingleton, Translate } from '@singletons'; import { CorePath } from '@singletons/path'; import { AddonModResource, AddonModResourceProvider } from './resource'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Service that provides helper functions for resources. @@ -206,6 +207,14 @@ export class AddonModResourceHelperProvider { } catch { // Ignore errors. } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_resource_view_resource', + name: module.name, + data: { id: module.instance, category: 'resource' }, + url: `/mod/resource/view.php?id=${module.id}`, + }); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.mod_resource.errorwhileloadingthecontent', true); } finally { diff --git a/src/addons/mod/resource/services/resource.ts b/src/addons/mod/resource/services/resource.ts index 10c6e1063..2b590b2c6 100644 --- a/src/addons/mod/resource/services/resource.ts +++ b/src/addons/mod/resource/services/resource.ts @@ -146,23 +146,19 @@ export class AddonModResourceProvider { * Report the resource as being viewed. * * @param id Module ID. - * @param name Name of the resource. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - async logView(id: number, name?: string, siteId?: string): Promise { + async logView(id: number, siteId?: string): Promise { const params: AddonModResourceViewResourceWSParams = { resourceid: id, }; - await CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.log( 'mod_resource_view_resource', params, AddonModResourceProvider.COMPONENT, id, - name, - 'resource', - {}, siteId, ); } diff --git a/src/addons/mod/scorm/components/index/index.ts b/src/addons/mod/scorm/components/index/index.ts index 90e36e1ed..8fca36f6c 100644 --- a/src/addons/mod/scorm/components/index/index.ts +++ b/src/addons/mod/scorm/components/index/index.ts @@ -57,7 +57,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom @Input() autoPlayData?: AddonModScormAutoPlayData; // Data to use to play the SCORM automatically. component = AddonModScormProvider.COMPONENT; - moduleName = 'scorm'; + pluginName = 'scorm'; scorm?: AddonModScormScorm; // The SCORM object. currentOrganization: Partial & { identifier: string} = { @@ -361,7 +361,9 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom return; // Shouldn't happen. } - await AddonModScorm.logView(this.scorm.id, this.scorm.name); + await CoreUtils.ignoreErrors(AddonModScorm.logView(this.scorm.id)); + + this.analyticsLogEvent('mod_scorm_view_scorm'); } /** diff --git a/src/addons/mod/scorm/pages/player/player.ts b/src/addons/mod/scorm/pages/player/player.ts index 5e6e79137..aaa42569a 100644 --- a/src/addons/mod/scorm/pages/player/player.ts +++ b/src/addons/mod/scorm/pages/player/player.ts @@ -35,6 +35,7 @@ import { } from '../../services/scorm'; import { AddonModScormHelper, AddonModScormTOCScoWithIcon } from '../../services/scorm-helper'; import { AddonModScormSync } from '../../services/scorm-sync'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that allows playing a SCORM. @@ -442,8 +443,7 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { this.markCompleted(sco); } - // Trigger SCO launch event. - CoreUtils.ignoreErrors(AddonModScorm.logLaunchSco(this.scorm.id, sco.id, this.scorm.name)); + this.logEvent(sco.id); } /** @@ -581,6 +581,27 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { })); } + /** + * Log event. + */ + protected async logEvent(scoId: number): Promise { + await CoreUtils.ignoreErrors(AddonModScorm.logLaunchSco(this.scorm.id, scoId)); + + let url = '/mod/scorm/player.php'; + if (this.scorm.popup) { + url += `?a=${this.scorm.id}¤torg=${this.organizationId}&scoid=${scoId}` + + `&display=popup&mode=${this.mode}`; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_scorm_get_scorm_user_data', + name: this.scorm.name, + data: { id: this.scorm.id, scoid: scoId, organization: this.organizationId, category: 'scorm' }, + url, + }); + } + /** * @inheritdoc */ diff --git a/src/addons/mod/scorm/services/scorm.ts b/src/addons/mod/scorm/services/scorm.ts index 424aacfab..93e79b536 100644 --- a/src/addons/mod/scorm/services/scorm.ts +++ b/src/addons/mod/scorm/services/scorm.ts @@ -1420,24 +1420,20 @@ export class AddonModScormProvider { * * @param scormId SCORM ID. * @param scoId SCO ID. - * @param name Name of the SCORM. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - logLaunchSco(scormId: number, scoId: number, name?: string, siteId?: string): Promise { + logLaunchSco(scormId: number, scoId: number, siteId?: string): Promise { const params: AddonModScormLaunchScoWSParams = { scormid: scormId, scoid: scoId, }; - return CoreCourseLogHelper.logSingle( + return CoreCourseLogHelper.log( 'mod_scorm_launch_sco', params, AddonModScormProvider.COMPONENT, scormId, - name, - 'scorm', - { scoid: scoId }, siteId, ); } @@ -1446,23 +1442,19 @@ export class AddonModScormProvider { * Report a SCORM as being viewed. * * @param id Module ID. - * @param name Name of the SCORM. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - logView(id: number, name?: string, siteId?: string): Promise { + logView(id: number, siteId?: string): Promise { const params: AddonModScormViewScormWSParams = { scormid: id, }; - return CoreCourseLogHelper.logSingle( + return CoreCourseLogHelper.log( 'mod_scorm_view_scorm', params, AddonModScormProvider.COMPONENT, id, - name, - 'scorm', - {}, siteId, ); } diff --git a/src/addons/mod/survey/components/index/index.ts b/src/addons/mod/survey/components/index/index.ts index 8fa968dda..287830ed8 100644 --- a/src/addons/mod/survey/components/index/index.ts +++ b/src/addons/mod/survey/components/index/index.ts @@ -38,6 +38,7 @@ import { AddonModSurveySyncProvider, AddonModSurveySyncResult, } from '../../services/survey-sync'; +import { CoreUtils } from '@services/utils/utils'; /** * Component that displays a survey. @@ -50,7 +51,7 @@ import { export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit { component = AddonModSurveyProvider.COMPONENT; - moduleName = 'survey'; + pluginName = 'survey'; survey?: AddonModSurveySurvey; questions: AddonModSurveyQuestionFormatted[] = []; @@ -168,7 +169,9 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo return; // Shouldn't happen. } - await AddonModSurvey.logView(this.survey.id, this.survey.name); + await CoreUtils.ignoreErrors(AddonModSurvey.logView(this.survey.id)); + + this.analyticsLogEvent('mod_survey_view_survey'); } /** diff --git a/src/addons/mod/survey/services/survey.ts b/src/addons/mod/survey/services/survey.ts index 7ae3d4f52..4a7202356 100644 --- a/src/addons/mod/survey/services/survey.ts +++ b/src/addons/mod/survey/services/survey.ts @@ -208,7 +208,6 @@ export class AddonModSurveyProvider { * Report the survey as being viewed. * * @param id Module ID. - * @param name Name of the assign. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ @@ -217,14 +216,11 @@ export class AddonModSurveyProvider { surveyid: id, }; - await CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.log( 'mod_survey_view_survey', params, AddonModSurveyProvider.COMPONENT, id, - name, - 'survey', - {}, siteId, ); } diff --git a/src/addons/mod/url/components/index/index.ts b/src/addons/mod/url/components/index/index.ts index de2899f36..0d2cea0c1 100644 --- a/src/addons/mod/url/components/index/index.ts +++ b/src/addons/mod/url/components/index/index.ts @@ -34,6 +34,7 @@ import { AddonModUrlHelper } from '../../services/url-helper'; export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit { component = AddonModUrlProvider.COMPONENT; + pluginName = 'url'; url?: string; name?: string; @@ -153,12 +154,14 @@ export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceCompo */ protected async logView(): Promise { try { - await AddonModUrl.logView(this.module.instance, this.module.name); + await AddonModUrl.logView(this.module.instance); this.checkCompletion(); } catch { // Ignore errors. } + + this.analyticsLogEvent('mod_url_view_url'); } /** diff --git a/src/addons/mod/url/services/handlers/module.ts b/src/addons/mod/url/services/handlers/module.ts index 60c044bf8..c4ea9f52a 100644 --- a/src/addons/mod/url/services/handlers/module.ts +++ b/src/addons/mod/url/services/handlers/module.ts @@ -26,6 +26,7 @@ import { makeSingleton } from '@singletons'; import { AddonModUrlIndexComponent } from '../../components/index/index'; import { AddonModUrl } from '../url'; import { AddonModUrlHelper } from '../url-helper'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Handler to support url modules. @@ -64,14 +65,7 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple * @param courseId The course ID. */ const openUrl = async (module: CoreCourseModuleData, courseId: number): Promise => { - try { - if (module.instance) { - await AddonModUrl.logView(module.instance, module.name); - CoreCourse.checkModuleCompletion(module.course, module.completiondata); - } - } catch { - // Ignore errors. - } + await this.logView(module); CoreCourse.storeModuleViewed(courseId, module.id); @@ -196,5 +190,27 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple return !iconUrl?.startsWith('assets/img/files/'); } + /** + * Log module viewed. + */ + protected async logView(module: CoreCourseModuleData): Promise { + try { + if (module.instance) { + await AddonModUrl.logView(module.instance); + CoreCourse.checkModuleCompletion(module.course, module.completiondata); + } + } catch { + // Ignore errors. + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_url_view_url', + name: module.name, + data: { id: module.instance, category: 'url' }, + url: `/mod/url/view.php?id=${module.id}`, + }); + } + } export const AddonModUrlModuleHandler = makeSingleton(AddonModUrlModuleHandlerService); diff --git a/src/addons/mod/url/services/url.ts b/src/addons/mod/url/services/url.ts index 404138cba..216ac680c 100644 --- a/src/addons/mod/url/services/url.ts +++ b/src/addons/mod/url/services/url.ts @@ -210,23 +210,19 @@ export class AddonModUrlProvider { * Report the url as being viewed. * * @param id Module ID. - * @param name Name of the assign. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - logView(id: number, name?: string, siteId?: string): Promise { + logView(id: number, siteId?: string): Promise { const params: AddonModUrlViewUrlWSParams = { urlid: id, }; - return CoreCourseLogHelper.logSingle( + return CoreCourseLogHelper.log( 'mod_url_view_url', params, AddonModUrlProvider.COMPONENT, id, - name, - 'url', - {}, siteId, ); } diff --git a/src/addons/mod/wiki/components/index/index.ts b/src/addons/mod/wiki/components/index/index.ts index f3d82dc3e..16dc33583 100644 --- a/src/addons/mod/wiki/components/index/index.ts +++ b/src/addons/mod/wiki/components/index/index.ts @@ -75,7 +75,7 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp component = AddonModWikiProvider.COMPONENT; componentId?: number; - moduleName = 'wiki'; + pluginName = 'wiki'; groupWiki = false; isOnline = false; @@ -327,9 +327,7 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp await this.showLoadingAndFetch(true, false); - if (this.currentPage && this.wiki) { - CoreUtils.ignoreErrors(AddonModWiki.logPageView(this.currentPage, this.wiki.id, this.wiki.name)); - } + this.currentPage && this.logPageViewed(this.currentPage); }, CoreSites.getCurrentSiteId()); } @@ -443,12 +441,60 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp return; // Shouldn't happen. } - if (!this.pageId) { - await AddonModWiki.logView(this.wiki.id, this.wiki.name); - } else { + if (this.pageId) { + // View page. this.checkCompletionAfterLog = false; - CoreUtils.ignoreErrors(AddonModWiki.logPageView(this.pageId, this.wiki.id, this.wiki.name)); + await this.logPageViewed(this.pageId); + + return; } + + await AddonModWiki.logView(this.wiki.id); + + if (this.groupId === undefined && this.userId === undefined) { + // View initial page. + this.analyticsLogEvent('mod_wiki_view_wiki', { name: this.currentPageObj?.title }); + + return; + } + + // Viewing a different subwiki. + const hasPersonalSubwikis = this.loadedSubwikis.some(subwiki => subwiki.userid > 0); + const hasGroupSubwikis = this.loadedSubwikis.some(subwiki => subwiki.groupid > 0); + + let url = `/mod/wiki/view.php?wid=${this.wiki.id}&title=${this.wiki.firstpagetitle}`; + if (hasPersonalSubwikis && hasGroupSubwikis) { + url += `&groupanduser=${this.groupId}-${this.userId}`; + } else if (hasPersonalSubwikis) { + url += `&uid=${this.userId}`; + } else { + url += `&group=${this.groupId}`; + } + + this.analyticsLogEvent('mod_wiki_view_wiki', { + name: this.currentPageObj?.title, + data: { subwiki: this.subwikiId, userid: this.userId, groupid: this.groupId }, + url, + }); + } + + /** + * Log page viewed. + * + * @param pageId Page ID. + */ + protected async logPageViewed(pageId: number): Promise { + if (!this.wiki) { + return; // Shouldn't happen. + } + + await CoreUtils.ignoreErrors(AddonModWiki.logPageView(pageId, this.wiki.id)); + + this.analyticsLogEvent('mod_wiki_view_page', { + name: this.currentPageObj?.title, + data: { pageid: this.pageId }, + url: `/mod/wiki/view.php?page=${this.pageId}`, + }); } /** @@ -619,7 +665,9 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp homeView: this.getWikiHomeView(), moduleId: this.module.id, courseId: this.courseId, + selectedId: this.currentPage, selectedTitle: this.currentPageObj && this.currentPageObj.title, + wiki: this.wiki, }, }); diff --git a/src/addons/mod/wiki/components/map/map.ts b/src/addons/mod/wiki/components/map/map.ts index 87cf410a0..9884350b0 100644 --- a/src/addons/mod/wiki/components/map/map.ts +++ b/src/addons/mod/wiki/components/map/map.ts @@ -15,7 +15,8 @@ import { Component, Input, OnInit } from '@angular/core'; import { ModalController } from '@singletons'; import { AddonModWikiPageDBRecord } from '../../services/database/wiki'; -import { AddonModWikiSubwikiPage } from '../../services/wiki'; +import { AddonModWikiSubwikiPage, AddonModWikiWiki } from '../../services/wiki'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Modal to display the map of a Wiki. @@ -27,6 +28,8 @@ import { AddonModWikiSubwikiPage } from '../../services/wiki'; export class AddonModWikiMapModalComponent implements OnInit { @Input() pages: (AddonModWikiSubwikiPage | AddonModWikiPageDBRecord)[] = []; + @Input() wiki?: AddonModWikiWiki; + @Input() selectedId?: number; @Input() selectedTitle?: string; @Input() moduleId?: number; @Input() courseId?: number; @@ -39,6 +42,16 @@ export class AddonModWikiMapModalComponent implements OnInit { */ ngOnInit(): void { this.constructMap(); + + if (this.selectedId && this.wiki) { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_wiki_get_subwiki_pages', + name: this.selectedTitle || this.wiki.name, + data: { id: this.wiki.id, pageid: this.selectedId, category: 'wiki' }, + url: `/mod/wiki/map.php?pageid=${this.selectedId}`, + }); + } } /** diff --git a/src/addons/mod/wiki/pages/edit/edit.ts b/src/addons/mod/wiki/pages/edit/edit.ts index 6c2c1b96a..86bd71e26 100644 --- a/src/addons/mod/wiki/pages/edit/edit.ts +++ b/src/addons/mod/wiki/pages/edit/edit.ts @@ -30,6 +30,7 @@ import { CoreForms } from '@singletons/form'; import { AddonModWiki, AddonModWikiProvider } from '../../services/wiki'; import { AddonModWikiOffline } from '../../services/wiki-offline'; import { AddonModWikiSync } from '../../services/wiki-sync'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that allows adding or editing a wiki page. @@ -64,6 +65,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave { protected editOffline = false; // Whether the user is editing an offline page. protected subwikiFiles: CoreWSFile[] = []; // List of files of the subwiki. protected originalContent?: string; // The original page content. + protected originalTitle?: string; // The original page title. protected version?: number; // Page version. protected renewLockInterval?: number; // An interval to renew the lock every certain time. protected forceLeave = false; // To allow leaving the page without checking for changes. @@ -89,17 +91,17 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave { this.groupId = CoreNavigator.getRouteNumberParam('groupId'); this.userId = CoreNavigator.getRouteNumberParam('userId'); - let pageTitle = CoreNavigator.getRouteParam('pageTitle'); - pageTitle = pageTitle ? CoreTextUtils.cleanTags(pageTitle.replace(/\+/g, ' '), { singleLine: true }) : ''; + const pageTitle = CoreNavigator.getRouteParam('pageTitle'); + this.originalTitle = pageTitle ? CoreTextUtils.cleanTags(pageTitle.replace(/\+/g, ' '), { singleLine: true }) : ''; - this.canEditTitle = !pageTitle; - this.title = pageTitle ? - Translate.instant('addon.mod_wiki.editingpage', { $a: pageTitle }) : + this.canEditTitle = !this.originalTitle; + this.title = this.originalTitle ? + Translate.instant('addon.mod_wiki.editingpage', { $a: this.originalTitle }) : Translate.instant('addon.mod_wiki.newpagehdr'); this.blockId = AddonModWikiSync.getSubwikiBlockId(this.subwikiId, this.wikiId, this.userId, this.groupId); // Create the form group and its controls. - this.pageForm.addControl('title', this.formBuilder.control(pageTitle)); + this.pageForm.addControl('title', this.formBuilder.control(this.originalTitle)); this.pageForm.addControl('text', this.contentControl); // Block the wiki so it cannot be synced. @@ -111,8 +113,8 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave { if (this.section) { this.editorExtraParams.section = this.section; } - } else if (pageTitle) { - this.editorExtraParams.pagetitle = pageTitle; + } else if (this.originalTitle) { + this.editorExtraParams.pagetitle = this.originalTitle; } try { @@ -126,6 +128,8 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave { this.blockId = newBlockId; CoreSync.blockOperation(this.component, this.blockId); } + + this.logView(); } } finally { this.loaded = true; @@ -158,6 +162,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave { this.wikiId = pageContents.wikiid; this.subwikiId = pageContents.subwikiid; this.title = Translate.instant('addon.mod_wiki.editingpage', { $a: pageContents.title }); + this.originalTitle = pageContents.title; this.groupId = pageContents.groupid; this.userId = pageContents.userid; canEdit = pageContents.caneditpage; @@ -466,6 +471,34 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave { } } + /** + * Log view. + */ + protected logView(): void { + let url: string; + if (this.pageId) { + url = `/mod/wiki/edit.php?pageid=${this.pageId}` + + (this.section ? `§ion=${this.section.replace(/ /g, '+')}` : ''); + } else if (this.originalTitle) { + const title = this.originalTitle.replace(/ /g, '+'); + if (this.subwikiId) { + url = `/mod/wiki/create.php?swid=${this.subwikiId}&title=${title}&action=new`; + } else { + url = `/mod/wiki/create.php?wid=${this.wikiId}&group=${this.groupId ?? 0}&uid=${this.userId ?? 0}&title=${title}`; + } + } else { + url = `/mod/wiki/create.php?action=new&wid=${this.wikiId}&swid=${this.subwikiId}`; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: this.pageId ? 'mod_wiki_edit_page' : 'mod_wiki_new_page', + name: this.originalTitle ?? Translate.instant('addon.mod_wiki.newpagehdr'), + data: { id: this.wikiId, subwiki: this.subwikiId, category: 'wiki' }, + url, + }); + } + /** * @inheritdoc */ diff --git a/src/addons/mod/wiki/services/wiki.ts b/src/addons/mod/wiki/services/wiki.ts index 62932720c..dc79309eb 100644 --- a/src/addons/mod/wiki/services/wiki.ts +++ b/src/addons/mod/wiki/services/wiki.ts @@ -621,23 +621,19 @@ export class AddonModWikiProvider { * * @param id Page ID. * @param wikiId Wiki ID. - * @param name Name of the wiki. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - logPageView(id: number, wikiId: number, name?: string, siteId?: string): Promise { + logPageView(id: number, wikiId: number, siteId?: string): Promise { const params: AddonModWikiViewPageWSParams = { pageid: id, }; - return CoreCourseLogHelper.logSingle( + return CoreCourseLogHelper.log( 'mod_wiki_view_page', params, AddonModWikiProvider.COMPONENT, wikiId, - name, - 'wiki', - params, siteId, ); } @@ -646,23 +642,19 @@ export class AddonModWikiProvider { * Report the wiki as being viewed. * * @param id Wiki ID. - * @param name Name of the wiki. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - logView(id: number, name?: string, siteId?: string): Promise { + logView(id: number, siteId?: string): Promise { const params: AddonModWikiViewWikiWSParams = { wikiid: id, }; - return CoreCourseLogHelper.logSingle( + return CoreCourseLogHelper.log( 'mod_wiki_view_wiki', params, AddonModWikiProvider.COMPONENT, id, - name, - 'wiki', - {}, siteId, ); } diff --git a/src/addons/mod/workshop/components/index/index.ts b/src/addons/mod/workshop/components/index/index.ts index cfed3a329..8b7c54776 100644 --- a/src/addons/mod/workshop/components/index/index.ts +++ b/src/addons/mod/workshop/components/index/index.ts @@ -66,7 +66,7 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity @Input() group = 0; component = AddonModWorkshopProvider.COMPONENT; - moduleName = 'workshop'; + pluginName = 'workshop'; workshop?: AddonModWorkshopData; page = 0; @@ -255,7 +255,9 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity return; // Shouldn't happen. } - await AddonModWorkshop.logView(this.workshop.id, this.workshop.name); + await CoreUtils.ignoreErrors(AddonModWorkshop.logView(this.workshop.id)); + + this.analyticsLogEvent('mod_workshop_view_workshop'); } /** diff --git a/src/addons/mod/workshop/pages/assessment/assessment.ts b/src/addons/mod/workshop/pages/assessment/assessment.ts index 961591bac..973a16a03 100644 --- a/src/addons/mod/workshop/pages/assessment/assessment.ts +++ b/src/addons/mod/workshop/pages/assessment/assessment.ts @@ -39,6 +39,8 @@ import { import { AddonModWorkshopHelper, AddonModWorkshopSubmissionAssessmentWithFormData } from '../../services/workshop-helper'; import { AddonModWorkshopOffline } from '../../services/workshop-offline'; import { AddonModWorkshopSyncProvider } from '../../services/workshop-sync'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays a workshop assessment. @@ -89,6 +91,7 @@ export class AddonModWorkshopAssessmentPage implements OnInit, OnDestroy, CanLea protected siteId: string; protected currentUserId: number; protected forceLeave = false; + protected logView: () => void; constructor( protected fb: FormBuilder, @@ -111,6 +114,20 @@ export class AddonModWorkshopAssessmentPage implements OnInit, OnDestroy, CanLea this.refreshAllData(); } }, this.siteId); + + this.logView = CoreTime.once(async () => { + if (!this.workshop) { + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_workshop_get_assessment', + name: this.workshop.name, + data: { id: this.workshop.id, assessmentid: this.assessment.id, category: 'workshop' }, + url: `/mod/workshop/assessment.php?asid=${this.assessment.id}`, + }); + }); } /** diff --git a/src/addons/mod/workshop/pages/edit-submission/edit-submission.ts b/src/addons/mod/workshop/pages/edit-submission/edit-submission.ts index d83fd9d4c..4c7ea2d08 100644 --- a/src/addons/mod/workshop/pages/edit-submission/edit-submission.ts +++ b/src/addons/mod/workshop/pages/edit-submission/edit-submission.ts @@ -40,6 +40,7 @@ import { } from '../../services/workshop'; import { AddonModWorkshopHelper, AddonModWorkshopSubmissionDataWithOfflineData } from '../../services/workshop-helper'; import { AddonModWorkshopOffline } from '../../services/workshop-offline'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays the workshop edit submission. @@ -224,6 +225,8 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy, Ca } this.loaded = true; + + this.logView(); } catch (error) { this.loaded = false; @@ -233,6 +236,23 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy, Ca } } + /** + * Log view. + */ + protected logView(): void { + if (!this.workshop) { + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: this.editing ? 'mod_workshop_update_submission' : 'mod_workshop_add_submission', + name: this.workshop.name, + data: { id: this.workshop.id, submissionid: this.submissionId, category: 'workshop' }, + url: `/mod/workshop/submission.php?cmid=${this.module.id}&id=${this.submissionId}&edit=on`, + }); + } + /** * Force leaving the page, without checking for changes. */ diff --git a/src/addons/mod/workshop/pages/submission/submission.ts b/src/addons/mod/workshop/pages/submission/submission.ts index 711c20fb5..faf95933b 100644 --- a/src/addons/mod/workshop/pages/submission/submission.ts +++ b/src/addons/mod/workshop/pages/submission/submission.ts @@ -47,6 +47,8 @@ import { } from '../../services/workshop-helper'; import { AddonModWorkshopOffline } from '../../services/workshop-offline'; import { AddonModWorkshopSyncProvider, AddonModWorkshopAutoSyncData } from '../../services/workshop-sync'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreTime } from '@singletons/time'; /** * Page that displays a workshop submission. @@ -102,7 +104,7 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLea protected obsAssessmentSaved: CoreEventObserver; protected syncObserver: CoreEventObserver; protected isDestroyed = false; - protected fetchSuccess = false; + protected logView: () => void; constructor( protected fb: FormBuilder, @@ -125,6 +127,8 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLea // Update just when all database is synced. this.eventReceived(data); }, this.siteId); + + this.logView = CoreTime.once(() => this.performLogView()); } /** @@ -599,19 +603,21 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLea /** * Log submission viewed. */ - protected async logView(): Promise { - if (this.fetchSuccess) { - return; // Already done. - } - - this.fetchSuccess = true; - + protected async performLogView(): Promise { try { - await AddonModWorkshop.logViewSubmission(this.submissionId, this.workshopId, this.workshop.name); + await AddonModWorkshop.logViewSubmission(this.submissionId, this.workshopId); CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata); } catch { // Ignore errors. } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'mod_workshop_view_submission', + name: this.workshop.name, + data: { id: this.workshop.id, submissionid: this.submissionId, category: 'workshop' }, + url: `/mod/workshop/submission.php?cmid=${this.module.id}&id=${this.submissionId}`, + }); } /** diff --git a/src/addons/mod/workshop/services/workshop.ts b/src/addons/mod/workshop/services/workshop.ts index 26c14809d..a70f23a55 100644 --- a/src/addons/mod/workshop/services/workshop.ts +++ b/src/addons/mod/workshop/services/workshop.ts @@ -1443,23 +1443,19 @@ export class AddonModWorkshopProvider { * Report the workshop as being viewed. * * @param id Workshop ID. - * @param name Name of the workshop. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - async logView(id: number, name?: string, siteId?: string): Promise { + async logView(id: number, siteId?: string): Promise { const params: AddonModWorkshopViewWorkshopWSParams = { workshopid: id, }; - await CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.log( 'mod_workshop_view_workshop', params, AddonModWorkshopProvider.COMPONENT, id, - name, - 'workshop', - {}, siteId, ); } @@ -1469,23 +1465,19 @@ export class AddonModWorkshopProvider { * * @param id Submission ID. * @param workshopId Workshop ID. - * @param name Name of the workshop. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the WS call is successful. */ - async logViewSubmission(id: number, workshopId: number, name?: string, siteId?: string): Promise { + async logViewSubmission(id: number, workshopId: number, siteId?: string): Promise { const params: AddonModWorkshopViewSubmissionWSParams = { submissionid: id, }; - await CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.log( 'mod_workshop_view_submission', params, AddonModWorkshopProvider.COMPONENT, workshopId, - name, - 'workshop', - params, siteId, ); } diff --git a/src/addons/notes/pages/list/list.ts b/src/addons/notes/pages/list/list.ts index 1369ec308..58bf0e68e 100644 --- a/src/addons/notes/pages/list/list.ts +++ b/src/addons/notes/pages/list/list.ts @@ -21,12 +21,16 @@ import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { CoreAnimations } from '@components/animations'; import { CoreUser, CoreUserProfile } from '@features/user/services/user'; import { IonContent, IonRefresher } from '@ionic/angular'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { CoreDomUtils, ToastDuration } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; +import { CoreUrlUtils } from '@services/utils/url'; import { CoreUtils } from '@services/utils/utils'; +import { Translate } from '@singletons'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; +import { CoreTime } from '@singletons/time'; /** * Page that displays a list of notes. @@ -54,9 +58,11 @@ export class AddonNotesListPage implements OnInit, OnDestroy { currentUserId!: number; protected syncObserver!: CoreEventObserver; - protected logAfterFetch = true; + protected logView: () => void; constructor() { + this.logView = CoreTime.once(() => this.performLogView()); + try { this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); this.userId = CoreNavigator.getRouteNumberParam('userId'); @@ -128,10 +134,7 @@ export class AddonNotesListPage implements OnInit, OnDestroy { this.notes = await AddonNotes.getNotesUserData(notesList); } - if (this.logAfterFetch) { - this.logAfterFetch = false; - CoreUtils.ignoreErrors(AddonNotes.logView(this.courseId, this.userId)); - } + this.logView(); } catch (error) { CoreDomUtils.showErrorModal(error); } finally { @@ -176,7 +179,6 @@ export class AddonNotesListPage implements OnInit, OnDestroy { this.notesLoaded = false; this.refreshIcon = CoreConstants.ICON_LOADING; this.syncIcon = CoreConstants.ICON_LOADING; - this.logAfterFetch = true; await this.fetchNotes(true); } @@ -190,6 +192,8 @@ export class AddonNotesListPage implements OnInit, OnDestroy { e.preventDefault(); e.stopPropagation(); + this.logViewAdd(); + const modalData = await CoreDomUtils.openModal({ component: AddonNotesAddComponent, componentProps: { @@ -225,6 +229,8 @@ export class AddonNotesListPage implements OnInit, OnDestroy { e.stopPropagation(); try { + this.logViewDelete(note); + await CoreDomUtils.showDeleteConfirm('addon.notes.deleteconfirm'); try { await AddonNotes.deleteNote(note, this.courseId); @@ -294,6 +300,58 @@ export class AddonNotesListPage implements OnInit, OnDestroy { } } + /** + * Log view. + */ + protected async performLogView(): Promise { + await CoreUtils.ignoreErrors(AddonNotes.logView(this.courseId, this.userId)); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_notes_view_notes', + name: Translate.instant('addon.notes.notes'), + data: { courseid: this.courseId, userid: this.userId || 0, category: 'notes' }, + url: CoreUrlUtils.addParamsToUrl('/notes/index.php', { + user: this.userId, + course: this.courseId !== CoreSites.getCurrentSiteHomeId() ? this.courseId : undefined, + }), + }); + } + + /** + * Log view. + */ + protected async logViewAdd(): Promise { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'core_notes_create_notes', + name: Translate.instant('addon.notes.notes'), + data: { courseid: this.courseId, userid: this.userId || 0, category: 'notes' }, + url: CoreUrlUtils.addParamsToUrl('/notes/edit.php', { + courseid: this.courseId, + userid: this.userId, + publishstate: this.type === 'personal' ? 'draft' : (this.type === 'course' ? 'public' : 'site'), + }), + }); + } + + /** + * Log view. + */ + protected async logViewDelete(note: AddonNotesNoteFormatted): Promise { + if (!note.id) { + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'core_notes_delete_notes', + name: Translate.instant('addon.notes.notes'), + data: { id: note.id, category: 'notes' }, + url: `/notes/delete.php?id=${note.id}`, + }); + } + /** * Page destroyed. */ diff --git a/src/addons/notes/services/notes.ts b/src/addons/notes/services/notes.ts index 71e4d3c06..0c2cfd8db 100644 --- a/src/addons/notes/services/notes.ts +++ b/src/addons/notes/services/notes.ts @@ -15,7 +15,6 @@ import { Injectable } from '@angular/core'; import { CoreWSError } from '@classes/errors/wserror'; import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; -import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; import { CoreUser } from '@features/user/services/user'; import { CoreNetwork } from '@services/network'; import { CoreSites } from '@services/sites'; @@ -414,8 +413,6 @@ export class AddonNotesProvider { userid: userId || 0, }; - CorePushNotifications.logViewListEvent('notes', 'core_notes_view_notes', params, site.getId()); - await site.write('core_notes_view_notes', params); } diff --git a/src/addons/notifications/pages/notification/notification.ts b/src/addons/notifications/pages/notification/notification.ts index 7a5b4edb8..a8f6258df 100644 --- a/src/addons/notifications/pages/notification/notification.ts +++ b/src/addons/notifications/pages/notification/notification.ts @@ -24,9 +24,11 @@ import { ActivatedRouteSnapshot } from '@angular/router'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; import { CoreContentLinksAction, CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; +import { Translate } from '@singletons'; /** * Page to render a notification. @@ -72,6 +74,16 @@ export class AddonNotificationsNotificationPage implements OnInit, OnDestroy { AddonNotificationsHelper.markNotificationAsRead(notification); this.loaded = true; + + if (notification.id) { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'core_message_get_messages', + name: Translate.instant('addon.notifications.notifications'), + data: { id: notification.id, category: 'notifications' }, + url: `/message/output/popup/notifications.php?notificationid=${notification.id}&offset=0`, + }); + } } /** diff --git a/src/addons/notifications/pages/settings/settings.ts b/src/addons/notifications/pages/settings/settings.ts index b41f96611..f0c7a5d56 100644 --- a/src/addons/notifications/pages/settings/settings.ts +++ b/src/addons/notifications/pages/settings/settings.ts @@ -37,6 +37,9 @@ import { AddonNotificationsPreferencesProcessorFormatted, } from '@addons/notifications/services/notifications-helper'; import { CoreNavigator } from '@services/navigator'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { Translate } from '@singletons'; /** * Page that displays notifications settings. @@ -58,12 +61,23 @@ export class AddonNotificationsSettingsPage implements OnInit, OnDestroy { loggedInOffLegacyMode = false; protected updateTimeout?: number; + protected logView: () => void; constructor() { this.canChangeSound = CoreLocalNotifications.canDisableSound(); const currentSite = CoreSites.getRequiredCurrentSite(); this.loggedInOffLegacyMode = !currentSite.isVersionGreaterEqualThan('4.0'); + + this.logView = CoreTime.once(async () => { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_message_get_user_notification_preferences', + name: Translate.instant('addon.notifications.notificationpreferences'), + data: { category: 'notifications' }, + url: '/message/notificationpreferences.php', + }); + }); } /** @@ -100,6 +114,8 @@ export class AddonNotificationsSettingsPage implements OnInit, OnDestroy { preferences.enableall = !preferences.disableall; this.preferences = AddonNotificationsHelper.formatPreferences(preferences); this.loadProcessor(currentProcessor); + + this.logView(); } catch (error) { CoreDomUtils.showErrorModal(error); } finally { diff --git a/src/addons/privatefiles/pages/index/index.ts b/src/addons/privatefiles/pages/index/index.ts index 6cf24b9f7..4eca6fd5f 100644 --- a/src/addons/privatefiles/pages/index/index.ts +++ b/src/addons/privatefiles/pages/index/index.ts @@ -32,6 +32,8 @@ import { import { AddonPrivateFilesHelper } from '@addons/privatefiles/services/privatefiles-helper'; import { CoreUtils } from '@services/utils/utils'; import { CoreNavigator } from '@services/navigator'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays the list of files. @@ -57,12 +59,23 @@ export class AddonPrivateFilesIndexPage implements OnInit, OnDestroy { filesLoaded = false; // Whether the files are loaded. protected updateSiteObserver: CoreEventObserver; + protected logView: () => void; constructor() { // Update visibility if current site info is updated. this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => { this.setVisibility(); }, CoreSites.getCurrentSiteId()); + + this.logView = CoreTime.once(async () => { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_files_get_files', + name: Translate.instant('addon.privatefiles.files'), + data: { category: 'files' }, + url: '/user/files.php', + }); + }); } /** @@ -208,6 +221,8 @@ export class AddonPrivateFilesIndexPage implements OnInit, OnDestroy { // User quota isn't useful, delete it. delete this.userQuota; } + + this.logView(); } else { // Unknown root. CoreDomUtils.showErrorModal('addon.privatefiles.couldnotloadfiles', true); diff --git a/src/core/classes/delegate.ts b/src/core/classes/delegate.ts index 799f0cc17..adc2023f0 100644 --- a/src/core/classes/delegate.ts +++ b/src/core/classes/delegate.ts @@ -207,6 +207,15 @@ export class CoreDelegate { return enabled ? this.enabledHandlers[name] !== undefined : this.handlers[name] !== undefined; } + /** + * Check if the delegate has at least 1 registered handler (not necessarily enabled). + * + * @returns If there is at least 1 handler. + */ + hasHandlers(): boolean { + return Object.keys(this.handlers).length > 0; + } + /** * Check if a time belongs to the last update handlers call. * This is to handle the cases where updateHandlers don't finish in the same order as they're called. diff --git a/src/core/classes/items-management/list-items-manager.ts b/src/core/classes/items-management/list-items-manager.ts index 83c466882..3ec31d484 100644 --- a/src/core/classes/items-management/list-items-manager.ts +++ b/src/core/classes/items-management/list-items-manager.ts @@ -23,6 +23,7 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreRoutedItemsManagerSource } from './routed-items-manager-source'; import { CoreRoutedItemsManager } from './routed-items-manager'; import { CoreDom } from '@singletons/dom'; +import { CoreTime } from '@singletons/time'; /** * Helper class to manage the state and routing of a list of items in a page. @@ -35,7 +36,7 @@ export class CoreListItemsManager< protected pageRouteLocator?: unknown | ActivatedRoute; protected splitView?: CoreSplitViewComponent; protected splitViewOutletSubscription?: Subscription; - protected fetchSuccess = false; // Whether a fetch was finished successfully. + protected finishSuccessfulFetch: () => void; constructor(source: Source, pageRouteLocator: unknown | ActivatedRoute) { super(source); @@ -44,6 +45,7 @@ export class CoreListItemsManager< this.pageRouteLocator = pageRouteLocator; this.addListener({ onSelectedItemUpdated: debouncedScrollToCurrentElement }); + this.finishSuccessfulFetch = CoreTime.once(() => CoreUtils.ignoreErrors(this.logActivity())); } get items(): Item[] { @@ -160,19 +162,6 @@ export class CoreListItemsManager< this.finishSuccessfulFetch(); } - /** - * Finish a successful fetch. - */ - protected async finishSuccessfulFetch(): Promise { - if (this.fetchSuccess) { - return; // Already treated. - } - - // Log activity. - this.fetchSuccess = true; - await CoreUtils.ignoreErrors(this.logActivity()); - } - /** * Log activity when the page starts. */ diff --git a/src/core/classes/site.ts b/src/core/classes/site.ts index 0fe84afb1..c5339634b 100644 --- a/src/core/classes/site.ts +++ b/src/core/classes/site.ts @@ -33,7 +33,7 @@ import { import { CoreDomUtils, ToastDuration } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; import { CoreTimeUtils } from '@services/utils/time'; -import { CoreUrlUtils, CoreUrlParams } from '@services/utils/url'; +import { CoreUrlUtils } from '@services/utils/url'; import { CoreUtils, CoreUtilsOpenInBrowserOptions } from '@services/utils/utils'; import { CoreConstants } from '@/core/constants'; import { SQLiteDB } from '@classes/sqlitedb'; @@ -63,6 +63,7 @@ import { firstValueFrom } from '../utils/rxjs'; import { CoreSiteError } from '@classes/errors/siteerror'; import { CoreUserAuthenticatedSupportConfig } from '@features/user/classes/support/authenticated-support-config'; import { CoreLoginHelper } from '@features/login/services/login-helper'; +import { CorePath } from '@singletons/path'; /** * QR Code type enumeration. @@ -1598,8 +1599,8 @@ export class CoreSite { * @param anchor Anchor text if needed. * @returns URL with params. */ - createSiteUrl(path: string, params?: CoreUrlParams, anchor?: string): string { - return CoreUrlUtils.addParamsToUrl(this.siteUrl + path, params, anchor); + createSiteUrl(path: string, params?: Record, anchor?: string): string { + return CoreUrlUtils.addParamsToUrl(CorePath.concatenatePaths(this.siteUrl, path), params, anchor); } /** @@ -1891,12 +1892,12 @@ export class CoreSite { options.showBrowserWarning = false; // A warning already shown, no need to show another. } + options.originalUrl = url; + // Open the URL. if (inApp) { return CoreUtils.openInApp(autoLoginUrl, options); } else { - options.browserWarningUrl = url; - return CoreUtils.openInBrowser(autoLoginUrl, options); } } diff --git a/src/core/features/course/classes/main-activity-component.ts b/src/core/features/course/classes/main-activity-component.ts index e2a8d20fd..02f2e8127 100644 --- a/src/core/features/course/classes/main-activity-component.ts +++ b/src/core/features/course/classes/main-activity-component.ts @@ -34,7 +34,7 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR @Input() group?: number; // Group ID the component belongs to. - moduleName?: string; // Raw module name to be translated. It will be translated on init. + moduleName = ''; // Translated module name. Calculated from pluginName. protected syncObserver?: CoreEventObserver; // It will observe the sync auto event. protected syncEventName?: string; // Auto sync event name. @@ -54,7 +54,7 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR await super.ngOnInit(); this.hasOffline = false; - this.moduleName = CoreCourse.translateModuleName(this.moduleName || ''); + this.moduleName = CoreCourse.translateModuleName(this.pluginName || this.moduleName || ''); if (this.syncEventName) { // Refresh data if this discussion is synchronized automatically. diff --git a/src/core/features/course/classes/main-resource-component.ts b/src/core/features/course/classes/main-resource-component.ts index 5944a2a3b..0e3761b1e 100644 --- a/src/core/features/course/classes/main-resource-component.ts +++ b/src/core/features/course/classes/main-resource-component.ts @@ -31,6 +31,9 @@ import { CoreCourse } from '../services/course'; import { CoreCourseHelper, CoreCourseModuleData } from '../services/course-helper'; import { CoreCourseModuleDelegate, CoreCourseModuleMainComponent } from '../services/module-delegate'; import { CoreCourseModulePrefetchDelegate } from '../services/module-prefetch-delegate'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreUrlUtils } from '@services/utils/url'; +import { CoreTime } from '@singletons/time'; /** * Result of a resource download. @@ -56,8 +59,8 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, component?: string; // Component name. componentId?: number; // Component ID. hasOffline = false; // Resources don't have any data to sync. - description?: string; // Module description. + pluginName?: string; // The plugin name without "mod_", e.g. assign or book. protected fetchContentDefaultError = 'core.course.errorgetmodule'; // Default error to show when loading contents. protected isCurrentView = false; // Whether the component is in the current view. @@ -72,14 +75,16 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, protected showCompletion = false; // Whether to show completion inside the activity. protected displayDescription = true; // Wether to show Module description on module page, and not on summary or the contrary. protected isDestroyed = false; // Whether the component is destroyed. - protected fetchSuccess = false; // Whether a fetch was finished successfully. protected checkCompletionAfterLog = true; // Whether to check if completion has changed after calling logActivity. + protected finishSuccessfulFetch: () => void; constructor( @Optional() @Inject('') loggerName: string = 'CoreCourseModuleMainResourceComponent', protected courseContentsPage?: CoreCourseContentsPage, ) { this.logger = CoreLogger.getInstance(loggerName); + + this.finishSuccessfulFetch = CoreTime.once(() => this.performFinishSuccessfulFetch()); } /** @@ -447,16 +452,11 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, } /** - * Finish a successful fetch. + * Finish first successful fetch. * * @returns Promise resolved when done. */ - protected async finishSuccessfulFetch(): Promise { - if (this.fetchSuccess) { - return; // Already treated. - } - - this.fetchSuccess = true; + protected async performFinishSuccessfulFetch(): Promise { this.storeModuleViewed(); // Log activity now. @@ -489,6 +489,36 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, // To be overridden. } + /** + * Log activity view in analytics. + * + * @param wsName Name of the WS used. + * @param data Other data to send. + * @returns Promise resolved when done. + */ + async analyticsLogEvent( + wsName: string, + options: AnalyticsLogEventOptions = {}, + ): Promise { + let url: string | undefined; + if (options.sendUrl === true || options.sendUrl === undefined) { + if (typeof options.url === 'string') { + url = options.url; + } else if (this.pluginName) { + // Use default value. + url = CoreUrlUtils.addParamsToUrl(`/mod/${this.pluginName}/view.php?id=${this.module.id}`, options.data); + } + } + + await CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: wsName, + name: options.name || this.module.name, + data: { id: this.module.instance, category: this.pluginName, ...options.data }, + url, + }); + } + /** * Check the module completion. */ @@ -534,3 +564,10 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, } } + +type AnalyticsLogEventOptions = { + data?: Record; // Other data to send. + name?: string; // Name to send, defaults to activity name. + url?: string; // URL to use. If not set and sendUrl is true, a default value will be used. + sendUrl?: boolean; // Whether to pass a URL to analytics. Defaults to true. +}; diff --git a/src/core/features/course/components/course-format/course-format.ts b/src/core/features/course/components/course-format/course-format.ts index 697cdd331..381c6040a 100644 --- a/src/core/features/course/components/course-format/course-format.ts +++ b/src/core/features/course/components/course-format/course-format.ts @@ -50,6 +50,7 @@ import { CoreUserToursAlignment, CoreUserToursSide } from '@features/usertours/s import { CoreCourseCourseIndexTourComponent } from '../course-index-tour/course-index-tour'; import { CoreDom } from '@singletons/dom'; import { CoreUserTourDirectiveOptions } from '@directives/user-tour'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Component to display course contents using a certain format. If the format isn't found, use default one. @@ -518,9 +519,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { this.content.scrollToTop(0); } - CoreUtils.ignoreErrors( - CoreCourse.logView(this.course.id, newSection.section, undefined, this.course.fullname), - ); + this.logView(newSection.section, !previousValue); } this.changeDetectorRef.markForCheck(); } @@ -655,6 +654,37 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { return CoreCourseHelper.canUserViewSection(section) && !CoreCourseHelper.isSectionStealth(section); } + /** + * Log view. + * + * @param sectionNumber Section loaded (if any). + * @param firstLoad Whether it's the first load when opening the course. + */ + async logView(sectionNumber?: number, firstLoad = false): Promise { + await CoreUtils.ignoreErrors( + CoreCourse.logView(this.course.id, sectionNumber), + ); + + let extraParams = sectionNumber ? `§ion=${sectionNumber}` : ''; + if (firstLoad && sectionNumber) { + // If course is configured to show all sections in one page, don't include section in URL in first load. + const courseDisplay = 'courseformatoptions' in this.course && + this.course.courseformatoptions?.find(option => option.name === 'coursedisplay'); + + if (!courseDisplay || Number(courseDisplay.value) !== 0) { + extraParams = ''; + } + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'core_course_view_course', + name: this.course.fullname, + data: { id: this.course.id, sectionnumber: sectionNumber, category: 'course' }, + url: `/course/view.php?id=${this.course.id}${extraParams}`, + }); + } + } type CoreCourseSectionToDisplay = CoreCourseSection & { diff --git a/src/core/features/course/pages/course-summary/course-summary.page.ts b/src/core/features/course/pages/course-summary/course-summary.page.ts index b510fc6b3..8185b5989 100644 --- a/src/core/features/course/pages/course-summary/course-summary.page.ts +++ b/src/core/features/course/pages/course-summary/course-summary.page.ts @@ -41,6 +41,8 @@ import { CorePromisedValue } from '@classes/promised-value'; import { CorePlatform } from '@services/platform'; import { CoreCourse } from '@features/course/services/course'; import { CorePasswordModalResponse } from '@components/password-modal/password-modal'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; const ENROL_BROWSER_METHODS = ['fee', 'paypal']; @@ -81,6 +83,7 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy { protected courseStatusObserver?: CoreEventObserver; protected appResumeSubscription: Subscription; protected waitingForBrowserEnrol = false; + protected logView: () => void; constructor() { // Refresh the view when the app is resumed. @@ -96,6 +99,20 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy { await this.refreshData(); }); }); + + this.logView = CoreTime.once(async () => { + if (!this.course || this.isModal) { + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'core_course_get_courses', + name: this.course.fullname, + data: { id: this.course.id, category: 'course' }, + url: `/enrol/index.php?id=${this.course.id}`, + }); + }); } /** @@ -161,6 +178,8 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy { this.getCourseData(), this.loadCourseExtraData(), ]); + + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error getting enrolment data'); } diff --git a/src/core/features/course/pages/list-mod-type/list-mod-type.ts b/src/core/features/course/pages/list-mod-type/list-mod-type.ts index e0eb86ae3..c27314dcc 100644 --- a/src/core/features/course/pages/list-mod-type/list-mod-type.ts +++ b/src/core/features/course/pages/list-mod-type/list-mod-type.ts @@ -22,6 +22,8 @@ import { CoreNavigator } from '@services/navigator'; import { CoreConstants } from '@/core/constants'; import { IonRefresher } from '@ionic/angular'; import { CoreUtils } from '@services/utils/utils'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays all modules of a certain type in a course. @@ -39,6 +41,24 @@ export class CoreCourseListModTypePage implements OnInit { protected modName?: string; protected archetypes: Record = {}; // To speed up the check of modules. + protected logView: () => void; + + constructor() { + this.logView = CoreTime.once(async () => { + if (!this.modName) { + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_course_get_contents', + name: this.title, + data: { category: this.modName }, + url: (this.modName === 'resources' ? '/course/resources.php' : `/mod/${this.modName}/index.php`) + + `?id=${this.courseId}`, + }); + }); + } /** * @inheritdoc diff --git a/src/core/features/course/services/course.ts b/src/core/features/course/services/course.ts index f7506171e..6788eb699 100644 --- a/src/core/features/course/services/course.ts +++ b/src/core/features/course/services/course.ts @@ -38,7 +38,6 @@ import { } from '../../courses/services/courses'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreWSError } from '@classes/errors/wserror'; -import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; import { CoreCourseHelper, CoreCourseModuleData, CoreCourseModuleCompletionData } from './course-helper'; import { CoreCourseFormatDelegate } from './format-delegate'; import { CoreCronDelegate } from '@services/cron'; @@ -1191,22 +1190,19 @@ export class CoreCourseProvider { * @param courseId Course ID. * @param sectionNumber Section number. * @param siteId Site ID. If not defined, current site. - * @param name Name of the course. * @returns Promise resolved when the WS call is successful. */ - async logView(courseId: number, sectionNumber?: number, siteId?: string, name?: string): Promise { + async logView(courseId: number, sectionNumber?: number, siteId?: string): Promise { const params: CoreCourseViewCourseWSParams = { courseid: courseId, }; - const wsName = 'core_course_view_course'; if (sectionNumber !== undefined) { params.sectionnumber = sectionNumber; } const site = await CoreSites.getSite(siteId); - CorePushNotifications.logViewEvent(courseId, name, 'course', wsName, { sectionnumber: sectionNumber }, siteId); - const response: CoreStatusWithWarningsWSResponse = await site.write(wsName, params); + const response: CoreStatusWithWarningsWSResponse = await site.write('core_course_view_course', params); if (!response.status) { throw Error('WS core_course_view_course failed.'); diff --git a/src/core/features/course/services/handlers/log-cron.ts b/src/core/features/course/services/handlers/log-cron.ts index fe87cd189..f9653eeeb 100644 --- a/src/core/features/course/services/handlers/log-cron.ts +++ b/src/core/features/course/services/handlers/log-cron.ts @@ -44,7 +44,7 @@ export class CoreCourseLogCronHandlerService implements CoreCronHandler { const site = await CoreSites.getSite(siteId); - return CoreCourse.logView(site.getSiteHomeId(), undefined, site.getId(), site.getInfo()?.sitename); + return CoreCourse.logView(site.getSiteHomeId(), undefined, site.getId()); } /** diff --git a/src/core/features/course/services/log-helper.ts b/src/core/features/course/services/log-helper.ts index 83b2ac5be..052149036 100644 --- a/src/core/features/course/services/log-helper.ts +++ b/src/core/features/course/services/log-helper.ts @@ -19,7 +19,6 @@ import { CoreSites } from '@services/sites'; import { CoreTextUtils } from '@services/utils/text'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreUtils } from '@services/utils/utils'; -import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; import { makeSingleton } from '@singletons'; import { ACTIVITY_LOG_TABLE, CoreCourseActivityLogDBRecord } from './database/log'; import { CoreStatusWithWarningsWSResponse } from '@services/ws'; @@ -190,7 +189,6 @@ export class CoreCourseLogHelperProvider { /** * Perform log online. Data will be saved offline for syncing. - * It also triggers a Firebase view_item event. * * @param ws WS name. * @param data Data to send to the WS. @@ -198,9 +196,10 @@ export class CoreCourseLogHelperProvider { * @param componentId Component ID. * @param name Name of the viewed item. * @param category Category of the viewed item. - * @param eventData Data to pass to the Firebase event. + * @param eventData Data to pass to the analytics event. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when done. + * @deprecated since 4.3. Please use CoreCourseLogHelper.log instead. */ logSingle( ws: string, @@ -209,26 +208,24 @@ export class CoreCourseLogHelperProvider { componentId: number, name?: string, category?: string, - eventData?: Record, + eventData?: Record, siteId?: string, ): Promise { - CorePushNotifications.logViewEvent(componentId, name, category, ws, eventData, siteId); - return this.log(ws, data, component, componentId, siteId); } /** * Perform log online. Data will be saved offline for syncing. - * It also triggers a Firebase view_item_list event. * * @param ws WS name. * @param data Data to send to the WS. * @param component Component name. * @param componentId Component ID. * @param category Category of the viewed item. - * @param eventData Data to pass to the Firebase event. + * @param eventData Data to pass to the analytics event. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when done. + * @deprecated since 4.3. Please use CoreCourseLogHelper.log instead. */ logList( ws: string, @@ -236,11 +233,9 @@ export class CoreCourseLogHelperProvider { component: string, componentId: number, category: string, - eventData?: Record, + eventData?: Record, siteId?: string, ): Promise { - CorePushNotifications.logViewListEvent(category, ws, eventData, siteId); - return this.log(ws, data, component, componentId, siteId); } diff --git a/src/core/features/courses/pages/categories/categories.ts b/src/core/features/courses/pages/categories/categories.ts index 903c19012..f081616d8 100644 --- a/src/core/features/courses/pages/categories/categories.ts +++ b/src/core/features/courses/pages/categories/categories.ts @@ -21,6 +21,8 @@ import { CoreCategoryData, CoreCourseListItem, CoreCourses, CoreCoursesProvider import { Translate } from '@singletons'; import { CoreNavigator } from '@services/navigator'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreTime } from '@singletons/time'; /** * Page that displays a list of categories and the courses in the current category if any. @@ -50,6 +52,7 @@ export class CoreCoursesCategoriesPage implements OnInit, OnDestroy { protected siteUpdatedObserver: CoreEventObserver; protected downloadEnabledObserver: CoreEventObserver; protected isDestroyed = false; + protected logView: () => void; constructor() { this.title = Translate.instant('core.courses.categories'); @@ -78,6 +81,16 @@ export class CoreCoursesCategoriesPage implements OnInit, OnDestroy { this.downloadEnabledObserver = CoreEvents.on(CoreCoursesProvider.EVENT_DASHBOARD_DOWNLOAD_ENABLED_CHANGED, (data) => { this.downloadEnabled = (this.downloadCourseEnabled || this.downloadCoursesEnabled) && data.enabled; }); + + this.logView = CoreTime.once(() => { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_course_get_categories', + name: this.title, + data: { categoryid: this.categoryId, category: 'course' }, + url: '/course/index.php' + (this.categoryId > 0 ? `?categoryid=${this.categoryId}` : ''), + }); + }); } /** @@ -138,6 +151,8 @@ export class CoreCoursesCategoriesPage implements OnInit, OnDestroy { !this.isDestroyed && CoreDomUtils.showErrorModalDefault(error, 'core.courses.errorloadcourses', true); } } + + this.logView(); } catch (error) { !this.isDestroyed && CoreDomUtils.showErrorModalDefault(error, 'core.courses.errorloadcategories', true); } diff --git a/src/core/features/courses/pages/dashboard/dashboard.ts b/src/core/features/courses/pages/dashboard/dashboard.ts index 8bca938e0..cc0ad8382 100644 --- a/src/core/features/courses/pages/dashboard/dashboard.ts +++ b/src/core/features/courses/pages/dashboard/dashboard.ts @@ -24,6 +24,9 @@ import { CoreCourseBlock } from '@features/course/services/course'; import { CoreBlockComponent } from '@features/block/components/block/block'; import { CoreNavigator } from '@services/navigator'; import { CoreBlockDelegate } from '@features/block/services/block-delegate'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { Translate } from '@singletons'; /** * Page that displays the dashboard page. @@ -46,6 +49,7 @@ export class CoreCoursesDashboardPage implements OnInit, OnDestroy { loaded = false; protected updateSiteObserver: CoreEventObserver; + protected logView: () => void; constructor() { // Refresh the enabled flags if site is updated. @@ -55,6 +59,16 @@ export class CoreCoursesDashboardPage implements OnInit, OnDestroy { this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite(); }, CoreSites.getCurrentSiteId()); + + this.logView = CoreTime.once(async () => { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_my_view_page', + name: Translate.instant('core.courses.mymoodle'), + data: { category: 'course' }, + url: '/my/', + }); + }); } /** @@ -102,6 +116,8 @@ export class CoreCoursesDashboardPage implements OnInit, OnDestroy { } this.loaded = true; + + this.logView(); } /** diff --git a/src/core/features/courses/pages/list/list.ts b/src/core/features/courses/pages/list/list.ts index 3ba71b126..f61ba3d0a 100644 --- a/src/core/features/courses/pages/list/list.ts +++ b/src/core/features/courses/pages/list/list.ts @@ -20,6 +20,9 @@ import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreCourseBasicSearchedData, CoreCourses, CoreCoursesProvider } from '../../services/courses'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { Translate } from '@singletons'; type CoreCoursesListMode = 'search' | 'all' | 'my'; @@ -61,6 +64,8 @@ export class CoreCoursesListPage implements OnInit, OnDestroy { protected downloadEnabledObserver: CoreEventObserver; protected courseIds = ''; protected isDestroyed = false; + protected logView: () => void; + protected logSearch?: () => void; constructor() { this.currentSiteId = CoreSites.getRequiredCurrentSite().getId(); @@ -96,6 +101,26 @@ export class CoreCoursesListPage implements OnInit, OnDestroy { this.downloadEnabledObserver = CoreEvents.on(CoreCoursesProvider.EVENT_DASHBOARD_DOWNLOAD_ENABLED_CHANGED, (data) => { this.downloadEnabled = (this.downloadCourseEnabled || this.downloadCoursesEnabled) && data.enabled; }); + + this.logView = CoreTime.once(async () => { + if (this.showOnlyEnrolled) { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_enrol_get_users_courses', + name: Translate.instant('core.courses.mycourses'), + data: { category: 'course' }, + url: '/my/courses.php', + }); + } else { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_course_get_courses_by_field', + name: Translate.instant('core.courses.availablecourses'), + data: { category: 'course' }, + url: '/course/index.php', + }); + } + }); } /** @@ -135,7 +160,7 @@ export class CoreCoursesListPage implements OnInit, OnDestroy { try { if (this.searchMode) { if (this.searchText) { - await this.search(this.searchText); + await this.searchCourses(); } } else { await this.loadCourses(true); @@ -176,6 +201,8 @@ export class CoreCoursesListPage implements OnInit, OnDestroy { this.coursesLoaded = this.courses.length; this.canLoadMore = this.loadedCourses.length > this.courses.length; + + this.logView(); } catch (error) { this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading. !this.isDestroyed && CoreDomUtils.showErrorModalDefault(error, 'core.courses.errorloadcourses', true); @@ -221,6 +248,7 @@ export class CoreCoursesListPage implements OnInit, OnDestroy { this.courses = []; this.searchPage = 0; this.searchTotal = 0; + this.logSearch = CoreTime.once(() => this.performLogSearch()); const modal = await CoreDomUtils.showModalLoading('core.searching', true); await this.searchCourses().finally(() => { @@ -242,6 +270,23 @@ export class CoreCoursesListPage implements OnInit, OnDestroy { this.fetchCourses(); } + /** + * Log search. + */ + protected async performLogSearch(): Promise { + if (!this.searchMode) { + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_course_search_courses', + name: Translate.instant('core.courses.availablecourses'), + data: { search: this.searchText, category: 'course' }, + url: `/course/search.php?search=${this.searchText}`, + }); + } + /** * Load more courses. * @@ -279,6 +324,8 @@ export class CoreCoursesListPage implements OnInit, OnDestroy { this.searchPage++; this.canLoadMore = this.courses.length < this.searchTotal; + + this.logSearch?.(); } catch (error) { this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading. !this.isDestroyed && CoreDomUtils.showErrorModalDefault(error, 'core.courses.errorsearching', true); diff --git a/src/core/features/courses/pages/my/my.ts b/src/core/features/courses/pages/my/my.ts index c9d956500..3f9ab7907 100644 --- a/src/core/features/courses/pages/my/my.ts +++ b/src/core/features/courses/pages/my/my.ts @@ -29,6 +29,9 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { Subscription } from 'rxjs'; import { CoreCourses } from '../../services/courses'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { Translate } from '@singletons'; /** * Page that shows a my courses. @@ -58,6 +61,7 @@ export class CoreCoursesMyPage implements OnInit, OnDestroy, AsyncDirective { protected updateSiteObserver: CoreEventObserver; protected onReadyPromise = new CorePromisedValue(); protected loadsManagerSubscription: Subscription; + protected logView: () => void; constructor(protected loadsManager: PageLoadsManager) { // Refresh the enabled flags if site is updated. @@ -73,6 +77,16 @@ export class CoreCoursesMyPage implements OnInit, OnDestroy, AsyncDirective { this.loaded = false; this.loadContent(); }); + + this.logView = CoreTime.once(async () => { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_enrol_get_users_courses', + name: Translate.instant('core.courses.mycourses'), + data: { category: 'course' }, + url: '/my/courses.php', + }); + }); } /** @@ -138,6 +152,8 @@ export class CoreCoursesMyPage implements OnInit, OnDestroy, AsyncDirective { this.loaded = true; this.onReadyPromise.resolve(); + + this.logView(); } /** diff --git a/src/core/features/grades/pages/course/course.page.ts b/src/core/features/grades/pages/course/course.page.ts index 7bca24a87..d148992d1 100644 --- a/src/core/features/grades/pages/course/course.page.ts +++ b/src/core/features/grades/pages/course/course.page.ts @@ -35,6 +35,8 @@ import { CoreUserParticipantsSource } from '@features/user/classes/participants- import { CoreUserData, CoreUserParticipant } from '@features/user/services/user'; import { CoreGradesCoursesSource } from '@features/grades/classes/grades-courses-source'; import { CoreDom } from '@singletons/dom'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays a course grades. @@ -61,12 +63,14 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy { loaded = false; protected useLegacyLayout?: boolean; // Whether to use the layout before 4.1. - protected fetchSuccess = false; + protected logView: () => void; constructor( protected route: ActivatedRoute, protected element: ElementRef, ) { + this.logView = CoreTime.once(() => this.performLogView()); + try { this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId', { route }); this.userId = CoreNavigator.getRouteNumberParam('userId', { route }) ?? CoreSites.getCurrentSiteUserId(); @@ -232,10 +236,7 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy { this.rowsOnView = this.getRowsOnHeight(); this.totalColumnsSpan = formattedTable.columns.reduce((total, column) => total + column.colspan, 0); - if (!this.fetchSuccess) { - this.fetchSuccess = true; - await CoreGrades.logCourseGradesView(this.courseId, this.userId); - } + this.logView(); } /** @@ -257,6 +258,22 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy { infiniteComplete && infiniteComplete(); } + /** + * Log view. + */ + protected async performLogView(): Promise { + await CoreUtils.ignoreErrors(CoreGrades.logCourseGradesView(this.courseId, this.userId)); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'gradereport_user_view_grade_report', + name: this.title ?? '', + data: { id: this.courseId, userid: this.userId, category: 'grades' }, + url: `/grade/report/user/index.php?id=${this.courseId}` + + (this.userId !== CoreSites.getCurrentSiteUserId() ? `&userid=${this.userId}` : ''), + }); + } + } /** diff --git a/src/core/features/grades/pages/courses/courses.ts b/src/core/features/grades/pages/courses/courses.ts index dd47d084c..79e55d35a 100644 --- a/src/core/features/grades/pages/courses/courses.ts +++ b/src/core/features/grades/pages/courses/courses.ts @@ -20,8 +20,11 @@ import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreGradesCoursesSource } from '@features/grades/classes/grades-courses-source'; import { CoreGrades } from '@features/grades/services/grades'; import { IonRefresher } from '@ionic/angular'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; +import { Translate } from '@singletons'; /** * Page that displays courses grades (main menu option). @@ -93,6 +96,14 @@ class CoreGradesCoursesManager extends CoreListItemsManager { */ protected async logActivity(): Promise { await CoreGrades.logCoursesGradesView(); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'gradereport_overview_view_grade_report', + name: Translate.instant('core.grades.grades'), + data: { courseId: CoreSites.getCurrentSiteHomeId(), category: 'grades' }, + url: '/grade/report/overview/index.php', + }); } } diff --git a/src/core/features/grades/services/grades.ts b/src/core/features/grades/services/grades.ts index be80ee574..85ae19c4e 100644 --- a/src/core/features/grades/services/grades.ts +++ b/src/core/features/grades/services/grades.ts @@ -15,7 +15,6 @@ import { Injectable } from '@angular/core'; import { CoreCourses } from '@features/courses/services/courses'; import { CoreSites } from '@services/sites'; -import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; import { makeSingleton } from '@singletons'; import { CoreLogger } from '@singletons/logger'; import { CoreWSExternalWarning } from '@services/ws'; @@ -358,34 +357,16 @@ export class CoreGradesProvider { * * @param courseId Course ID. * @param userId User ID. - * @param name Course name. If not set, it will be calculated. * @returns Promise resolved when done. */ - async logCourseGradesView(courseId: number, userId: number, name?: string): Promise { + async logCourseGradesView(courseId: number, userId: number): Promise { userId = userId || CoreSites.getCurrentSiteUserId(); - const wsName = 'gradereport_user_view_grade_report'; - - if (!name) { - // eslint-disable-next-line promise/catch-or-return - CoreCourses.getUserCourse(courseId, true) - .catch(() => ({})) - .then(course => CorePushNotifications.logViewEvent( - courseId, - 'fullname' in course ? course.fullname : '', - 'grades', - wsName, - { userid: userId }, - )); - } else { - CorePushNotifications.logViewEvent(courseId, name, 'grades', wsName, { userid: userId }); - } - const site = CoreSites.getCurrentSite(); const params: CoreGradesGradereportViewGradeReportWSParams = { courseid: courseId, userid: userId }; - await site?.write(wsName, params); + await site?.write('gradereport_user_view_grade_report', params); } /** @@ -403,8 +384,6 @@ export class CoreGradesProvider { courseid: courseId, }; - CorePushNotifications.logViewListEvent('grades', 'gradereport_overview_view_grade_report', params); - const site = CoreSites.getCurrentSite(); await site?.write('gradereport_overview_view_grade_report', params); diff --git a/src/core/features/h5p/components/h5p-iframe/h5p-iframe.ts b/src/core/features/h5p/components/h5p-iframe/h5p-iframe.ts index ea7bb3e38..080140489 100644 --- a/src/core/features/h5p/components/h5p-iframe/h5p-iframe.ts +++ b/src/core/features/h5p/components/h5p-iframe/h5p-iframe.ts @@ -29,6 +29,7 @@ import { CoreSite } from '@classes/site'; import { CoreLogger } from '@singletons/logger'; import { CoreH5PCore, CoreH5PDisplayOptions } from '../../classes/core'; import { CoreH5PHelper } from '../../classes/helper'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Component to render an iframe with an H5P package. @@ -64,7 +65,6 @@ export class CoreH5PIframeComponent implements OnChanges, OnDestroy { public elementRef: ElementRef, router: Router, ) { - this.logger = CoreLogger.getInstance('CoreH5PIframeComponent'); this.site = CoreSites.getRequiredCurrentSite(); this.siteId = this.site.getId(); @@ -101,6 +101,12 @@ export class CoreH5PIframeComponent implements OnChanges, OnDestroy { protected async play(): Promise { let localUrl: string | undefined; let state: string; + this.onlinePlayerUrl = this.onlinePlayerUrl || CoreH5P.h5pPlayer.calculateOnlinePlayerUrl( + this.site.getURL(), + this.fileUrl || '', + this.displayOptions, + this.trackComponent, + ); if (this.fileUrl) { state = await CoreFilepool.getFileStateByUrl(this.siteId, this.fileUrl); @@ -117,14 +123,16 @@ export class CoreH5PIframeComponent implements OnChanges, OnDestroy { if (localUrl) { // Local package. this.iframeSrc = localUrl; - } else { - this.onlinePlayerUrl = this.onlinePlayerUrl || CoreH5P.h5pPlayer.calculateOnlinePlayerUrl( - this.site.getURL(), - this.fileUrl || '', - this.displayOptions, - this.trackComponent, - ); + // Only log analytics event when playing local package, online package already logs it. + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'core_h5p_get_trusted_h5p_file', + name: 'H5P content', + data: { category: 'h5p' }, + url: this.onlinePlayerUrl, + }); + } else { // Never allow downloading in the app. This will only work if the user is allowed to change the params. const src = this.onlinePlayerUrl.replace( CoreH5PCore.DISPLAY_OPTION_DOWNLOAD + '=1', diff --git a/src/core/features/login/pages/site-policy/site-policy.ts b/src/core/features/login/pages/site-policy/site-policy.ts index 0d7f20d70..4131a118b 100644 --- a/src/core/features/login/pages/site-policy/site-policy.ts +++ b/src/core/features/login/pages/site-policy/site-policy.ts @@ -22,6 +22,8 @@ import { CoreLoginHelper } from '@features/login/services/login-helper'; import { CoreSite } from '@classes/site'; import { CoreNavigator } from '@services/navigator'; import { CoreEvents } from '@singletons/events'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { Translate } from '@singletons'; /** * Page to accept a site policy. @@ -94,6 +96,14 @@ export class CoreLoginSitePolicyPage implements OnInit { } finally { this.policyLoaded = true; } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'auth_email_get_signup_settings', + name: Translate.instant('core.login.policyagreement'), + data: { category: 'policy' }, + url: '/user/policy.php', + }); } /** diff --git a/src/core/features/pushnotifications/services/pushnotifications.ts b/src/core/features/pushnotifications/services/pushnotifications.ts index 234e54825..16edd45f3 100644 --- a/src/core/features/pushnotifications/services/pushnotifications.ts +++ b/src/core/features/pushnotifications/services/pushnotifications.ts @@ -47,6 +47,7 @@ import { CoreDatabaseCachingStrategy, CoreDatabaseTableProxy } from '@classes/da import { CoreObject } from '@singletons/object'; import { lazyMap, LazyMap } from '@/core/utils/lazy-map'; import { CorePlatform } from '@services/platform'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Service to handle push notifications. @@ -133,8 +134,11 @@ export class CorePushNotificationsProvider { CoreLocalNotifications.registerClick( CorePushNotificationsProvider.COMPONENT, (notification) => { - // Log notification open event. - this.logEvent('moodle_notification_open', notification, true); + CoreAnalytics.logEvent({ + eventName: 'moodle_notification_open', + type: CoreAnalyticsEventType.PUSH_NOTIFICATION, + data: notification, + }); this.notificationClicked(notification); }, @@ -145,8 +149,11 @@ export class CorePushNotificationsProvider { 'clear', CorePushNotificationsProvider.COMPONENT, (notification) => { - // Log notification dismissed event. - this.logEvent('moodle_notification_dismiss', notification, true); + CoreAnalytics.logEvent({ + eventName: 'moodle_notification_dismiss', + type: CoreAnalyticsEventType.PUSH_NOTIFICATION, + data: notification, + }); }, ); } @@ -248,26 +255,14 @@ export class CorePushNotificationsProvider { } /** - * Enable or disable Firebase analytics. + * Enable or disable analytics. * * @param enable Whether to enable or disable. * @returns Promise resolved when done. + * @deprecated since 4.3. Use CoreAnalytics.enableAnalytics instead. */ async enableAnalytics(enable: boolean): Promise { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const win = window; // This feature is only present in our fork of the plugin. - - if (!CoreConstants.CONFIG.enableanalytics || !win.PushNotification?.enableAnalytics) { - return; - } - - await new Promise(resolve => { - win.PushNotification.enableAnalytics(resolve, (error) => { - this.logger.error('Error enabling or disabling Firebase analytics', enable, error); - - resolve(); - }, !!enable); - }); + return CoreAnalytics.enableAnalytics(enable); } /** @@ -340,37 +335,35 @@ export class CorePushNotificationsProvider { } /** - * Log a firebase event. + * Log an analytics event. * - * @param name Name of the event. + * @param eventName Name of the event. * @param data Data of the event. - * @param filter Whether to filter the data. This is useful when logging a full notification. * @returns Promise resolved when done. This promise is never rejected. + * @deprecated since 4.3. Use CoreAnalytics.logEvent instead. */ - async logEvent(name: string, data: Record, filter?: boolean): Promise { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const win = window; // This feature is only present in our fork of the plugin. - - if (!CoreConstants.CONFIG.enableanalytics || !win.PushNotification?.logEvent) { - return; + async logEvent(eventName: string, data: Record): Promise { + if (eventName !== 'view_item' && eventName !== 'view_item_list') { + return CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.PUSH_NOTIFICATION, + eventName, + data, + }); } - // Check if the analytics is enabled by the user. - const enabled = await CoreConfig.get(CoreConstants.SETTINGS_ANALYTICS_ENABLED, true); - if (!enabled) { - return; - } + const name = data.name ? String(data.name) : ''; + delete data.name; - await new Promise(resolve => { - win.PushNotification.logEvent(resolve, (error) => { - this.logger.error('Error logging firebase event', name, error); - resolve(); - }, name, data, !!filter); + return CoreAnalytics.logEvent({ + type: eventName === 'view_item' ? CoreAnalyticsEventType.VIEW_ITEM : CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: data.moodleaction ?? '', + name, + data, }); } /** - * Log a firebase view_item event. + * Log an analytics VIEW_ITEM_LIST event. * * @param itemId The item ID. * @param itemName The item name. @@ -379,57 +372,44 @@ export class CorePushNotificationsProvider { * @param data Other data to pass to the event. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when done. This promise is never rejected. + * @deprecated since 4.3. Use CoreAnalytics.logEvent instead. */ logViewEvent( itemId: number | string | undefined, itemName: string | undefined, itemCategory: string | undefined, wsName: string, - data?: Record, - siteId?: string, + data?: Record, ): Promise { data = data || {}; - - // Add "moodle" to the name of all extra params. - data = CoreUtils.prefixKeys(data, 'moodle'); + data.id = itemId; + data.name = itemName; + data.category = itemCategory; data.moodleaction = wsName; - data.moodlesiteid = siteId || CoreSites.getCurrentSiteId(); - if (itemId) { - data.item_id = itemId; - } - if (itemName) { - data.item_name = itemName; - } - if (itemCategory) { - data.item_category = itemCategory; - } - - return this.logEvent('view_item', data, false); + return this.logEvent('view_item', data); } /** - * Log a firebase view_item_list event. + * Log an analytics view item list event. * * @param itemCategory The item category. * @param wsName Name of the WS. * @param data Other data to pass to the event. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when done. This promise is never rejected. + * @deprecated since 4.3. Use CoreAnalytics.logEvent instead. */ - logViewListEvent(itemCategory: string, wsName: string, data?: Record, siteId?: string): Promise { + logViewListEvent( + itemCategory: string, + wsName: string, + data?: Record, + ): Promise { data = data || {}; - - // Add "moodle" to the name of all extra params. - data = CoreUtils.prefixKeys(data, 'moodle'); data.moodleaction = wsName; - data.moodlesiteid = siteId || CoreSites.getCurrentSiteId(); + data.category = itemCategory; - if (itemCategory) { - data.item_category = itemCategory; - } - - return this.logEvent('view_item_list', data, false); + return this.logEvent('view_item_list', data); } /** diff --git a/src/core/features/reportbuilder/components/report-detail/report-detail.ts b/src/core/features/reportbuilder/components/report-detail/report-detail.ts index 873dc76f9..9a445b942 100644 --- a/src/core/features/reportbuilder/components/report-detail/report-detail.ts +++ b/src/core/features/reportbuilder/components/report-detail/report-detail.ts @@ -21,6 +21,7 @@ import { REPORT_ROWS_LIMIT, } from '@features/reportbuilder/services/reportbuilder'; import { IonRefresher } from '@ionic/angular'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { CoreNavigator } from '@services/navigator'; import { CoreScreen } from '@services/screen'; import { CoreSites } from '@services/sites'; @@ -28,6 +29,7 @@ import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextErrorObject } from '@services/utils/text'; import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; +import { CoreTime } from '@singletons/time'; import { BehaviorSubject, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -63,6 +65,8 @@ export class CoreReportBuilderReportDetailComponent implements OnInit { isString = (value: unknown): boolean => CoreReportBuilder.isString(value); + protected logView: (report: CoreReportBuilderRetrieveReportMapped) => void; + constructor() { this.source$ = this.state$.pipe( map(state => { @@ -72,6 +76,18 @@ export class CoreReportBuilderReportDetailComponent implements OnInit { return source ?? 'system'; }), ); + + this.logView = CoreTime.once(async (report) => { + await CoreUtils.ignoreErrors(CoreReportBuilder.viewReport(this.reportId)); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'core_reportbuilder_view_report', + name: report.details.name, + data: { id: this.reportId, category: 'reportbuilder' }, + url: `/reportbuilder/view.php?id=${this.reportId}`, + }); + }); } /** @@ -105,14 +121,13 @@ export class CoreReportBuilderReportDetailComponent implements OnInit { return; } - await CoreReportBuilder.viewReport(this.reportId); - this.updateState({ report, cardVisibleColumns: report.details.settingsdata.cardviewVisibleColumns, cardviewShowFirstTitle: report.details.settingsdata.cardviewShowFirstTitle, }); + this.logView(report); this.onReportLoaded.emit(report.details); } catch { const errorConfig: CoreTextErrorObject = { diff --git a/src/core/features/reportbuilder/pages/list/list.ts b/src/core/features/reportbuilder/pages/list/list.ts index 2538cb260..b57ce261f 100644 --- a/src/core/features/reportbuilder/pages/list/list.ts +++ b/src/core/features/reportbuilder/pages/list/list.ts @@ -18,9 +18,12 @@ import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/ import { CoreReportBuilderReportsSource } from '@features/reportbuilder/classes/reports-source'; import { CoreReportBuilder, CoreReportBuilderReport, REPORTS_LIST_LIMIT } from '@features/reportbuilder/services/reportbuilder'; import { IonRefresher } from '@ionic/angular'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { CoreNavigator } from '@services/navigator'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; +import { Translate } from '@singletons'; +import { CoreTime } from '@singletons/time'; import { BehaviorSubject } from 'rxjs'; @Component({ @@ -39,7 +42,11 @@ export class CoreReportBuilderListPage implements AfterViewInit, OnDestroy { loadMoreError: false, }); + protected logView: () => void; + constructor() { + this.logView = CoreTime.once(() => this.performLogView()); + try { const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(CoreReportBuilderReportsSource, []); this.reports = new CoreListItemsManager(source, CoreReportBuilderListPage); @@ -71,6 +78,8 @@ export class CoreReportBuilderListPage implements AfterViewInit, OnDestroy { async fetchReports(reload: boolean): Promise { reload ? await this.reports.reload() : await this.reports.load(); this.updateState({ loadMoreError: false }); + + this.logView(); } /** @@ -111,6 +120,19 @@ export class CoreReportBuilderListPage implements AfterViewInit, OnDestroy { await ionRefresher?.complete(); } + /** + * Log view. + */ + protected performLogView(): void { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_reportbuilder_list_reports', + name: Translate.instant('core.reportbuilder.reports'), + data: { category: 'reportbuilder' }, + url: '/reportbuilder/index.php', + }); + } + /** * @inheritdoc */ diff --git a/src/core/features/settings/lang.json b/src/core/features/settings/lang.json index 2680792cb..5af24834b 100644 --- a/src/core/features/settings/lang.json +++ b/src/core/features/settings/lang.json @@ -32,9 +32,9 @@ "disabled": "Disabled", "disallowed": "Locked off", "displayformat": "Display format", + "enableanalytics": "Enable analytics", + "enableanalyticsdescription": "If enabled, the app will collect anonymous data usage.", "enabledownloadsection": "Enable download sections", - "enablefirebaseanalytics": "Enable Firebase analytics", - "enablefirebaseanalyticsdescription": "If enabled, the app will collect anonymous data usage.", "enablerichtexteditor": "Enable text editor", "enablerichtexteditordescription": "If enabled, a text editor will be available when entering content.", "encryptedpushsupported": "Encrypted push notifications supported", diff --git a/src/core/features/settings/pages/general/general.html b/src/core/features/settings/pages/general/general.html index 9cc006e57..6be8e054e 100644 --- a/src/core/features/settings/pages/general/general.html +++ b/src/core/features/settings/pages/general/general.html @@ -76,8 +76,8 @@ -

{{ 'core.settings.enablefirebaseanalytics' | translate }}

-

{{ 'core.settings.enablefirebaseanalyticsdescription' | translate }}

+

{{ 'core.settings.enableanalytics' | translate }}

+

{{ 'core.settings.enableanalyticsdescription' | translate }}

diff --git a/src/core/features/settings/pages/general/general.ts b/src/core/features/settings/pages/general/general.ts index 874028c8e..88a498b77 100644 --- a/src/core/features/settings/pages/general/general.ts +++ b/src/core/features/settings/pages/general/general.ts @@ -27,6 +27,7 @@ import { CoreUtils } from '@services/utils/utils'; import { AlertButton } from '@ionic/angular'; import { CoreNavigator } from '@services/navigator'; import { CorePlatform } from '@services/platform'; +import { CoreAnalytics } from '@services/analytics'; /** * Page that displays the general settings. @@ -101,7 +102,7 @@ export class CoreSettingsGeneralPage { this.debugDisplay = await CoreConfig.get(CoreConstants.SETTINGS_DEBUG_DISPLAY, false); - this.analyticsSupported = CoreConstants.CONFIG.enableanalytics; + this.analyticsSupported = CoreAnalytics.hasHandlers(); if (this.analyticsSupported) { this.analyticsEnabled = await CoreConfig.get(CoreConstants.SETTINGS_ANALYTICS_ENABLED, true); } diff --git a/src/core/features/sitehome/pages/index/index.ts b/src/core/features/sitehome/pages/index/index.ts index ec1432416..a7f41db8a 100644 --- a/src/core/features/sitehome/pages/index/index.ts +++ b/src/core/features/sitehome/pages/index/index.ts @@ -29,6 +29,8 @@ import { CoreCourseModulePrefetchDelegate } from '@features/course/services/modu import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; import { CoreBlockHelper } from '@features/block/services/block-helper'; import { CoreUtils } from '@services/utils/utils'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays site home index. @@ -54,13 +56,25 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy { newsForumModule?: CoreCourseModuleData; protected updateSiteObserver: CoreEventObserver; - protected fetchSuccess = false; + protected logView: () => void; constructor() { // Refresh the enabled flags if site is updated. this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => { this.searchEnabled = !CoreCourses.isSearchCoursesDisabledInSite(); }, CoreSites.getCurrentSiteId()); + + this.logView = CoreTime.once(async () => { + await CoreUtils.ignoreErrors(CoreCourse.logView(this.siteHomeId)); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'core_course_view_course', + name: this.currentSite.getInfo()?.sitename ?? '', + data: { id: this.siteHomeId, category: 'course' }, + url: '/?redirect=0', + }); + }); } /** @@ -138,15 +152,7 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy { this.hasContent = result.hasContent || this.hasContent; } - if (!this.fetchSuccess) { - this.fetchSuccess = true; - CoreUtils.ignoreErrors(CoreCourse.logView( - this.siteHomeId, - undefined, - undefined, - this.currentSite.getInfo()?.sitename, - )); - } + this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'core.course.couldnotloadsectioncontent', true); } diff --git a/src/core/features/tag/pages/index/index.ts b/src/core/features/tag/pages/index/index.ts index 09a40f97f..84dcd5bb9 100644 --- a/src/core/features/tag/pages/index/index.ts +++ b/src/core/features/tag/pages/index/index.ts @@ -19,6 +19,10 @@ import { CoreTag } from '@features/tag/services/tag'; import { CoreTagAreaDelegate } from '@features/tag/services/tag-area-delegate'; import { CoreScreen } from '@services/screen'; import { CoreNavigator } from '@services/navigator'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { Translate } from '@singletons'; +import { CoreUrlUtils } from '@services/utils/url'; /** * Page that displays the tag index. @@ -42,6 +46,28 @@ export class CoreTagIndexPage implements OnInit { areas: CoreTagAreaDisplay[] = []; + protected logView: () => void; + + constructor() { + this.logView = CoreTime.once(async () => { + const params = { + tc: this.collectionId || undefined, + tag: this.tagName || undefined, + ta: this.areaId || undefined, + from: this.fromContextId || undefined, + ctx: this.contextId || undefined, + }; + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_tag_get_tagindex_per_area', + name: this.tagName || Translate.instant('core.tag.tag'), + data: { id: this.tagId || undefined, ...params, category: 'tag' }, + url: CoreUrlUtils.addParamsToUrl('/tag/index.php', params), + }); + }); + } + /** * @inheritdoc */ @@ -111,6 +137,8 @@ export class CoreTagIndexPage implements OnInit { this.areas = areasDisplay; + this.logView(); + } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error loading tag index'); } diff --git a/src/core/features/tag/pages/search/search.html b/src/core/features/tag/pages/search/search.html index 6126aa8c3..f428692cb 100644 --- a/src/core/features/tag/pages/search/search.html +++ b/src/core/features/tag/pages/search/search.html @@ -22,7 +22,7 @@ autocorrect="off" [spellcheck]="false" [autoFocus]="false" [lengthCheck]="0" searchArea="CoreTag"> - + {{ 'core.tag.inalltagcoll' | translate }} diff --git a/src/core/features/tag/pages/search/search.ts b/src/core/features/tag/pages/search/search.ts index 02ad43eb3..a608bd197 100644 --- a/src/core/features/tag/pages/search/search.ts +++ b/src/core/features/tag/pages/search/search.ts @@ -24,6 +24,8 @@ import { Translate } from '@singletons'; import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper'; import { CoreNavigator } from '@services/navigator'; import { CoreMainMenuDeepLinkManager } from '@features/mainmenu/classes/deep-link-manager'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays most used tags and allows searching. @@ -42,6 +44,21 @@ export class CoreTagSearchPage implements OnInit { loaded = false; searching = false; + protected logView: () => void; + protected logSearch?: () => void; + + constructor() { + this.logView = CoreTime.once(async () => { + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_tag_get_tag_cloud', + name: Translate.instant('core.tag.searchtags'), + data: { category: 'tag' }, + url: '/tag/search.php', + }); + }); + } + /** * View loaded. */ @@ -63,6 +80,10 @@ export class CoreTagSearchPage implements OnInit { this.fetchCollections(), this.fetchTags(), ]); + + if (!this.query) { + this.logView(); + } } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error loading tags.'); } @@ -92,6 +113,8 @@ export class CoreTagSearchPage implements OnInit { */ async fetchTags(): Promise { this.cloud = await CoreTag.getTagCloud(this.collectionId, undefined, undefined, this.query); + + this.logSearch?.(); } /** @@ -120,11 +143,17 @@ export class CoreTagSearchPage implements OnInit { * Search tags. * * @param query Search query. + * @param collectionId Collection ID to use. * @returns Resolved when done. */ - searchTags(query: string): Promise { + searchTags(query: string, collectionId?: number): Promise { this.searching = true; this.query = query; + if (collectionId !== undefined) { + this.collectionId = collectionId; + } + + this.logSearch = CoreTime.once(() => this.performLogSearch()); CoreApp.closeKeyboard(); return this.fetchTags().catch((error) => { @@ -134,4 +163,21 @@ export class CoreTagSearchPage implements OnInit { }); } + /** + * Log search. + */ + protected async performLogSearch(): Promise { + if (!this.query) { + return; + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_tag_get_tag_cloud', + name: Translate.instant('core.tag.searchtags'), + data: { category: 'tag' }, + url: `/tag/search.php&query=${this.query}&tc=${this.collectionId}&go=${Translate.instant('core.search')}`, + }); + } + } diff --git a/src/core/features/user/pages/participants/participants.page.ts b/src/core/features/user/pages/participants/participants.page.ts index d851cefd9..78c4d81ca 100644 --- a/src/core/features/user/pages/participants/participants.page.ts +++ b/src/core/features/user/pages/participants/participants.page.ts @@ -24,6 +24,8 @@ import { CoreUser, CoreUserParticipant, CoreUserData } from '@features/user/serv import { CoreUtils } from '@services/utils/utils'; import { CoreUserParticipantsSource } from '@features/user/classes/participants-source'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { Translate } from '@singletons'; /** * Page that displays the list of course participants. @@ -195,7 +197,15 @@ class CoreUserParticipantsManager extends CoreListItemsManager { - await CoreUser.logParticipantsView(this.getSource().COURSE_ID); + await CoreUtils.ignoreErrors(CoreUser.logParticipantsView(this.getSource().COURSE_ID)); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_user_view_user_list', + name: Translate.instant('core.user.participants'), + data: { courseid: this.getSource().COURSE_ID, category: 'user' }, + url: `/user/index.php?id=${this.getSource().COURSE_ID}`, + }); } } diff --git a/src/core/features/user/pages/profile/profile.ts b/src/core/features/user/pages/profile/profile.ts index 3cb53f79a..cf8197521 100644 --- a/src/core/features/user/pages/profile/profile.ts +++ b/src/core/features/user/pages/profile/profile.ts @@ -35,6 +35,9 @@ import { CoreCourses } from '@features/courses/services/courses'; import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; import { CoreUserParticipantsSource } from '@features/user/classes/participants-source'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; +import { CoreTime } from '@singletons/time'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { Translate } from '@singletons'; @Component({ selector: 'page-core-user-profile', @@ -48,7 +51,7 @@ export class CoreUserProfilePage implements OnInit, OnDestroy { protected site!: CoreSite; protected obsProfileRefreshed: CoreEventObserver; protected subscription?: Subscription; - protected fetchSuccess = false; + protected logView: (user: CoreUserProfile) => void; userLoaded = false; isLoadingHandlers = false; @@ -72,6 +75,29 @@ export class CoreUserProfilePage implements OnInit, OnDestroy { this.user.email = data.user.email; this.user.address = CoreUserHelper.formatAddress('', data.user.city, data.user.country); }, CoreSites.getCurrentSiteId()); + + this.logView = CoreTime.once(async (user) => { + try { + await CoreUser.logView(this.userId, this.courseId, user.fullname); + } catch (error) { + this.isDeleted = error?.errorcode === 'userdeleted' || error?.errorcode === 'wsaccessuserdeleted'; + this.isSuspended = error?.errorcode === 'wsaccessusersuspended'; + this.isEnrolled = error?.errorcode !== 'notenrolledprofile'; + } + let extraParams = ''; + if (this.userId !== CoreSites.getCurrentSiteUserId()) { + const isCourseProfile = this.courseId && this.courseId !== CoreSites.getCurrentSiteHomeId(); + extraParams = `?id=${this.userId}` + (isCourseProfile ? `&course=${this.courseId}` : ''); + } + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM, + ws: 'core_user_view_user_profile', + name: user.fullname + ': ' + Translate.instant('core.publicprofile'), + data: { id: this.userId, courseid: this.courseId || undefined, category: 'user' }, + url: `/user/profile.php${extraParams}`, + }); + }); } /** @@ -151,18 +177,7 @@ export class CoreUserProfilePage implements OnInit, OnDestroy { this.isLoadingHandlers = !CoreUserDelegate.areHandlersLoaded(user.id, context, this.courseId); }); - if (!this.fetchSuccess) { - this.fetchSuccess = true; - - try { - await CoreUser.logView(this.userId, this.courseId, this.user.fullname); - } catch (error) { - this.isDeleted = error?.errorcode === 'userdeleted' || error?.errorcode === 'wsaccessuserdeleted'; - this.isSuspended = error?.errorcode === 'wsaccessusersuspended'; - this.isEnrolled = error?.errorcode !== 'notenrolledprofile'; - } - } - + this.logView(user); } catch (error) { // Error is null for deleted users, do not show the modal. CoreDomUtils.showErrorModal(error); diff --git a/src/core/features/user/services/user.ts b/src/core/features/user/services/user.ts index 4e80ccf32..65ca5a1aa 100644 --- a/src/core/features/user/services/user.ts +++ b/src/core/features/user/services/user.ts @@ -26,7 +26,6 @@ import { CoreEvents, CoreEventSiteData, CoreEventUserDeletedData, CoreEventUserS import { CoreStatusWithWarningsWSResponse, CoreWSExternalWarning } from '@services/ws'; import { CoreError } from '@classes/errors/error'; import { USERS_TABLE_NAME, CoreUserDBRecord } from './database/user'; -import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; import { CoreUserHelper } from './user-helper'; import { CoreUrl } from '@singletons/url'; @@ -569,24 +568,20 @@ export class CoreUserProvider { * * @param userId User ID. * @param courseId Course ID. - * @param name Name of the user. * @returns Promise resolved when done. */ - async logView(userId: number, courseId?: number, name?: string, siteId?: string): Promise { + async logView(userId: number, courseId?: number, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); const params: CoreUserViewUserProfileWSParams = { userid: userId, }; - const wsName = 'core_user_view_user_profile'; if (courseId) { params.courseid = courseId; } - CorePushNotifications.logViewEvent(userId, name, 'user', wsName, { courseid: courseId }); - - return site.write(wsName, params); + return site.write('core_user_view_user_profile', params); } /** @@ -602,8 +597,6 @@ export class CoreUserProvider { courseid: courseId, }; - CorePushNotifications.logViewListEvent('user', 'core_user_view_user_list', params); - return site.write('core_user_view_user_list', params); } diff --git a/src/core/lang.json b/src/core/lang.json index fc5ff75b5..dfbbe7b2c 100644 --- a/src/core/lang.json +++ b/src/core/lang.json @@ -248,6 +248,7 @@ "play": "Play", "previous": "Previous", "proceed": "Proceed", + "publicprofile": "Public profile", "pulltorefresh": "Pull to refresh", "qrscanner": "QR scanner", "quotausage": "You have currently used {{$a.used}} of your {{$a.total}} limit.", diff --git a/src/core/services/analytics.ts b/src/core/services/analytics.ts new file mode 100644 index 000000000..ae4e5ad29 --- /dev/null +++ b/src/core/services/analytics.ts @@ -0,0 +1,184 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; +import { CorePushNotificationsNotificationBasicData } from '@features/pushnotifications/services/pushnotifications'; +import { makeSingleton } from '@singletons'; +import { CoreEvents } from '@singletons/events'; +import { CoreSites } from './sites'; +import { CoreConfig, CoreConfigProvider } from './config'; +import { CoreConstants } from '../constants'; +import { CoreUrlUtils } from './utils/url'; + +/** + * Helper service to support analytics. + */ +@Injectable({ providedIn: 'root' }) +export class CoreAnalyticsService extends CoreDelegate { + + constructor() { + super('CoreAnalyticsService', true); + + CoreEvents.on(CoreConfigProvider.ENVIRONMENT_UPDATED, () => this.updateHandlers()); + CoreEvents.on(CoreEvents.LOGOUT, () => this.clearSiteHandlers()); + } + + /** + * Clear current site handlers. Reserved for core use. + */ + protected clearSiteHandlers(): void { + this.enabledHandlers = {}; + } + + /** + * Enable or disable analytics for all handlers. + * + * @param enable Whether to enable or disable. + * @returns Promise resolved when done. + */ + async enableAnalytics(enable: boolean): Promise { + try { + await Promise.all(Object.values(this.handlers).map(handler => handler.enableAnalytics?.(enable))); + } catch (error) { + this.logger.error(`Error ${enable ? 'enabling' : 'disabling'} analytics`, error); + } + } + + /** + * Log an event for the current site. + * + * @param event Event data. + */ + async logEvent(event: CoreAnalyticsAnyEvent): Promise { + const site = CoreSites.getCurrentSite(); + if (!site) { + return; + } + + // Check if analytics is enabled by the user. + const enabled = await CoreConfig.get(CoreConstants.SETTINGS_ANALYTICS_ENABLED, true); + if (!enabled) { + return; + } + + const treatedEvent: CoreAnalyticsEvent = { + ...event, + siteId: site.getId(), + }; + if ('url' in treatedEvent && treatedEvent.url) { + if (!CoreUrlUtils.isAbsoluteURL(treatedEvent.url)) { + treatedEvent.url = site.createSiteUrl(treatedEvent.url); + } else if (!site.containsUrl(treatedEvent.url)) { + // URL belongs to a different site, ignore the event. + return; + } + } + + try { + await Promise.all(Object.values(this.enabledHandlers).map(handler => handler.logEvent(treatedEvent))); + } catch (error) { + this.logger.error('Error logging event', event, error); + } + } + +} + +export const CoreAnalytics = makeSingleton(CoreAnalyticsService); + +/** + * Interface that all analytics handlers must implement. + */ +export interface CoreAnalyticsHandler extends CoreDelegateHandler { + + /** + * Log an event. + * + * @param event Event data. + */ + logEvent(event: CoreAnalyticsEvent): Promise; + + /** + * Enable or disable analytics. + * + * @param enable Whether to enable or disable. + * @returns Promise resolved when done. + */ + enableAnalytics?(enable: boolean): Promise; + +} + +/** + * Possible types of events. + */ +export enum CoreAnalyticsEventType { + VIEW_ITEM = 'view_item', // View some page or data that mainly contains one item. + VIEW_ITEM_LIST = 'view_item_list', // View some page or data that mainly contains a list of items. + PUSH_NOTIFICATION = 'push_notification', // Event related to push notifications. + DOWNLOAD_FILE = 'download_file', // A file was downloaded. + OPEN_LINK = 'open_link', // A link was opened in browser or InAppBrowser. +} + +/** + * Any type of event data. + */ +export type CoreAnalyticsAnyEvent = CoreAnalyticsViewEvent | CoreAnalyticsPushEvent | CoreAnalyticsDownloadFileEvent | +CoreAnalyticsOpenLinkEvent; + +/** + * Event data, including calculated data. + */ +export type CoreAnalyticsEvent = CoreAnalyticsAnyEvent & { + siteId: string; +}; + +/** + * Data specific for the VIEW_ITEM and VIEW_LIST events. + */ +export type CoreAnalyticsViewEvent = { + type: CoreAnalyticsEventType.VIEW_ITEM | CoreAnalyticsEventType.VIEW_ITEM_LIST; + ws: string; // Name of the WS used to log the data in LMS or to obtain the data if there is no log WS. + name: string; // Name of the item or page viewed. + url?: string; // Moodle URL. You can use the URL without the domain, e.g. /mod/foo/view.php. + data?: { + id?: number | string; // ID of the item viewed (if any). + category?: string; // Category of the data viewed (if any). + [key: string]: string | number | boolean | undefined; + }; +}; + +/** + * Data specific for the PUSH_NOTIFICATION events. + */ +export type CoreAnalyticsPushEvent = { + type: CoreAnalyticsEventType.PUSH_NOTIFICATION; + eventName: string; // Name of the event. + data: CorePushNotificationsNotificationBasicData; +}; + +/** + * Data specific for the DOWNLOAD_FILE events. + */ +export type CoreAnalyticsDownloadFileEvent = { + type: CoreAnalyticsEventType.DOWNLOAD_FILE; + fileUrl: string; +}; + +/** + * Data specific for the OPEN_LINK events. + */ +export type CoreAnalyticsOpenLinkEvent = { + type: CoreAnalyticsEventType.OPEN_LINK; + link: string; +}; diff --git a/src/core/services/filepool.ts b/src/core/services/filepool.ts index 3c633823a..6c352d777 100644 --- a/src/core/services/filepool.ts +++ b/src/core/services/filepool.ts @@ -55,6 +55,7 @@ import { lazyMap, LazyMap } from '../utils/lazy-map'; import { asyncInstance, AsyncInstance } from '../utils/async-instance'; import { CorePath } from '@singletons/path'; import { CorePromisedValue } from '@classes/promised-value'; +import { CoreAnalytics, CoreAnalyticsEventType } from './analytics'; /* * Factory for handling downloading files and retrieve downloaded files. @@ -764,6 +765,11 @@ export class CoreFilepoolProvider { extension: fileEntry.extension, }); + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.DOWNLOAD_FILE, + fileUrl: CoreUrlUtils.unfixPluginfileURL(fileUrl, site.getURL()), + }); + // Add the anchor again to the local URL. return fileEntry.toURL() + (anchor || ''); }).finally(() => { diff --git a/src/core/services/tests/utils/url.test.ts b/src/core/services/tests/utils/url.test.ts index 50b3fea57..fe3ee07b3 100644 --- a/src/core/services/tests/utils/url.test.ts +++ b/src/core/services/tests/utils/url.test.ts @@ -65,6 +65,17 @@ describe('CoreUrlUtilsProvider', () => { expect(url).toEqual(originalUrl); }); + it('doesn\'t add undefined or null params', () => { + const originalUrl = 'https://moodle.org'; + const url = urlUtils.addParamsToUrl(originalUrl, { + foo: undefined, + bar: null, + baz: 1, + }); + + expect(url).toEqual('https://moodle.org?baz=1'); + }); + it('adds anchor to URL', () => { const originalUrl = 'https://moodle.org'; const params = { diff --git a/src/core/services/utils/url.ts b/src/core/services/utils/url.ts index f0148b84c..b0c1286eb 100644 --- a/src/core/services/utils/url.ts +++ b/src/core/services/utils/url.ts @@ -64,18 +64,18 @@ export class CoreUrlUtilsProvider { const urlAndAnchor = url.split('#'); url = urlAndAnchor[0]; - let separator = url.indexOf('?') != -1 ? '&' : '?'; + let separator = url.indexOf('?') !== -1 ? '&' : '?'; for (const key in params) { let value = params[key]; - if (boolToNumber && typeof value == 'boolean') { + if (boolToNumber && typeof value === 'boolean') { // Convert booleans to 1 or 0. value = value ? '1' : '0'; } - // Ignore objects. - if (typeof value != 'object') { + // Ignore objects and undefined. + if (typeof value !== 'object' && value !== undefined) { url += separator + key + '=' + value; separator = '&'; } @@ -542,7 +542,10 @@ export class CoreUrlUtilsProvider { return url; } - // Not a pluginfile URL. Treat webservice/pluginfile case. + // Check tokenpluginfile first. + url = url.replace(/\/tokenpluginfile\.php\/[^/]+\//, '/pluginfile.php/'); + + // Treat webservice/pluginfile case. url = url.replace(/\/webservice\/pluginfile\.php\//, '/pluginfile.php/'); // Make sure the URL doesn't contain the token. diff --git a/src/core/services/utils/utils.ts b/src/core/services/utils/utils.ts index 4c10f5c40..a1e675031 100644 --- a/src/core/services/utils/utils.ts +++ b/src/core/services/utils/utils.ts @@ -39,6 +39,8 @@ import { CoreErrorWithOptions } from '@classes/errors/errorwithoptions'; import { CoreFilepool } from '@services/filepool'; import { CoreSites } from '@services/sites'; import { CoreCancellablePromise } from '@classes/cancellable-promise'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreUrlUtils } from './url'; export type TreeNode = T & { children: TreeNode[] }; @@ -1058,7 +1060,7 @@ export class CoreUtilsProvider { * @param options Override default options passed to InAppBrowser. * @returns The opened window. */ - openInApp(url: string, options?: InAppBrowserOptions): InAppBrowserObject { + openInApp(url: string, options?: CoreUtilsOpenInAppOptions): InAppBrowserObject { options = options || {}; options.usewkwebview = 'yes'; // Force WKWebView in iOS. options.enableViewPortScale = options.enableViewPortScale ?? 'yes'; // Enable zoom on iOS by default. @@ -1116,6 +1118,11 @@ export class CoreUtilsProvider { }); } + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.OPEN_LINK, + link: CoreUrlUtils.unfixPluginfileURL(options.originalUrl ?? url), + }); + return this.iabInstance; } @@ -1170,14 +1177,20 @@ export class CoreUtilsProvider { * @param options Options. */ async openInBrowser(url: string, options: CoreUtilsOpenInBrowserOptions = {}): Promise { + const originaUrl = CoreUrlUtils.unfixPluginfileURL(options.originalUrl ?? options.browserWarningUrl ?? url); if (options.showBrowserWarning || options.showBrowserWarning === undefined) { try { - await CoreWindow.confirmOpenBrowserIfNeeded(options.browserWarningUrl ?? url); + await CoreWindow.confirmOpenBrowserIfNeeded(originaUrl); } catch (error) { return; // Cancelled, stop. } } + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.OPEN_LINK, + link: originaUrl, + }); + window.open(url, '_system'); } @@ -1204,12 +1217,19 @@ export class CoreUtilsProvider { type: mimetype, }; - return WebIntent.startActivity(options).catch((error) => { + try { + await WebIntent.startActivity(options); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.OPEN_LINK, + link: CoreUrlUtils.unfixPluginfileURL(url), + }); + } catch (error) { this.logger.error('Error opening online file ' + url + ' with mimetype ' + mimetype); this.logger.error('Error: ', JSON.stringify(error)); throw new Error(Translate.instant('core.erroropenfilenoapp')); - }); + } } // In the rest of platforms we need to open them in InAppBrowser. @@ -1897,7 +1917,18 @@ export type CoreUtilsOpenFileOptions = { */ export type CoreUtilsOpenInBrowserOptions = { showBrowserWarning?: boolean; // Whether to display a warning before opening in browser. Defaults to true. - browserWarningUrl?: string; // The URL to display in the warning message. Use it to hide sensitive information. + originalUrl?: string; // Original URL to open (in case the URL was treated, e.g. to add a token or an auto-login). + /** + * @deprecated since 4.3, use originalUrl instead. + */ + browserWarningUrl?: string; +}; + +/** + * Options for opening in InAppBrowser. + */ +export type CoreUtilsOpenInAppOptions = InAppBrowserOptions & { + originalUrl?: string; // Original URL to open (in case the URL was treated, e.g. to add a token or an auto-login). }; /** diff --git a/src/core/singletons/object.ts b/src/core/singletons/object.ts index e89b0e0ff..a0e0ee4ac 100644 --- a/src/core/singletons/object.ts +++ b/src/core/singletons/object.ts @@ -30,6 +30,20 @@ export type CoreObjectWithoutUndefined = Pretty<{ */ export class CoreObject { + /** + * Returns a value of an object and deletes it from the object. + * + * @param obj Object. + * @param key Key of the value to consume. + * @returns Whether objects are equal. + */ + static consumeKey(obj: T, key: K): T[K] { + const value = obj[key]; + delete obj[key]; + + return value; + } + /** * Check if two objects have the same shape and the same leaf values. * diff --git a/src/core/singletons/tests/object.test.ts b/src/core/singletons/tests/object.test.ts index c0ce0d302..64702ef10 100644 --- a/src/core/singletons/tests/object.test.ts +++ b/src/core/singletons/tests/object.test.ts @@ -16,6 +16,22 @@ import { CoreObject } from '@singletons/object'; describe('CoreObject singleton', () => { + it('consumes object keys', () => { + const object = { + foo: 'a', + bar: 'b', + baz: 'c', + }; + + const fooValue = CoreObject.consumeKey(object, 'foo'); + const bazValue = CoreObject.consumeKey(object, 'baz'); + + expect(fooValue).toEqual('a'); + expect(bazValue).toEqual('c'); + expect(object.bar).toEqual('b'); + expect(Object.keys(object)).toEqual(['bar']); + }); + it('compares two values, checking all subproperties if needed', () => { expect(CoreObject.deepEquals(1, 1)).toBe(true); expect(CoreObject.deepEquals(1, 2)).toBe(false); diff --git a/src/types/config.d.ts b/src/types/config.d.ts index cd32bce2d..49743638d 100644 --- a/src/types/config.d.ts +++ b/src/types/config.d.ts @@ -50,7 +50,6 @@ export interface EnvironmentConfig { forcedefaultlanguage: boolean; privacypolicy: string; notificoncolor: string; - enableanalytics: boolean; enableonboarding: boolean; forceColorScheme: CoreColorScheme; forceLoginLogo: boolean; diff --git a/upgrade.txt b/upgrade.txt index e0b022b49..dcb5816f9 100644 --- a/upgrade.txt +++ b/upgrade.txt @@ -5,6 +5,8 @@ information provided here is intended especially for developers. - CoreSiteBasicInfo fullName attribute has changed to fullname and avatar to userpictureurl to match user fields. - Font Awesome icon library has been updated to 6.4.0. But nothing has changed, only version number. + - The analytics system in the app has been refactored and some functions that could trigger analytics calls no longer do it, now you need to use CoreAnalytics instead. Some functions in CoreCourseLogHelper and CorePushNotificationsProvider have been deprecated. + - Due to the analytics refactor, the parameters of most log functions have changed. === 4.2.0 ===