• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

michaelmbugua-me / DataAnalyticsPlatform / #13

28 Aug 2025 09:30AM UTC coverage: 26.838% (+2.1%) from 24.769%
#13

push

github

michaelmbugua-me
feat: Overall Improvement in terms of responsiveness
- Clean up styling to ensure the portal is responsive on mobile devices.

85 of 743 branches covered (11.44%)

Branch coverage included in aggregate %.

9 of 12 new or added lines in 4 files covered. (75.0%)

566 existing lines in 10 files now uncovered.

488 of 1392 relevant lines covered (35.06%)

1.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

18.14
/src/app/features/DataExplorerModule/RawEvents/RawEventsComponent.ts
1
import {Component, inject, OnInit, signal, computed, Signal, ChangeDetectionStrategy} from '@angular/core';
1✔
2
import {AgGridAngular} from 'ag-grid-angular';
1✔
3
import {ColDef, GridOptions, GridReadyEvent, ValueFormatterParams, CellClassParams} from 'ag-grid-community';
4
import {Button, ButtonDirective, ButtonIcon, ButtonLabel} from 'primeng/button';
1✔
5
import {Select} from 'primeng/select';
1✔
6
import {FormsModule} from '@angular/forms';
1✔
7
import {DataService} from '../../../core/services/DataService';
1✔
8
import {FilterDrawerComponent} from '../../shared/components/filter-drawer';
1✔
9
import {
10
  RawEvent,
11
  AnalyticsEvent,
12
  PerformanceEvent,
13
  CrashEvent,
14
  EventSource,
15
  Platform,
1✔
16
  Country,
1✔
17
  ReleaseChannel
1✔
18
} from '../../../core/models/DataModels';
1✔
19
import { getSourceCellStyle, getReleaseChannelStyle, getDurationCellStyle } from '../../shared/utils/gridCellStyles';
1✔
20
import { AllCommunityModule, ModuleRegistry } from 'ag-grid-community';
1✔
21
import {DataExportationService} from '../../../core/services/DataExportationService';
1✔
22
import { FiltersService } from '../../../core/services/FiltersService';
1✔
23
import { evaluateQuery } from '../../../core/utils/query';
24
import { PageHeaderComponent } from '../../shared/components/PageHeaderComponent/PageHeaderComponent';
25

1✔
26
// Register AG Grid modules lazily for this feature chunk
27
ModuleRegistry.registerModules([AllCommunityModule]);
28

29
@Component({
30
  selector: 'app-raw-events-component',
31
  templateUrl: './RawEventsComponent.html',
32
  imports: [
33
    AgGridAngular,
34
    ButtonDirective,
35
    ButtonIcon,
36
    ButtonLabel,
37
    FormsModule,
38
    Button,
39
    FilterDrawerComponent,
40
    Select,
41
    PageHeaderComponent
42
  ],
43
  providers: [],
44
  styleUrls: ['./RawEventsComponent.scss'],
45
  standalone: true,
46
  changeDetection: ChangeDetectionStrategy.OnPush,
47
})
1✔
48
export class RawEventsComponent implements OnInit {
49

50
  filtersService = inject(FiltersService);
51

1✔
52
  // Facet selection signals for easier user interaction
53
  selectedSource = signal<string | null>(null);
54
  selectedPlatform = signal<string | null>(null);
UNCOV
55
  selectedCountry = signal<string | null>(null);
×
56
  selectedReleaseChannel = signal<string | null>(null);
57

58
  // Options for dropdowns derived from current data
59
  sourceOptions = computed(() => uniqSorted(this.data().map(r => r.source).filter(Boolean)));
60
  platformOptions = computed(() => uniqSorted(this.data().map(r => r.platform).filter(Boolean)));
UNCOV
61
  countryOptions = computed(() => uniqSorted(this.data().map(r => r.country).filter(Boolean)));
×
62
  releaseChannelOptions = computed(() => uniqSorted(this.data().map(r => r.release_channel).filter(Boolean)));
63

64
  // Saved config names
65
  savedConfigNames = computed(() => (this.filtersService.savedConfigs() || []).map(c => c.name));
66

67
  // Derived filtered data based on search + custom query + facet dropdowns
×
68
  public viewData: Signal<RawEvent[]> = computed(() => {
69
    const base = this.data();
70
    const search = (this.filtersService.searchText() || '').toLowerCase();
71
    const query = this.filtersService.customQuery() || '';
72

73
    const src = this.selectedSource();
1✔
74
    const plat = this.selectedPlatform();
75
    const ctry = this.selectedCountry();
76
    const rel = this.selectedReleaseChannel();
1✔
77

1✔
78
    const bySearch = (row: RawEvent) => {
1✔
79
      if (!search) return true;
1✔
80
      // simple: stringify limited fields for performance
81
      const hay = [row.id, row.event_name, row.platform, row.country, row.app_id, row.source, row.release_channel]
82
        .map(v => String(v ?? '')).join(' ').toLowerCase();
1✔
83
      return hay.includes(search);
1✔
84
    };
1✔
85

1✔
86
    const byQuery = (row: RawEvent) => {
87
      if (!query.trim()) return true;
88
      try { return evaluateQuery(row as any, query); } catch { return false; }
1!
89
    };
90

1✔
UNCOV
91
    const byFacets = (row: RawEvent) =>
×
UNCOV
92
      (!src || row.source === src) &&
×
UNCOV
93
      (!plat || row.platform === plat) &&
×
94
      (!ctry || row.country === ctry) &&
95
      (!rel || row.release_channel === rel);
×
UNCOV
96

×
UNCOV
97
    return base.filter(r => bySearch(r) && byQuery(r) && byFacets(r));
×
UNCOV
98
  });
×
99

UNCOV
100
  private dataService = inject(DataService);
×
UNCOV
101
  private dataExportationService = inject(DataExportationService);
×
102

×
103
  // Type-safe data with proper typing
×
UNCOV
104
  public data: Signal<RawEvent[]> = this.dataService.filteredRawData;
×
105
  error: Signal<string | null> = this.dataService.error;
106

UNCOV
107
  filters!: Filter[];
×
UNCOV
108
  visible = signal(false);
×
109

×
110
  // Computed signals for different event types (optional - for additional functionality)
111
  analyticsEvents = computed(() =>
UNCOV
112
    this.data()?.filter((event): event is AnalyticsEvent => event.source === 'analytics') ?? []
×
113
  );
×
114

115
  performanceEvents = computed(() =>
116
    this.data()?.filter((event): event is PerformanceEvent => event.source === 'performance') ?? []
117
  );
118

×
119
  crashEvents = computed(() =>
120
    this.data()?.filter((event): event is CrashEvent => event.source === 'crash') ?? []
121
  );
1✔
122

1✔
123
  // Enhanced column definitions with type-aware formatters
124
  columnDefs: ColDef<RawEvent>[] = [
1✔
125
    {
1✔
126
      field: 'id',
1✔
127
      headerName: 'ID',
128
      filter: 'agTextColumnFilter',
1✔
129
      minWidth: 200
130
    },
131
    {
132
      field: 'timestamp',
133
      headerName: 'Timestamp',
134
      filter: 'agDateColumnFilter',
135
      minWidth: 180,
136
      valueFormatter: (params: any) =>
137
        new Date(params.value).toLocaleString()
138
    },
139
    {
140
      field: 'day',
UNCOV
141
      headerName: 'Day',
×
142
      filter: 'agDateColumnFilter',
143
      minWidth: 120
144
    },
145
    {
146
      field: 'hour',
147
      headerName: 'Hour',
148
      filter: 'agNumberColumnFilter',
149
      minWidth: 80
150
    },
151
    {
152
      field: 'source',
153
      headerName: 'Source',
154
      filter: 'agTextColumnFilter',
155
      minWidth: 160,
156
      cellStyle: (params: CellClassParams) => getSourceCellStyle(params.value as EventSource)
157
    },
158
    {
159
      field: 'event_name',
UNCOV
160
      headerName: 'Event Name',
×
161
      filter: 'agTextColumnFilter',
162
      minWidth: 150
163
    },
164
    {
165
      field: 'platform',
166
      headerName: 'Platform',
167
      filter: 'agTextColumnFilter',
168
      minWidth: 100,
169
      valueFormatter: (params: any) => this.getPlatformIcon(params.value)
170
    },
171
    {
172
      field: 'app_id',
UNCOV
173
      headerName: 'App ID',
×
174
      filter: 'agTextColumnFilter',
175
      minWidth: 140
176
    },
177
    {
178
      field: 'country',
179
      headerName: 'Country',
180
      filter: 'agTextColumnFilter',
181
      minWidth: 100
182
    },
183
    {
184
      field: 'release_channel',
185
      headerName: 'Release',
186
      filter: 'agTextColumnFilter',
187
      minWidth: 100,
188
      cellStyle: (params: CellClassParams) => getReleaseChannelStyle(params.value as ReleaseChannel)
189
    },
190
    {
191
      field: 'session_id',
UNCOV
192
      headerName: 'Session ID',
×
193
      filter: 'agTextColumnFilter',
194
      minWidth: 160
195
    },
196
    {
197
      field: 'user_pseudo_id',
198
      headerName: 'User ID',
199
      filter: 'agTextColumnFilter',
200
      minWidth: 160
201
    },
202
    // Conditional columns based on event type
203
    {
204
      field: 'analytics_event',
205
      headerName: 'Analytics Event',
206
      filter: 'agTextColumnFilter',
207
      minWidth: 150,
208
      hide: false // Show/hide based on filters
209
    },
210
    {
211
      field: 'duration_ms',
212
      headerName: 'Duration (ms)',
213
      filter: 'agNumberColumnFilter',
214
      minWidth: 160,
215
      valueFormatter: (params: ValueFormatterParams<RawEvent, number | undefined>) =>
216
        params.value ? `${params.value}ms` : '-',
217
      cellStyle: (params: CellClassParams) => getDurationCellStyle(params.value as number)
218
    },
219
    {
UNCOV
220
      field: 'status_code',
×
UNCOV
221
      headerName: 'Status',
×
222
      filter: 'agNumberColumnFilter',
223
      minWidth: 100,
224
      cellStyle: (params: CellClassParams) => this.getStatusCodeStyle(params.value as number)
225
    },
226
    {
227
      field: 'crash_type',
UNCOV
228
      headerName: 'Crash Type',
×
229
      filter: 'agTextColumnFilter',
230
      minWidth: 160,
231
      cellStyle: { color: '#dc3545', fontWeight: 'bold' }
232
    }
233
  ];
234

235
  public defaultColDef: ColDef<RawEvent> = {
236
    sortable: true,
237
    resizable: true,
238
    filter: true
239
  };
1✔
240

241
  public gridOptions: GridOptions<RawEvent> = {
242
    rowModelType: 'clientSide',
243
    pagination: true,
244
    paginationPageSize: 100,
245
    enableCellTextSelection: true,
1✔
246
    ensureDomOrder: true,
247
    animateRows: true,
248
    suppressMenuHide: true,
249
    domLayout: 'normal'
250
  };
251

252
  async ngOnInit() {
253
    // restore selected config application on init if needed
254
    const sel = this.filtersService.selectedConfigName();
255
    if (sel) this.applySelectedConfig(sel);
256
    this.filters = [
UNCOV
257
      {name: 'Today\'s records', code: 'TODAY'},
×
UNCOV
258
      {name: 'This week\'s records', code: 'WEEK'},
×
259
      {name: 'This month\'s records', code: 'MONTH'},
260
      {name: 'This year\'s records', code: 'YEAR'}
261
    ];
UNCOV
262
  }
×
263

×
264
  onGridReady(params: GridReadyEvent<RawEvent>) {
265
    console.log('Grid is ready');
UNCOV
266
    params.api.sizeColumnsToFit();
×
267

268
    // Apply initial column visibility based on data
269
    this.updateColumnVisibility(params);
UNCOV
270
  }
×
271

272
  toggleFilterVisibility() {
273
    this.visible.update(v => !v);
274
  }
UNCOV
275

×
276
  // Type-safe helper methods using the model types
277
  private getPlatformIcon(platform: Platform): string {
278
    const icons = {
279
      ios: 'iOS',
UNCOV
280
      android: 'Android',
×
281
      web: 'Web'
282
    };
283
    return icons[platform] || platform;
UNCOV
284
  }
×
285

UNCOV
286
  private getStatusCodeStyle(statusCode: number): Record<string, string> {
×
287
    if (!statusCode) return {};
×
288

×
289
    if (statusCode >= 500) return { backgroundColor: '#d32f2f', color: 'white' };
×
290
    if (statusCode >= 400) return { backgroundColor: '#ff9800', color: 'white' };
291
    if (statusCode >= 200 && statusCode < 300) return { backgroundColor: '#4caf50', color: 'white' };
292
    return {};
UNCOV
293
  }
×
294

×
295
  private updateColumnVisibility(params: GridReadyEvent<RawEvent>) {
UNCOV
296
    const data = this.data();
×
297
    if (!data || data.length === 0) return;
×
298

×
299
    const hasAnalytics = data.some(event => event.source === 'analytics');
UNCOV
300
    const hasPerformance = data.some(event => event.source === 'performance');
×
301
    const hasCrashes = data.some(event => event.source === 'crash');
302

303
    params.api.applyColumnState({
304
      state: [
305
        { colId: 'analytics_event', hide: !hasAnalytics },
306
        { colId: 'duration_ms', hide: !hasPerformance },
307
        { colId: 'status_code', hide: !hasPerformance },
308
        { colId: 'crash_type', hide: !hasCrashes }
309
      ]
310
    });
×
311
  }
UNCOV
312

×
UNCOV
313
  exportRecords() {
×
314

×
315

316
    let cols: string[] = this.columnDefs.map((item: any) => {
×
317
      if(item['headerName'].toLowerCase() !== 'actions'){
318
        return item['field'].toUpperCase()
319
      } else {
UNCOV
320
        return ''
×
321
      }
322
    })
×
UNCOV
323

×
324
    cols = cols.filter(item => item !== '')
325

×
326
    let rowKeys: string[] = Object.keys(this.data()[0]);
327
    let arr: string[][]= []
328

×
329
    this.data().forEach((row: any) => {
330
      let temp: string[] = []
×
331
      cols.forEach(colKey => {
332
        rowKeys.forEach(key => {
×
UNCOV
333
          if(colKey == key.toUpperCase()){
×
UNCOV
334
            temp.push(row[key])
×
UNCOV
335
          }
×
336
        })
×
UNCOV
337
      })
×
UNCOV
338
      arr.push(temp)
×
339
    })
×
340

341
    this.dataExportationService.exportToPdf(cols, arr, 'raw_events.pdf')
342
  }
UNCOV
343

×
UNCOV
344
  // Type-safe filter methods
×
345
  filterBySource(source: EventSource) {
346
    // Implementation for filtering by event source
UNCOV
347
    console.log(`Filtering by source: ${source}`);
×
UNCOV
348
  }
×
UNCOV
349

×
350
  filterByPlatform(platform: Platform) {
×
UNCOV
351
    // Implementation for filtering by platform
×
UNCOV
352
    console.log(`Filtering by platform: ${platform}`);
×
353
  }
354

355
  filterByCountry(country: Country) {
356
    // Implementation for filtering by country
357
    console.log(`Filtering by country: ${country}`);
358
  }
359

360
  // Export functionality with proper typing
361
  exportAnalyticsEvents() {
×
UNCOV
362
    const analyticsData = this.analyticsEvents();
×
UNCOV
363
    console.log('Exporting analytics events:', analyticsData.length);
×
364
    // Implementation for export
365
  }
366

367
  exportPerformanceEvents() {
×
UNCOV
368
    const performanceData = this.performanceEvents();
×
UNCOV
369
    console.log('Exporting performance events:', performanceData.length);
×
UNCOV
370
    // Implementation for export
×
UNCOV
371
  }
×
372

×
373
  exportCrashEvents() {
374
    const crashData = this.crashEvents();
375
    console.log('Exporting crash events:', crashData.length);
376
    // Implementation for export
377
  }
378

×
379
  saveCurrentConfig() {
×
380
    const name = prompt('Save current filters as (name):');
×
UNCOV
381
    if (!name) return;
×
UNCOV
382
    this.filtersService.saveConfig(name, this.dataService.dateRange());
×
UNCOV
383
  }
×
384

×
385
  applySelectedConfig(name: string) {
386
    const cfg = this.filtersService.loadConfig(name);
387
    if (cfg?.dateRange) {
388
      try {
×
389
        const from = new Date(cfg.dateRange.from);
390
        const to = new Date(cfg.dateRange.to);
391
        if (!isNaN(from.getTime()) && !isNaN(to.getTime())) this.dataService.setDateRange({ from, to });
392
      } catch {}
UNCOV
393
    }
×
UNCOV
394
  }
×
395

×
396
  clearAllFilters() {
397
    this.filtersService.searchText.set('');
398
    this.filtersService.customQuery.set('');
399
    this.selectedSource.set(null);
400
    this.selectedPlatform.set(null);
401
    this.selectedCountry.set(null);
402
    this.selectedReleaseChannel.set(null);
403
    this.filtersService.selectedConfigName.set(null);
404
  }
405

406
  resetDateRangeToThisMonth() {
407
    this.dataService.resetToThisMonth();
408
  }
409
}
410

411
function uniqSorted(arr: (string | null | undefined)[]): string[] {
412
  const set = new Set<string>();
413
  for (const v of arr) { if (v != null) set.add(String(v)); }
414
  return Array.from(set).sort((a,b) => a.localeCompare(b));
415
}
416

417
interface Filter {
418
  name: string;
419
  code: string;
420
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc