DavidWells / analytics

Lightweight analytics abstraction layer for tracking page views, custom events, & identifying visitors
https://getanalytics.io
MIT License
2.43k stars 245 forks source link

TypeScript support for @analytics/google-tag-manager #99

Open hongbo-miao opened 3 years ago

hongbo-miao commented 3 years ago

I am using @analytics/google-tag-manager. I saw analytics have TypeScript support while @analytics/google-tag-manager not.

When add

import googleTagManager from '@analytics/google-tag-manager';

got

TS7016: Could not find a declaration file for module '@analytics/google-tag-manager'. '/hongbomiao.com/client/node_modules/@analytics/google-tag-manager/lib/analytics-plugin-google-tag-manager.cjs.js' implicitly has an 'any' type.   Try npm install @types/analytics__google-tag-manager if it exists or add a new declaration (.d.ts) file containing declare module '@analytics/google-tag-manager';

It would be great to add TypeScript in future. Thanks! : )

DavidWells commented 3 years ago

I would like to improve TS support for browser & node packages.

Running into some issues where the node/browser types are different but TS only allows for one type definition & doesn't follow the module resolution to grab the proper types.

From what I can tell, this might require publishing 2 separate packages. 1 for browser and 1 for serverside node... This is a bummer because it will double the amount of maintenance/packages for the project 😅

The solution proposed looks like a lot of work...

Will need to think on this one

GeoMarkou commented 3 years ago

As a workaround I added this code in a Types.ts file

declare module '@analytics/google-analytics' {
    type GoogleAnalyticsOptions = {
        /** Google Analytics site tracking Id */
        trackingId:string;

        /** Enable Google Analytics debug mode */
        debug?:boolean;

        /** Enable Anonymizing IP addresses sent to Google Analytics. See details below */
        anonymizeIp?:boolean;

        /** Map Custom dimensions to send extra information to Google Analytics. See details below */
        customDimensions?:object;

        /** Reset custom dimensions by key on analytics.page() calls. Useful for single page apps. */
        resetCustomDimensionsOnPage?:object;

        /** Mapped dimensions will be set to the page & sent as properties of all subsequent events on that page. If false, analytics will only pass custom dimensions as part of individual events */
        setCustomDimensionsToPage?:boolean;

        /** Custom tracker name for google analytics. Use this if you need multiple googleAnalytics scripts loaded */
        instanceName?:string;

        /** Custom URL for google analytics script, if proxying calls */
        customScriptSrc?:string;

        /** Additional cookie properties for configuring the ga cookie */
        cookieConfig?:object;

        /** Set custom google analytic tasks */
        tasks?:object;
    };

    type AnalyticsPlugin = {
        /** Name of plugin */
        name: string;

        /** exposed events of plugin */
        EVENTS?: any;

        /** Configuration of plugin */
        config?: any;

        /** Load analytics scripts method */
        initialize?: (...params: any[]) => any;

        /** Page visit tracking method */
        page?: (...params: any[]) => any;

        /** Custom event tracking method */
        track?: (...params: any[]) => any;

        /** User identify method */
        identify?: (...params: any[]) => any;

        /** Function to determine if analytics script loaded */
        loaded?: (...params: any[]) => any;

        /** Fire function when plugin ready */
        ready?: (...params: any[]) => any;
    };

    function GoogleAnalytics (options:GoogleAnalyticsOptions):AnalyticsPlugin;
    export default GoogleAnalytics;
}
saiichihashimoto commented 3 years ago

What can we do to help @DavidWells? It's unclear what the solution is here since it looks like the typescript files in this repo are generated.

DavidWells commented 3 years ago

I'm stuck on the issue where types can be different between server and client implementations

https://twitter.com/DavidWells/status/1340059740672442368

It seems like this isn't supported in typescript and this is a real bummer

GeoMarkou commented 3 years ago

Just some ideas: the way I've seen other libraries do it is to have a separate folders. If the code is completely different between the 2 this makes a lot of sense - then the programmer just imports the one that they want. e.g.

import Analytics from '@analytics/google-analytics/browser';
// or
import Analytics from '@analytics/google-analytics/node';

I've also seen libraries have a single class but they want to restrict some functions*. So you would have a single class, but export it twice with different types so that the programmer can choose what makes sense for them. e.g.

/** This is stuff that is shared to both node.js and browser */
type CommonModules = {
    GenericFunc: () => void;
}

/** Browser specific definitions */
type BrowserModules = CommonModules & {
    BrowserFunc: () => void;
}

/** Server specific definitions */
type ServerModules = CommonModules & {
    ServerFunc: () => void;
}

/** Write all your code */
const AllMyCode:BrowserModules&ServerModules = {
    GenericFunc: () => {},
    BrowserFunc: () => {},
    ServerFunc: () => {}
};

/** Restricted set of browser functions */
const BrowserCode = AllMyCode as BrowserModules;
export BrowserCode;

/** Restricted set of server functions */
const ServerCode = AllMyCode as ServerModules;
export ServerCode;

// Inside user code, they import which one they want
import { BrowserCode, ServerCode } from '@analytics/google-analytics';

Dunno if this helps or not

saiichihashimoto commented 3 years ago

So you would have a single class, but export it twice with different types so that the programmer can choose what makes sense for them. e.g.

Since you're not using typescript internally, there's something to be said here; you're NOT using typescript internally so it's ultimately about exporting types so users can use it. You could export a Browser Type, Node Type, and union type and have your functions use the union types. Being less strict is not ideal, but then you're allowing us to cast to the more specific types when we need to, which solves our issue much more than having no type at all.

xoscar commented 2 years ago

bump

livthomas commented 2 years ago

The original question was about @analytics/google-tag-manager. And although @GeoMarkou posted a really good workaround for @analytics/google-analytics, those types are a little bit different.

Here are the types for @analytics/google-tag-manager package:

declare module '@analytics/google-tag-manager' {
  type AnalyticsPlugin = import('analytics').AnalyticsPlugin;

  type GoogleTagManagerConfig = {
    auth?: string;
    containerId: string;
    customScriptSrc?: string;
    dataLayerName?: string;
    debug?: boolean;
    execution?: string;
    preview?: string;
  };

  function googleTagManager(config: GoogleTagManagerConfig): AnalyticsPlugin;
  export default googleTagManager;
}
santyas commented 2 years ago

The original question was about @analytics/google-tag-manager. And although @GeoMarkou posted a really good workaround for @analytics/google-analytics, those types are a little bit different.

Here are the types for @analytics/google-tag-manager package:

declare module '@analytics/google-tag-manager' {
  type AnalyticsPlugin = import('analytics').AnalyticsPlugin;

  type GoogleTagManagerConfig = {
    auth?: string;
    containerId: string;
    customScriptSrc?: string;
    dataLayerName?: string;
    debug?: boolean;
    execution?: string;
    preview?: string;
  };

  function googleTagManager(config: GoogleTagManagerConfig): AnalyticsPlugin;
  export default googleTagManager;
}

where do you insert that code? or GeoMarkou declare?

livthomas commented 2 years ago

@santyas You simply put it to something.d.ts file anywhere in your project and TypeScript will pick it up.

nifalconi commented 5 months ago

For the GA4, the declared module should be:

declare module '@analytics/google-analytics' {
  type GoogleAnalyticsOptions = {
      /** Google Analytics MEASUREMENT IDs */
      measurementIds: string[];

      /** Enable Google Analytics debug mode */
      debug?: boolean;

      /** The optional name for dataLayer object. Defaults to 'ga4DataLayer'. */
      dataLayerName?: string;

      /** The optional name for the global gtag function. Defaults to 'gtag'. */
      gtagName?: string;

      /** Configuration for gtag, including anonymizing IP and cookie settings */
      gtagConfig?: {
          anonymize_ip?: boolean;
          cookie_domain?: string;
          cookie_expires?: number;
          cookie_prefix?: string;
          cookie_update?: boolean;
          cookie_flags?: string;
      };

      /** Custom URL for google analytics script, if proxying calls */
      customScriptSrc?: string;
  };

  type AnalyticsPlugin = {
      /** Name of plugin */
      name: string;

      /** Exposed events of the plugin */
      EVENTS?: any;

      /** Configuration of the plugin */
      config?: any;

      /** Method to load analytics scripts */
      initialize?: (...params: any[]) => any;

      /** Page visit tracking method */
      page?: (...params: any[]) => any;

      /** Custom event tracking method */
      track?: (...params: any[]) => any;

      /** User identify method */
      identify?: (...params: any[]) => any;

      /** Function to determine if analytics script is loaded */
      loaded?: (...params: any[]) => any;

      /** Fire function when the plugin is ready */
      ready?: (...params: any[]) => any;
  };

  function GoogleAnalytics(options: GoogleAnalyticsOptions): AnalyticsPlugin;
  export default GoogleAnalytics;
}

This should be added on something.d.ts like @livthomas pointed out.

aprilmintacpineda commented 4 months ago

Unfortunately we still don't have this until today :(