Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions apps/devmx/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { RouterOutlet } from '@angular/router';
import { NavigationEnd, Router, RouterOutlet } from '@angular/router';
import { AnalyticsService } from '@devmx/shared-ui-global/analytics';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Env } from '@devmx/shared-api-interfaces/client';
import { filter, map, pairwise, startWith } from 'rxjs';
import { Component } from '@angular/core';

@Component({
Expand All @@ -13,4 +17,22 @@ import { Component } from '@angular/core';
`,
imports: [RouterOutlet],
})
export class AppComponent {}
export class AppComponent {
constructor(env: Env, analyticsService: AnalyticsService, router: Router) {
const routes$ = router.events.pipe(
filter((event) => event instanceof NavigationEnd),
map((e) => e.urlAfterRedirects),
startWith(''),
pairwise()
);

routes$
.pipe(
filter(() => env.prod),
takeUntilDestroyed()
)
.subscribe(([, toUrl]) => {
analyticsService.locationChanged(toUrl);
});
}
}
13 changes: 2 additions & 11 deletions apps/devmx/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
import { env } from './envs/env';
import './app/utils/google-tag';

if (env.prod) {
document.body.appendChild(
document.createElement('script', { is: 'google-tag' })
);
}

bootstrapApplication(AppComponent, appConfig).catch((err) =>
console.error(err)
);
bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
import { GithubContributor } from '@devmx/shared-api-interfaces';
import { MatChip, MatChipAvatar } from '@angular/material/chips';

@Component({
selector: 'devmx-contributors',
template: `
<marquee [scrollAmount]="4">
@for (contributor of data(); track contributor.id) {
<mat-chip>
<img matChipAvatar [src]="contributor.avatar_url" />
{{ contributor.login }}
</mat-chip>
}
</marquee>
`,
styles: `
:host {
display: flex;
flex-direction: column;

mat-chip {
margin-right: 1em;
}
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [MatChip, MatChipAvatar],
})
export class ContributorsComponent {
data = input<GithubContributor[]>([]);
}
1 change: 1 addition & 0 deletions packages/account/feature-shell/src/lib/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export * from './contributor-card-list/contributor-card-list.component';
export * from './album-card-list/album-card-list.component';
export * from './editable-photo/editable-photo.component';
export * from './editable-roles/editable-roles.component';
export * from './contributors/contributors.component';
export * from './social-icon/social-icon.component';
Original file line number Diff line number Diff line change
@@ -1,29 +1,4 @@
<header class="banner">
<div class="radio">
<audio #audioRef>
<source src="audios/loucos.mp3" type="audio/mp3" />
</audio>

<button mat-icon-button (click)="audioRef.muted = !audioRef.muted">
@if (audioRef.muted) {
<devmx-icon name="music/volume-off" />
} @else {
<devmx-icon name="music/volume" />
}
</button>

<button
mat-icon-button
(click)="audioRef.paused ? audioRef.play() : audioRef.pause()"
>
@if (audioRef.paused) {
<devmx-icon name="music/play-circle" />
} @else {
<devmx-icon name="music/pause-circle" />
}
</button>
</div>
</header>
<header class="banner"></header>

<div class="cards">
<section class="event-container">
Expand Down Expand Up @@ -60,7 +35,9 @@
} @placeholder {
<devmx-skeleton [rows]="3" />
}
</section>

<section>
<!-- -->

@defer (on timer(500ms)) {
Expand All @@ -73,16 +50,8 @@
<devmx-skeleton [rows]="4" />
}
</section>

<section class="contribution-cards">
@defer (on timer(500ms)) {
<!-- -->
@if (githubFacade.contributors$ | async; as contributors) {
<devmx-contributor-card-list [data]="contributors" />
}
<!-- -->
} @placeholder {
<devmx-skeleton [rows]="3" />
}
</section>
</div>

@if (githubFacade.contributors$ | async; as contributors) {
<devmx-contributors [data]="contributors" />
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { PresentationFacade } from '@devmx/presentation-data-access';
import { SkeletonComponent } from '@devmx/shared-ui-global/skeleton';
import { EventCardListComponent } from '@devmx/event-ui-shared';
import { JobOpeningFacade } from '@devmx/career-data-access';
import { IconComponent } from '@devmx/shared-ui-global/icon';
import { MatButtonModule } from '@angular/material/button';
import { GithubFacade } from '@devmx/shared-data-access';
import { MatCardModule } from '@angular/material/card';
Expand All @@ -14,7 +13,7 @@ import { AsyncPipe } from '@angular/common';
import {
AlbumCardListComponent,
JobOpeningCardListComponent,
ContributorCardListComponent,
ContributorsComponent,
} from '../../components';
@Component({
selector: 'devmx-home',
Expand All @@ -23,14 +22,13 @@ import {
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
PresentationCardListComponent,
ContributorCardListComponent,
JobOpeningCardListComponent,
EventCardListComponent,
AlbumCardListComponent,
ContributorsComponent,
SkeletonComponent,
MatCardModule,
MatButtonModule,
IconComponent,
MatCardModule,
AsyncPipe,
],
standalone: true,
Expand Down
4 changes: 4 additions & 0 deletions packages/shared/api-interfaces/src/client/envs/env.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export abstract class Env {
abstract prod: boolean

abstract api: {
url: string;
};
Expand All @@ -12,4 +14,6 @@ export abstract class Env {
url: string;
};
};

abstract googleTag: string
}
3 changes: 3 additions & 0 deletions packages/shared/ui-global/analytics/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @devmx/shared-ui-global/analytics

Secondary entry point of `@devmx/shared-ui-global`. It can be used by importing from `@devmx/shared-ui-global/analytics`.
5 changes: 5 additions & 0 deletions packages/shared/ui-global/analytics/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"lib": {
"entryFile": "src/index.ts"
}
}
3 changes: 3 additions & 0 deletions packages/shared/ui-global/analytics/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

export * from './lib/error-report-handler'
export * from './lib/analytics.service'
81 changes: 81 additions & 0 deletions packages/shared/ui-global/analytics/src/lib/analytics.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Env } from '@devmx/shared-api-interfaces/client';
import { formatErrorEventForAnalytics } from './utils';
import { Injectable } from '@angular/core';

declare global {
interface Window {
dataLayer?: unknown[];
gtag?(...args: unknown[]): void;
}
}

@Injectable({ providedIn: 'root' })
export class AnalyticsService {
private previousUrl: string | undefined;

constructor(private env: Env) {
if (env.prod) {
this.#installGlobalSiteTag();
this.#installWindowErrorHandler();
}
}

reportError(description: string, fatal = true) {
// Limit descriptions to maximum of 150 characters.
// See: https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#exd.
description = description.substring(0, 150);

this.#gtag('event', 'exception', { description: description, fatal });
}

locationChanged(url: string) {
this.#sendPage(url);
}

#sendPage(url: string) {
// Won't re-send if the url hasn't changed.
if (url === this.previousUrl) {
return;
}
this.previousUrl = url;
}

#gtag(...args: unknown[]) {
if (window.gtag) {
window.gtag(...args);
}
}

#installGlobalSiteTag() {
const url = `https://www.googletagmanager.com/gtag/js?id=${this.env.googleTag}`;

// Note: This cannot be an arrow function as `gtag.js` expects an actual `Arguments`
// instance with e.g. `callee` to be set. Do not attempt to change this and keep this
// as much as possible in sync with the tracking code snippet suggested by the Google
// Analytics 4 web UI under `Data Streams`.
window.dataLayer = window.dataLayer || [];
window.gtag = function (...params: unknown[]) {
window.dataLayer?.push(params);
};
window.gtag('js', new Date());

// Configure properties before loading the script. This is necessary to avoid
// loading multiple instances of the gtag JS scripts.
window.gtag('config', this.env.googleTag);

if (!this.env.prod) {
return;
}

const el = window.document.createElement('script');
el.async = true;
el.src = url;
window.document.head.appendChild(el);
}

#installWindowErrorHandler() {
window.addEventListener('error', (event) =>
this.reportError(formatErrorEventForAnalytics(event), true)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ErrorHandler, Injectable } from '@angular/core';
import { AnalyticsService } from './analytics.service';
import { formatErrorForAnalytics } from './utils';

@Injectable()
export class AnalyticsErrorReportHandler extends ErrorHandler {
constructor(private _analytics: AnalyticsService) {
super();
}

override handleError(error: ErrorHandler) {
super.handleError(error);

// Report the error in Google Analytics.
if (error instanceof Error) {
this._analytics.reportError(formatErrorForAnalytics(error));
} else {
this._analytics.reportError(error.toString());
}
}
}
34 changes: 34 additions & 0 deletions packages/shared/ui-global/analytics/src/lib/utils/format-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export function formatErrorEventForAnalytics(event: ErrorEvent): string {
const { message, filename, colno, lineno, error } = event;

if (error instanceof Error) {
return formatErrorForAnalytics(error);
}

const info = `${filename}:${lineno || '?'}:${colno || '?'}`;
return `${stripErrorMessagePrefix(message)} \n ${info}`;
}

export function formatErrorForAnalytics(error: Error): string {
let stack = '<no-stack>';

if (error.stack) {
stack = stripErrorMessagePrefix(error.stack)
// strip the message from the stack trace, if present
.replace(error.message + '\n', '')
// strip leading spaces
.replace(/^ +/gm, '')
// strip all leading "at " for each frame
.replace(/^at /gm, '')
// replace long urls with just the last segment: `filename:line:column`
.replace(/(?: \(|@)http.+\/([^/)]+)\)?(?:\n|$)/gm, '@$1\n')
// replace "eval code" in Edge
.replace(/ *\(eval code(:\d+:\d+)\)(?:\n|$)/gm, '@???$1\n');
}

return `${error.message}\n${stack}`;
}

function stripErrorMessagePrefix(input: string): string {
return input.replace(/^(Uncaught )?Error: /, '');
}
1 change: 1 addition & 0 deletions packages/shared/ui-global/analytics/src/lib/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './format-error';
3 changes: 3 additions & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@
"@devmx/shared-data-source": ["packages/shared/data-source/src/index.ts"],
"@devmx/shared-resource": ["packages/shared/resource/src/index.ts"],
"@devmx/shared-ui-global": ["packages/shared/ui-global/src/index.ts"],
"@devmx/shared-ui-global/analytics": [
"packages/shared/ui-global/analytics/src/index.ts"
],
"@devmx/shared-ui-global/bash": [
"packages/shared/ui-global/bash/src/index.ts"
],
Expand Down
Loading