Skip to content

Commit a70516a

Browse files
committed
refactor: melhoria na implementação de page tracking
closed #96
1 parent 6e17b20 commit a70516a

File tree

15 files changed

+224
-56
lines changed

15 files changed

+224
-56
lines changed

apps/devmx/src/app/app.component.ts

+24-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { RouterOutlet } from '@angular/router';
1+
import { NavigationEnd, Router, RouterOutlet } from '@angular/router';
2+
import { AnalyticsService } from '@devmx/shared-ui-global/analytics';
3+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
4+
import { Env } from '@devmx/shared-api-interfaces/client';
5+
import { filter, map, pairwise, startWith } from 'rxjs';
26
import { Component } from '@angular/core';
37

48
@Component({
@@ -13,4 +17,22 @@ import { Component } from '@angular/core';
1317
`,
1418
imports: [RouterOutlet],
1519
})
16-
export class AppComponent {}
20+
export class AppComponent {
21+
constructor(env: Env, analyticsService: AnalyticsService, router: Router) {
22+
const routes$ = router.events.pipe(
23+
filter((event) => event instanceof NavigationEnd),
24+
map((e) => e.urlAfterRedirects),
25+
startWith(''),
26+
pairwise()
27+
);
28+
29+
routes$
30+
.pipe(
31+
filter(() => env.prod),
32+
takeUntilDestroyed()
33+
)
34+
.subscribe(([, toUrl]) => {
35+
analyticsService.locationChanged(toUrl);
36+
});
37+
}
38+
}

apps/devmx/src/main.ts

+2-11
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,6 @@
11
import { bootstrapApplication } from '@angular/platform-browser';
22
import { AppComponent } from './app/app.component';
33
import { appConfig } from './app/app.config';
4-
import { env } from './envs/env';
5-
import './app/utils/google-tag';
64

7-
if (env.prod) {
8-
document.body.appendChild(
9-
document.createElement('script', { is: 'google-tag' })
10-
);
11-
}
12-
13-
bootstrapApplication(AppComponent, appConfig).catch((err) =>
14-
console.error(err)
15-
);
5+
bootstrapApplication(AppComponent, appConfig)
6+
.catch((err) => console.error(err));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
2+
import { GithubContributor } from '@devmx/shared-api-interfaces';
3+
import { MatChip, MatChipAvatar } from '@angular/material/chips';
4+
5+
@Component({
6+
selector: 'devmx-contributors',
7+
template: `
8+
<marquee [scrollAmount]="4">
9+
@for (contributor of data(); track contributor.id) {
10+
<mat-chip>
11+
<img matChipAvatar [src]="contributor.avatar_url" />
12+
{{ contributor.login }}
13+
</mat-chip>
14+
}
15+
</marquee>
16+
`,
17+
styles: `
18+
:host {
19+
display: flex;
20+
flex-direction: column;
21+
22+
mat-chip {
23+
margin-right: 1em;
24+
}
25+
}
26+
`,
27+
changeDetection: ChangeDetectionStrategy.OnPush,
28+
imports: [MatChip, MatChipAvatar],
29+
})
30+
export class ContributorsComponent {
31+
data = input<GithubContributor[]>([]);
32+
}

packages/account/feature-shell/src/lib/components/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ export * from './contributor-card-list/contributor-card-list.component';
33
export * from './album-card-list/album-card-list.component';
44
export * from './editable-photo/editable-photo.component';
55
export * from './editable-roles/editable-roles.component';
6+
export * from './contributors/contributors.component';
67
export * from './social-icon/social-icon.component';
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,4 @@
1-
<header class="banner">
2-
<div class="radio">
3-
<audio #audioRef>
4-
<source src="audios/loucos.mp3" type="audio/mp3" />
5-
</audio>
6-
7-
<button mat-icon-button (click)="audioRef.muted = !audioRef.muted">
8-
@if (audioRef.muted) {
9-
<devmx-icon name="music/volume-off" />
10-
} @else {
11-
<devmx-icon name="music/volume" />
12-
}
13-
</button>
14-
15-
<button
16-
mat-icon-button
17-
(click)="audioRef.paused ? audioRef.play() : audioRef.pause()"
18-
>
19-
@if (audioRef.paused) {
20-
<devmx-icon name="music/play-circle" />
21-
} @else {
22-
<devmx-icon name="music/pause-circle" />
23-
}
24-
</button>
25-
</div>
26-
</header>
1+
<header class="banner"></header>
272

283
<div class="cards">
294
<section class="event-container">
@@ -60,7 +35,9 @@
6035
} @placeholder {
6136
<devmx-skeleton [rows]="3" />
6237
}
38+
</section>
6339

40+
<section>
6441
<!-- -->
6542

6643
@defer (on timer(500ms)) {
@@ -73,16 +50,8 @@
7350
<devmx-skeleton [rows]="4" />
7451
}
7552
</section>
76-
77-
<section class="contribution-cards">
78-
@defer (on timer(500ms)) {
79-
<!-- -->
80-
@if (githubFacade.contributors$ | async; as contributors) {
81-
<devmx-contributor-card-list [data]="contributors" />
82-
}
83-
<!-- -->
84-
} @placeholder {
85-
<devmx-skeleton [rows]="3" />
86-
}
87-
</section>
8853
</div>
54+
55+
@if (githubFacade.contributors$ | async; as contributors) {
56+
<devmx-contributors [data]="contributors" />
57+
}

packages/account/feature-shell/src/lib/containers/home/home.container.ts

+3-5
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { PresentationFacade } from '@devmx/presentation-data-access';
44
import { SkeletonComponent } from '@devmx/shared-ui-global/skeleton';
55
import { EventCardListComponent } from '@devmx/event-ui-shared';
66
import { JobOpeningFacade } from '@devmx/career-data-access';
7-
import { IconComponent } from '@devmx/shared-ui-global/icon';
87
import { MatButtonModule } from '@angular/material/button';
98
import { GithubFacade } from '@devmx/shared-data-access';
109
import { MatCardModule } from '@angular/material/card';
@@ -14,7 +13,7 @@ import { AsyncPipe } from '@angular/common';
1413
import {
1514
AlbumCardListComponent,
1615
JobOpeningCardListComponent,
17-
ContributorCardListComponent,
16+
ContributorsComponent,
1817
} from '../../components';
1918
@Component({
2019
selector: 'devmx-home',
@@ -23,14 +22,13 @@ import {
2322
changeDetection: ChangeDetectionStrategy.OnPush,
2423
imports: [
2524
PresentationCardListComponent,
26-
ContributorCardListComponent,
2725
JobOpeningCardListComponent,
2826
EventCardListComponent,
2927
AlbumCardListComponent,
28+
ContributorsComponent,
3029
SkeletonComponent,
31-
MatCardModule,
3230
MatButtonModule,
33-
IconComponent,
31+
MatCardModule,
3432
AsyncPipe,
3533
],
3634
standalone: true,

packages/shared/api-interfaces/src/client/envs/env.ts

+4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
export abstract class Env {
2+
abstract prod: boolean
3+
24
abstract api: {
35
url: string;
46
};
@@ -12,4 +14,6 @@ export abstract class Env {
1214
url: string;
1315
};
1416
};
17+
18+
abstract googleTag: string
1519
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# @devmx/shared-ui-global/analytics
2+
3+
Secondary entry point of `@devmx/shared-ui-global`. It can be used by importing from `@devmx/shared-ui-global/analytics`.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"lib": {
3+
"entryFile": "src/index.ts"
4+
}
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
2+
export * from './lib/error-report-handler'
3+
export * from './lib/analytics.service'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { Env } from '@devmx/shared-api-interfaces/client';
2+
import { formatErrorEventForAnalytics } from './utils';
3+
import { Injectable } from '@angular/core';
4+
5+
declare global {
6+
interface Window {
7+
dataLayer?: unknown[];
8+
gtag?(...args: unknown[]): void;
9+
}
10+
}
11+
12+
@Injectable({ providedIn: 'root' })
13+
export class AnalyticsService {
14+
private previousUrl: string | undefined;
15+
16+
constructor(private env: Env) {
17+
if (env.prod) {
18+
this.#installGlobalSiteTag();
19+
this.#installWindowErrorHandler();
20+
}
21+
}
22+
23+
reportError(description: string, fatal = true) {
24+
// Limit descriptions to maximum of 150 characters.
25+
// See: https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#exd.
26+
description = description.substring(0, 150);
27+
28+
this.#gtag('event', 'exception', { description: description, fatal });
29+
}
30+
31+
locationChanged(url: string) {
32+
this.#sendPage(url);
33+
}
34+
35+
#sendPage(url: string) {
36+
// Won't re-send if the url hasn't changed.
37+
if (url === this.previousUrl) {
38+
return;
39+
}
40+
this.previousUrl = url;
41+
}
42+
43+
#gtag(...args: unknown[]) {
44+
if (window.gtag) {
45+
window.gtag(...args);
46+
}
47+
}
48+
49+
#installGlobalSiteTag() {
50+
const url = `https://www.googletagmanager.com/gtag/js?id=${this.env.googleTag}`;
51+
52+
// Note: This cannot be an arrow function as `gtag.js` expects an actual `Arguments`
53+
// instance with e.g. `callee` to be set. Do not attempt to change this and keep this
54+
// as much as possible in sync with the tracking code snippet suggested by the Google
55+
// Analytics 4 web UI under `Data Streams`.
56+
window.dataLayer = window.dataLayer || [];
57+
window.gtag = function (...params: unknown[]) {
58+
window.dataLayer?.push(params);
59+
};
60+
window.gtag('js', new Date());
61+
62+
// Configure properties before loading the script. This is necessary to avoid
63+
// loading multiple instances of the gtag JS scripts.
64+
window.gtag('config', this.env.googleTag);
65+
66+
if (!this.env.prod) {
67+
return;
68+
}
69+
70+
const el = window.document.createElement('script');
71+
el.async = true;
72+
el.src = url;
73+
window.document.head.appendChild(el);
74+
}
75+
76+
#installWindowErrorHandler() {
77+
window.addEventListener('error', (event) =>
78+
this.reportError(formatErrorEventForAnalytics(event), true)
79+
);
80+
}
81+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { ErrorHandler, Injectable } from '@angular/core';
2+
import { AnalyticsService } from './analytics.service';
3+
import { formatErrorForAnalytics } from './utils';
4+
5+
@Injectable()
6+
export class AnalyticsErrorReportHandler extends ErrorHandler {
7+
constructor(private _analytics: AnalyticsService) {
8+
super();
9+
}
10+
11+
override handleError(error: ErrorHandler) {
12+
super.handleError(error);
13+
14+
// Report the error in Google Analytics.
15+
if (error instanceof Error) {
16+
this._analytics.reportError(formatErrorForAnalytics(error));
17+
} else {
18+
this._analytics.reportError(error.toString());
19+
}
20+
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
export function formatErrorEventForAnalytics(event: ErrorEvent): string {
2+
const { message, filename, colno, lineno, error } = event;
3+
4+
if (error instanceof Error) {
5+
return formatErrorForAnalytics(error);
6+
}
7+
8+
const info = `${filename}:${lineno || '?'}:${colno || '?'}`;
9+
return `${stripErrorMessagePrefix(message)} \n ${info}`;
10+
}
11+
12+
export function formatErrorForAnalytics(error: Error): string {
13+
let stack = '<no-stack>';
14+
15+
if (error.stack) {
16+
stack = stripErrorMessagePrefix(error.stack)
17+
// strip the message from the stack trace, if present
18+
.replace(error.message + '\n', '')
19+
// strip leading spaces
20+
.replace(/^ +/gm, '')
21+
// strip all leading "at " for each frame
22+
.replace(/^at /gm, '')
23+
// replace long urls with just the last segment: `filename:line:column`
24+
.replace(/(?: \(|@)http.+\/([^/)]+)\)?(?:\n|$)/gm, '@$1\n')
25+
// replace "eval code" in Edge
26+
.replace(/ *\(eval code(:\d+:\d+)\)(?:\n|$)/gm, '@???$1\n');
27+
}
28+
29+
return `${error.message}\n${stack}`;
30+
}
31+
32+
function stripErrorMessagePrefix(input: string): string {
33+
return input.replace(/^(Uncaught )?Error: /, '');
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './format-error';

tsconfig.base.json

+3
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,9 @@
170170
"@devmx/shared-data-source": ["packages/shared/data-source/src/index.ts"],
171171
"@devmx/shared-resource": ["packages/shared/resource/src/index.ts"],
172172
"@devmx/shared-ui-global": ["packages/shared/ui-global/src/index.ts"],
173+
"@devmx/shared-ui-global/analytics": [
174+
"packages/shared/ui-global/analytics/src/index.ts"
175+
],
173176
"@devmx/shared-ui-global/bash": [
174177
"packages/shared/ui-global/bash/src/index.ts"
175178
],

0 commit comments

Comments
 (0)