diff --git a/backend/package.json b/backend/package.json index b606583..d8158bd 100644 --- a/backend/package.json +++ b/backend/package.json @@ -8,7 +8,7 @@ "start": "node --enable-source-maps dist/app.js", "test": "jest", "build": "tsc", - "dev": "tsx watch src/app.ts | bunyan -o short", + "dev": "tsx watch src/app.ts | bunyan -o short -l debug", "lint": "eslint src/**/*.ts", "db:start": "docker-compose -f ../compose.yml up -d db", "dotenv": "cp -n .env.example .env || true" diff --git a/backend/src/controllers/seats.controller.ts b/backend/src/controllers/seats.controller.ts index 49e47c7..1565307 100644 --- a/backend/src/controllers/seats.controller.ts +++ b/backend/src/controllers/seats.controller.ts @@ -12,14 +12,14 @@ class SeatsController { } async getActivity(req: Request, res: Response): Promise { - const { daysInactive } = req.query; + const { daysInactive, precision } = req.query; const _daysInactive = Number(daysInactive); if (!daysInactive || isNaN(_daysInactive)) { res.status(400).json({ error: 'daysInactive query parameter is required' }); return; } try { - const activityDays = await SeatsService.getAssigneesActivity(_daysInactive); + const activityDays = await SeatsService.getAssigneesActivity(_daysInactive, precision as 'hour' | 'day'); res.status(200).json(activityDays); } catch (error) { res.status(500).json(error); diff --git a/backend/src/database.ts b/backend/src/database.ts index 9ec055d..a39d283 100644 --- a/backend/src/database.ts +++ b/backend/src/database.ts @@ -10,7 +10,11 @@ const sequelize = process.env.JAWSDB_URL ? acquire: 30000, idle: 10000 }, - logging: (sql: string) => logger.debug(sql) + logging: (sql) => logger.debug(sql), + timezone: '+00:00', // Force UTC timezone + dialectOptions: { + timezone: '+00:00' // Force UTC for MySQL connection + }, }) : new Sequelize({ dialect: 'mysql', @@ -19,7 +23,11 @@ const sequelize = process.env.JAWSDB_URL ? username: process.env.MYSQL_USER || 'root', password: process.env.MYSQL_PASSWORD || 'octocat', database: process.env.MYSQL_DATABASE || 'value', - logging: (sql: string) => logger.debug(sql) + logging: (sql) => logger.debug(sql), + timezone: '+00:00', // Force UTC timezone + dialectOptions: { + timezone: '+00:00' // Force UTC for MySQL connection + }, }); const dbConnect = async () => { diff --git a/backend/src/models/metrics.model.ts b/backend/src/models/metrics.model.ts index 28d2730..540e19b 100644 --- a/backend/src/models/metrics.model.ts +++ b/backend/src/models/metrics.model.ts @@ -400,7 +400,7 @@ MetricDotcomChatModelStats.belongsTo(MetricDotcomChatMetrics, { export async function insertMetrics(data: CopilotMetrics[]) { for (const day of data) { const parts = day.date.split('-').map(Number); - const date = new Date(Date.UTC(parts[0], parts[1] - 1, parts[2], 12)); + const date = new Date(Date.UTC(parts[0], parts[1] - 1, parts[2] + 1)); let metric: MetricDaily; try { metric = await MetricDaily.create({ diff --git a/backend/src/services/copilot.seats.service.ts b/backend/src/services/copilot.seats.service.ts index 88e88df..945a9ba 100644 --- a/backend/src/services/copilot.seats.service.ts +++ b/backend/src/services/copilot.seats.service.ts @@ -101,20 +101,20 @@ class SeatsService { } } - async getAssigneesActivity(daysInactive: number): Promise { + async getAssigneesActivity(daysInactive: number, precision: 'hour' | 'day' = 'day'): Promise { const assignees = await Assignee.findAll({ attributes: ['login', 'id'], - order: [ - ['login', 'ASC'], - [{ model: Seat, as: 'activity' }, 'createdAt', 'DESC'] - ], include: [ { model: Seat, as: 'activity', required: false, - attributes: ['createdAt', 'last_activity_at'] + attributes: ['createdAt', 'last_activity_at'], + order: [['last_activity_at', 'ASC']], } + ], + order: [ + [{ model: Seat, as: 'activity' }, 'last_activity_at', 'ASC'] ] }); const activityDays: AssigneeDailyActivity = {}; @@ -123,9 +123,15 @@ class SeatsService { const fromTime = activity.last_activity_at?.getTime() || 0; const toTime = activity.createdAt.getTime(); const diff = Math.floor((toTime - fromTime) / 86400000); - const dateIndex = activity.createdAt.toISOString().slice(0, 10); - if (!activityDays[dateIndex]) { - activityDays[dateIndex] = { + const dateIndex = new Date(activity.createdAt); + if (precision === 'day') { + dateIndex.setUTCHours(0, 0, 0, 0); + } else if (precision === 'hour') { + dateIndex.setUTCMinutes(0, 0, 0); + } + const dateIndexStr = new Date(dateIndex).toISOString(); + if (!activityDays[dateIndexStr]) { + activityDays[dateIndexStr] = { totalSeats: 0, totalActive: 0, totalInactive: 0, @@ -133,13 +139,13 @@ class SeatsService { inactive: {} } } - if (activityDays[dateIndex].active[assignee.login] || activityDays[dateIndex].inactive[assignee.login]) { + if (activityDays[dateIndexStr].active[assignee.login] || activityDays[dateIndexStr].inactive[assignee.login]) { return; // already processed for this day } if (diff > daysInactive) { - activityDays[dateIndex].inactive[assignee.login] = assignee.activity[0].last_activity_at; + activityDays[dateIndexStr].inactive[assignee.login] = assignee.activity[0].last_activity_at; } else { - activityDays[dateIndex].active[assignee.login] = assignee.activity[0].last_activity_at; + activityDays[dateIndexStr].active[assignee.login] = assignee.activity[0].last_activity_at; } }); }); @@ -147,8 +153,14 @@ class SeatsService { activityDays[date].totalSeats = Object.values(activity.active).length + Object.values(activity.inactive).length activityDays[date].totalActive = Object.values(activity.active).length activityDays[date].totalInactive = Object.values(activity.inactive).length - }); - return activityDays; + }); + + const sortedActivityDays = Object.fromEntries( + Object.entries(activityDays) + .sort(([dateA], [dateB]) => new Date(dateA).getTime() - new Date(dateB).getTime()) + ); + + return sortedActivityDays; } } diff --git a/backend/src/services/query.service.ts b/backend/src/services/query.service.ts index 83e4d3f..a456526 100644 --- a/backend/src/services/query.service.ts +++ b/backend/src/services/query.service.ts @@ -7,7 +7,7 @@ import { insertMetrics } from '../models/metrics.model.js'; import { CopilotMetrics } from '../models/metrics.model.interfaces.js'; import { getLastUpdatedAt, Member, Team, TeamMemberAssociation } from '../models/teams.model.js'; -const DEFAULT_CRON_EXPRESSION = '0 0 * * *'; +const DEFAULT_CRON_EXPRESSION = '0 * * * *'; class QueryService { private static instance: QueryService; private cronJob: CronJob; @@ -32,7 +32,7 @@ class QueryService { setup.setSetupStatusDbInitialized({ copilotSeats: true })), ] // Query teams and members if it has been more than 24 hours since the last update - if ((await getLastUpdatedAt() || new Date(0)).getTime() < new Date().getTime() - 1000 * 60 * 60 * 24) { + if ((await getLastUpdatedAt()).getTime() < new Date().getTime() - 1000 * 60 * 60 * 24) { queries.push( this.queryTeamsAndMembers().then(() => setup.setSetupStatusDbInitialized({ teamsAndMembers: true })) diff --git a/backend/src/services/settings.service.ts b/backend/src/services/settings.service.ts index 5a530f9..e03948e 100644 --- a/backend/src/services/settings.service.ts +++ b/backend/src/services/settings.service.ts @@ -81,9 +81,7 @@ class SettingsService { await SmeeService.createSmeeWebhookProxy(); } if (name === 'webhookSecret') { - console.log('setting webhook secret', value) setup.addToEnv({ GITHUB_WEBHOOK_SECRET: value }); - console.log(name, value) try { await setup.createAppFromEnv(); } catch { @@ -96,7 +94,6 @@ class SettingsService { async updateSettings(obj: { [key: string]: string }) { Object.entries(obj).forEach(([name, value]) => { - console.log(name, value) this.updateSetting(name, value); }); } diff --git a/compose.yml b/compose.yml index 3947081..84ea10d 100644 --- a/compose.yml +++ b/compose.yml @@ -25,7 +25,7 @@ services: environment: MYSQL_ROOT_PASSWORD: octocat MYSQL_DATABASE: value - # TZ: UTC + TZ: UTC ports: - '3306:3306' volumes: diff --git a/frontend/src/app/highcharts.theme.ts b/frontend/src/app/highcharts.theme.ts index c3c4127..ee833db 100644 --- a/frontend/src/app/highcharts.theme.ts +++ b/frontend/src/app/highcharts.theme.ts @@ -194,6 +194,9 @@ Highcharts.theme = { }, xAxis: xAxisConfig, yAxis: yAxisConfig, + time: { + useUTC: false + }, legend: { align: 'left', verticalAlign: 'top', diff --git a/frontend/src/app/main/copilot/copilot-metrics/copilot-metrics.component.ts b/frontend/src/app/main/copilot/copilot-metrics/copilot-metrics.component.ts index ce63371..6b78c6a 100644 --- a/frontend/src/app/main/copilot/copilot-metrics/copilot-metrics.component.ts +++ b/frontend/src/app/main/copilot/copilot-metrics/copilot-metrics.component.ts @@ -10,6 +10,5 @@ import { DateRangeSelectComponent } from "../../../shared/date-range-select/date }) export class CopilotMetricsComponent { dateRangeChange(event: {start: Date, end: Date}) { - console.log(event); } } diff --git a/frontend/src/app/main/copilot/copilot-value/adoption-chart/adoption-chart.component.ts b/frontend/src/app/main/copilot/copilot-value/adoption-chart/adoption-chart.component.ts index a911d1e..0c99899 100644 --- a/frontend/src/app/main/copilot/copilot-value/adoption-chart/adoption-chart.component.ts +++ b/frontend/src/app/main/copilot/copilot-value/adoption-chart/adoption-chart.component.ts @@ -36,7 +36,7 @@ export class AdoptionChartComponent implements OnChanges { }, yAxis: { title: { - text: 'Adoption (%)' + text: 'Percent Active' }, min: 0, max: 100, @@ -114,14 +114,12 @@ export class AdoptionChartComponent implements OnChanges { } ngOnChanges(changes: SimpleChanges) { - console.log('old', this.chartOptions); if (changes['data'] && this.data) { this._chartOptions = this.highchartsService.transformActivityMetricsToLine(this.data); this.chartOptions = { ...this.chartOptions, ...this._chartOptions }; - console.log('new', this.chartOptions); this.updateFlag = true; } } diff --git a/frontend/src/app/main/copilot/copilot-value/daily-activity-chart/daily-activity-chart.component.ts b/frontend/src/app/main/copilot/copilot-value/daily-activity-chart/daily-activity-chart.component.ts index 621b835..8dddf9f 100644 --- a/frontend/src/app/main/copilot/copilot-value/daily-activity-chart/daily-activity-chart.component.ts +++ b/frontend/src/app/main/copilot/copilot-value/daily-activity-chart/daily-activity-chart.component.ts @@ -116,14 +116,12 @@ export class DailyActivityChartComponent implements OnChanges { } ngOnChanges() { - console.log('old', this.chartOptions); if (this.activity && this.metrics) { this._chartOptions = this.highchartsService.transformMetricsToDailyActivityLine(this.activity, this.metrics); this.chartOptions = { ...this.chartOptions, ...this._chartOptions }; - console.log('new', this.chartOptions); this.updateFlag = true; } } diff --git a/frontend/src/app/main/copilot/copilot-value/value.component.html b/frontend/src/app/main/copilot/copilot-value/value.component.html index 10c6ced..4d67e83 100644 --- a/frontend/src/app/main/copilot/copilot-value/value.component.html +++ b/frontend/src/app/main/copilot/copilot-value/value.component.html @@ -2,6 +2,25 @@
diff --git a/frontend/src/app/main/copilot/copilot-value/value.component.ts b/frontend/src/app/main/copilot/copilot-value/value.component.ts index 4ee67f2..5d2d8ab 100644 --- a/frontend/src/app/main/copilot/copilot-value/value.component.ts +++ b/frontend/src/app/main/copilot/copilot-value/value.component.ts @@ -6,6 +6,8 @@ import { DailyActivityChartComponent } from './daily-activity-chart/daily-activi import { TimeSavedChartComponent } from './time-saved-chart/time-saved-chart.component'; import { CopilotMetrics } from '../../../services/metrics.service.interfaces'; import { MetricsService } from '../../../services/metrics.service'; +import { FormControl } from '@angular/forms'; +import { combineLatest, startWith } from 'rxjs'; @Component({ selector: 'app-value', @@ -22,14 +24,22 @@ import { MetricsService } from '../../../services/metrics.service'; export class CopilotValueComponent implements OnInit { activityData?: ActivityResponse; metricsData?: CopilotMetrics[]; + daysInactive = new FormControl(30); + adoptionFidelity = new FormControl<'day' | 'hour'>('hour'); + constructor( private seatService: SeatService, private metricsService: MetricsService ) { } ngOnInit() { - this.seatService.getActivity().subscribe(data => { - this.activityData = data; + combineLatest([ + this.daysInactive.valueChanges.pipe(startWith(this.daysInactive.value || 30)), + this.adoptionFidelity.valueChanges.pipe(startWith(this.adoptionFidelity.value || 'day')) + ]).subscribe(([days, fidelity]) => { + this.seatService.getActivity(days || 30, fidelity || 'day').subscribe(data => { + this.activityData = data; + }); }); this.metricsService.getMetrics().subscribe(data => { this.metricsData = data; diff --git a/frontend/src/app/services/highcharts.service.ts b/frontend/src/app/services/highcharts.service.ts index cd683a8..cd6fbc8 100644 --- a/frontend/src/app/services/highcharts.service.ts +++ b/frontend/src/app/services/highcharts.service.ts @@ -260,9 +260,9 @@ export class HighchartsService { }; Object.entries(activity).forEach(([date, dateData]) => { - const currentMetrics = metrics.find(m => m.date.startsWith(date)); + console.log(date, date.slice(0, 10), metrics) + const currentMetrics = metrics.find(m => m.date.startsWith(date.slice(0, 10))); if (currentMetrics?.copilot_ide_code_completions) { - console.log(`date: ${date} total_code_suggestions: ${currentMetrics.copilot_ide_code_completions.total_code_suggestions} totalActive: ${dateData.totalActive} percentage: ${(currentMetrics.copilot_ide_code_completions.total_code_suggestions / dateData.totalActive)}`); (dailyActiveIdeCompletionsSeries.data).push({ x: new Date(date).getTime(), y: (currentMetrics.copilot_ide_code_completions.total_code_suggestions / dateData.totalActive), diff --git a/frontend/src/app/services/seat.service.ts b/frontend/src/app/services/seat.service.ts index 786ed8e..ed39184 100644 --- a/frontend/src/app/services/seat.service.ts +++ b/frontend/src/app/services/seat.service.ts @@ -27,10 +27,11 @@ export class SeatService { return this.http.get(`${this.apiUrl}`); } - getActivity(daysInactive = 30) { + getActivity(daysInactive = 30, precision: 'hour' | 'day' = 'day') { return this.http.get(`${this.apiUrl}/activity`, { params: { + precision, daysInactive: daysInactive.toString() } }