Search code examples
angulartypescript-decoratorangular-decoratorangular2-decorators

The correct way to use decorators when logging services in Angular


I have two services one is an Auth service and the other is an Analytics service and they exist in different libraries. The analytics service is used to do event logging. One obvious way I could use it to log the auth.service is injecting the analytics service in the auth service but I'd don't want to use this method, I'd like to use the decorator strategy. What is the correct approach to achieve this? Also is it possible to use a decorator without interfering with the auth.service codebase?

[update]: I've implemented this decorator as shown below (snippet A). The decorator is located @ libs/state/analytics/src/lib/decorators/analytics.decorator.ts I would like to use it on the Auth service as shown in (snippet B).

Then in the decorator there is an analytics function I will call from the analytics.service.ts i.e. the logEvent() function. How do I inject the logEvent() function from analytics.service in this decorator( The main idea behind this is to log the errors and send them to segment for analytics).

Snippet A

export const Log = () => {

    return function catchError(target: any, propertyName: any, descriptor: any) {
        const method = descriptor.value;

        descriptor.value = function (...args: any) {
            try {
                return method.apply(target, args);
            } catch (error) {
                throw new Error(`Special error message: ${error}`);
       
            }
        };
    }
}

Snippet B: The usecase would be something like this.

  @Log()
  public async loginWithEmailAndPassword(email: string, password: string)
  {
    return this.afAuth.signInWithEmailAndPassword(email, password)
              .then(() => {
                this._logger.log(() => `AuthService.signInWithEmailAndPassword: Successfully logged user in with Email and Password.`);
              })
              .catch((error) => {
                this._throwError(error);
              });
  }

Auth Service libs/state/analytics/src/lib/services/auth.service';

    @Injectable({ providedIn: 'root' })
    export class AuthService {
      constructor(private afAuth: AngularFireAuth,
                  private afs: AngularFirestore,
                  private router: Router,
                  private _toastService: ToastService,
                  @Inject('ENVIRONMENT') private _env: AuthEnvironment
                  )
      {
       }
    
      public getAuth() {
        return this.afAuth;
      }
    
      public async resetPassword(email: string, langCode: string = 'en' )
      {
    
        firebase.auth().languageCode = langCode;
       
        const actionCodeSettings : firebase.auth.ActionCodeSettings = {
          url: this._env.baseUrl
        }
    
        return firebase.auth()
                   .sendPasswordResetEmail( email, actionCodeSettings )
                   .then(() => this._toastService.doSimpleToast('A password reset link has been sent to your email address.'))
                   .catch(() => this._toastService.doSimpleToast('An error occurred while attempting to reset your password. Please contact support.'));
      }



  public async loginWithEmailAndPassword(email: string, password: string)
  {
    return this.afAuth.signInWithEmailAndPassword(email, password)
              .then(() => {
                this._logger.log(() => `AuthService.signInWithEmailAndPassword: Successfully logged user in with Email and Password.`);
              })
              .catch((error) => {
                this._throwError(error);
              });
  }
    
  public createUserWithEmailAndPassword(displayName: string, email: string, password: string, userProfile: UserProfile, roles: Roles)
      {
        return this.afAuth
                   .createUserWithEmailAndPassword(email, password)
                   .then((res) => {
                      this._checkUpdateUserData(res.user, displayName, userProfile, roles);
                      return <User> <unknown> res.user;
                   })
                   .catch((error) => {
                     this._throwError(error);
                   });
      }
...

Analytics libs/authentication/auth/src/lib/services/analytics.service';

export class AnalyticsService {

  user$: Observable<User>;

  constructor(
    private _analytics: SegmentService,
    private _cacheService: CacheService,
    private _userService: UserService<User>) {
    this.user$ = this._userService.getUser().pipe(take(1));
  }


  public logEvent(event: TrackEvent) {
    this.user$.subscribe(user => {
      const userId = user?user.id: null;
      const userEmail = user?user.email: null;
      const displayName = user?user.displayName: null;
      const roles = user?user.roles: null;
      this._analytics.track(event.name, {
        ...event,
        property_id: propID,
        user_id: userId,
        email: userEmail,
        displayName: displayName,
        roles: roles,
      })
    });
  }

  identifyUser() {
    this.user$.subscribe((user: User) => {
      if (user) {
        const cachedUser = this._cacheService.getValueByKey('ajs_user_id');
        if (!cachedUser) {
          const traits = { userId: user.id, email: user.email, displayName: user.displayName }
          this._analytics.identify(user.id, traits);
        }
      }
    });
  };

...


}

Solution

  • I took some tries about this and ended with that result (simplified some classes of yours):

    First, we have AnalyticalService (logs user and logged metod params)

    @Injectable({providedIn: 'root'})
    export class AnalyticsService {
      user$: Observable<User> = of({ name: 'Yoda' });
    
      constructor() {}
    
      public logEvent(event: any) {
        this.user$.subscribe(user => {
          console.log('event', event);
          console.log('user', user);
        });
      }
    }
    
    export interface User {
      name: string;
    }
    

    In AuthService we are using decorator

    @Injectable({providedIn: 'root'})
    export class AuthService {
    
      constructor(private as: AnalyticsService) {}
    
      @Log()
      public async loginWithEmailAndPassword(email: string, password: string) {
        console.log('logged in');
      }
    }
    

    The main challenge here is passing injectable service into decorator:

    
    export function Log() {
      return (
        target: Object,
        propertyKey: string,
        descriptor: PropertyDescriptor
      ) => {
        const originalMethod = descriptor.value;
    
        descriptor.value = async function (...args) {
          const service = SharedModule.injector.get<AnalyticsService>(AnalyticsService);
    
          service.logEvent(args);
          const result = originalMethod.apply(this, args);
          return result;
        };
        return descriptor;
      };
    }
    

    To make this work we need additional tricky module which need to be imported in i.e. app.module

    @NgModule({
      declarations: [],
      imports: [],
      providers: [AnalyticsService]
    })
    export class SharedModule {
      static injector: Injector;
    
      constructor(injector: Injector) {
        SharedModule.injector = injector;
      }
    }
    

    The trick of service injection in decorator comes from https://stackoverflow.com/a/66921030/2677292

    Sample code of logging user along with method params by decorator is here