Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Are there types available? #12

Closed
katsar0v opened this issue Jul 30, 2019 · 18 comments · Fixed by #41
Closed

Are there types available? #12

katsar0v opened this issue Jul 30, 2019 · 18 comments · Fixed by #41
Labels
help wanted Extra attention is needed

Comments

@katsar0v
Copy link

Hello guys! I am trying to use this analytics script in an existing typescript environment. I tested the vanilla js and it worked pretty well, now I'd like to integrate the tool in my existing typescript application.
My question is: are there any types available or is there documentation available for using this with TypeScript?
PS: I tried searching in the available documentation in github as well for existing types in npm / TypeSearch, nothing there.

@DavidWells
Copy link
Owner

Hey @katsar0v. Thanks for the feedback & issue.

Most of the interfaces are typed via JS doc inside of the core module https://github.com/DavidWells/analytics/blob/master/packages/analytics-core/src/index.js#L117-L123

From my (limited) understanding of typescript those should work? Ref

What happens when you try to use this in typescript right now?

@katsar0v
Copy link
Author

Hi @DavidWells , unfortunatelly this are only comments which the typescript interpreter cannot read, the following does not work:

import Analytics from 'analytics'
import googleAnalyticsPlugin from 'analytics-plugin-ga'

However I managed to run the script unter typescript, quite hacky, but it works index.ts:

interface AnalyticsEvent {
    [key: string]: any
}

// Adjusted from https://davidwalsh.name/javascript-loader
// Returns a promise when the element is loaded
function load_js(url: string) {
  return new Promise(function(resolve, reject) {
    var element = <HTMLScriptElement> document.createElement('script');
    element.async = true;
    element.type = 'text/javascript';
    element.src = url

    // Important success and error for the promise
    element.onload = function() {
      resolve(url);
    };
    element.onerror = function() {
      reject(url);
    };

    // Inject into document to kick off loading
    document.body.appendChild(element);
  });
}

declare global {
  interface Window {
    _analytics: any;
    analyticsGA: any;
  }
}

function load_analytics() {
  Promise.all([
    load_js('https://unpkg.com/analytics/dist/analytics.min.js'),
    load_js('https://unpkg.com/analytics-plugin-ga/dist/analytics-plugin-ga.min.js')
  ]).then(() => {
    run_analytics();
  }).catch(() => {
    console.log('There was a problem loading the analytics libraries!');
  });
}

function run_analytics() {
  const Analytics = window._analytics.init({
    app: 'analytics-test-1',
    version: 100,
    debug: true,
    plugins: [
      window.analyticsGA({
        trackingId: 'UA-65882609-1' // This is a real analytics id
      })
    ]
  });

  /* Attaching a listener for logging */
  Analytics.on('*', (event: AnalyticsEvent) => {
    console.log(`Event ${event.payload.type}`, event)
  })

  // This triggers the pageview event
  Analytics.page();
}

Of course, no type checking is possible with this.

@DavidWells
Copy link
Owner

Hey @katsar0v

What's the best way to support typescript types with the project?

I'm keen to help but also worried about adding additional complexity + maintenance.

How are other libraries handling this?

@katsar0v
Copy link
Author

katsar0v commented Aug 1, 2019

I guess we need an npm package with just type descriptions, as you can see I created really basic and abstract interface for the listener:

interface AnalyticsEvent {
    [key: string]: any
}

I guess we just need two files:

  • describe the Analytics class
  • describe the plugin

The usage and injection in the dom would still remain via html (am not really sure how this would happen here). The other (and better) possibility is to have a full npm package, where you just import (like for node.js) so it could work just using:

import Analytics from 'analytics'
import googleAnalyticsPlugin from 'analytics-plugin-ga'

I am happy to contribute to this project, I just cannot spend really much time, but am happy to answer any question. A good example to start is jquery for example: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/5344bfc80508c53a23dae37b860fb0c905ff7b24/types/jquery

@tommedema
Copy link
Contributor

FYI here's an incomplete typings file I created for my own purposes

https://gist.github.com/tommedema/b1e51013f709e42e1fb577b5d537a377

Very incomplete because I don't need most options like callbacks etc

@DavidWells DavidWells added the help wanted Extra attention is needed label Aug 25, 2019
@katsar0v
Copy link
Author

node_modules/analytics/lib/types.d.ts:496:21 - error TS1254: A 'const' initializer in an ambient context must be a string or numeric literal or literal enum reference.

496 export const init = analytics;
                        ~~~~~~~~~

node_modules/analytics/lib/types.d.ts:498:26 - error TS1254: A 'const' initializer in an ambient context must be a string or numeric literal or literal enum reference.

498 export const Analytics = analytics;
                             ~~~~~~~~~

node_modules/analytics/lib/types.d.ts:500:1 - error TS2309: An export assignment cannot be used in a module with other exported elements.

500 export = analytics;
    ~~~~~~~~~~~~~~~~~~~

This is what I get when using npm install analytics --save and afterwards import Analytics from 'analytics'; in TypeScript. Is this intended? @DavidWells

@DavidWells
Copy link
Owner

It’s intended to work 😅

Do you see what is wrong in that types.d.ts file?

@katsar0v
Copy link
Author

Well removing all exports and leaving export default analytics; works for me. How was this file generated/created?

@DavidWells
Copy link
Owner

The types are generated here: https://github.com/DavidWells/analytics/blob/master/packages/analytics-core/package.json#L47 and then the final output goes through https://github.com/DavidWells/analytics/blob/master/packages/analytics-core/scripts/types.js

I'm working on streamlining this for all packages and hopefully can get correct types for everything.

I have a project with the types file analytics.cjs.d.ts and it appears to be working (at least VScode is autocompleting correctly.

@katsar0v where are you seeing the error? In editor or on TS compile?

Thanks for reporting this

/**
 * Core Analytic constants. These are exposed for third party plugins & listeners
 * @typedef {Object} constants
 * @property {ANON_ID} ANON_ID - Anonymous visitor Id localstorage key
 * @property {USER_ID} USER_ID - Visitor Id localstorage key
 * @property {USER_TRAITS} USER_TRAITS - Visitor traits localstorage key
 */
declare type constants = {
    ANON_ID: ANON_ID;
    USER_ID: USER_ID;
    USER_TRAITS: USER_TRAITS;
};

/**
 * Anonymous visitor Id localstorage key
 * @typedef {String} ANON_ID
 */
declare type ANON_ID = string;

/**
 * Visitor Id localstorage key
 * @typedef {String} USER_ID
 */
declare type USER_ID = string;

/**
 * Visitor traits localstorage key
 * @typedef {String} USER_TRAITS
 */
declare type USER_TRAITS = string;

/**
 * Analytics library configuration
 *
 * After the library is initialized with config, the core API is exposed and ready for use in the application.
 *
 * @param {object} config - analytics core config
 * @param {string} [config.app] - Name of site / app
 * @param {string} [config.version] - Version of your app
 * @param {Array.<Object>}  [config.plugins] - Array of analytics plugins
 * @return {AnalyticsInstance} Analytics Instance
 * @example
 *
 * import Analytics from 'analytics'
 * import pluginABC from 'analytics-plugin-abc'
 * import pluginXYZ from 'analytics-plugin-xyz'
 *
 * // initialize analytics
 * const analytics = Analytics({
 *   app: 'my-awesome-app',
 *   plugins: [
 *     pluginABC,
 *     pluginXYZ
 *   ]
 * })
 *
 */
declare function analytics(config: {
    app?: string;
    version?: string;
    plugins?: object[];
}): AnalyticsInstance;

/**
 * Analytic instance returned from initialization
 * @typedef {Object} AnalyticsInstance
 * @property {Identify} identify - Identify a user
 * @property {Track} track - Track an analytics event
 * @property {Page} page - Trigger page view
 * @property {User} user - Get user data
 * @property {Reset} reset - Clear information about user & reset analytics
 * @property {Ready} ready - Fire callback on analytics ready event
 * @property {On} on - Fire callback on analytics lifecycle events.
 * @property {Once} once - Fire callback on analytics lifecycle events once.
 * @property {GetState} getState - Get data about user, activity, or context.
 * @property {Storage} storage - storage methods
 * @property {EnablePlugin} enablePlugin - Enable plugin
 * @property {DisablePlugin} disablePlugin - Disable plugin
 */
declare type AnalyticsInstance = {
    identify: Identify;
    track: Track;
    page: Page;
    user: User;
    reset: Reset;
    ready: Ready;
    on: On;
    once: Once;
    getState: GetState;
    storage: Storage;
    enablePlugin: EnablePlugin;
    disablePlugin: DisablePlugin;
};

/**
 * Identify a user. This will trigger `identify` calls in any installed plugins and will set user data in localStorage
 * @typedef {Function} Identify
 * @param  {String}   userId  - Unique ID of user
 * @param  {Object}   [traits]  - Object of user traits
 * @param  {Object}   [options] - Options to pass to identify call
 * @param  {Function} [callback] - Callback function after identify completes
 * @api public
 *
 * @example
 *
 * // Basic user id identify
 * analytics.identify('xyz-123')
 *
 * // Identify with additional traits
 * analytics.identify('xyz-123', {
 *   name: 'steve',
 *   company: 'hello-clicky'
 * })
 *
 * // Disable identify for specific plugin
 * analytics.identify('xyz-123', {}, {
 *  plugins: {
 *    // disable for segment plugin
 *    segment: false
 *  }
 * })
 *
 * // Fire callback with 2nd or 3rd argument
 * analytics.identify('xyz-123', () => {
 *   console.log('do this after identify')
 * })
 */
declare type Identify = (userId: string, traits?: any, options?: any, callback?: (...params: any[]) => any) => void;

/**
 * Track an analytics event. This will trigger `track` calls in any installed plugins
 * @typedef {Function} Track
 * @param  {String}   eventName - Event name
 * @param  {Object}   [payload]   - Event payload
 * @param  {Object}   [options]   - Event options
 * @param  {Function} [callback]  - Callback to fire after tracking completes
 * @api public
 *
 * @example
 *
 * // Basic event tracking
 * analytics.track('buttonClicked')
 *
 * // Event tracking with payload
 * analytics.track('itemPurchased', {
 *   price: 11,
 *   sku: '1234'
 * })
 *
 * // Disable specific plugin on track
 * analytics.track('cartAbandoned', {
 *   items: ['xyz', 'abc']
 * }, {
 *  plugins: {
 *    // disable track event for segment
 *    segment: false
 *  }
 * })
 *
 * // Fire callback with 2nd or 3rd argument
 * analytics.track('newsletterSubscribed', () => {
 *   console.log('do this after track')
 * })
 */
declare type Track = (eventName: string, payload?: any, options?: any, callback?: (...params: any[]) => any) => void;

/**
 * Trigger page view. This will trigger `page` calls in any installed plugins
 * @typedef {Function} Page
 * @param  {PageData} [data] - Page data overrides.
 * @param  {Object}   [options] - Page tracking options
 * @param  {Function} [callback] - Callback to fire after page view call completes
 * @api public
 *
 * @example
 *
 * // Basic page tracking
 * analytics.page()
 *
 * // Page tracking with page data overides
 * analytics.page({
 *   url: 'https://google.com'
 * })
 *
 * // Disable specific plugin page tracking
 * analytics.page({}, {
 *  plugins: {
 *    // disable page tracking event for segment
 *    segment: false
 *  }
 * })
 *
 * // Fire callback with 1st, 2nd or 3rd argument
 * analytics.page(() => {
 *   console.log('do this after page')
 * })
 */
declare type Page = (data?: PageData, options?: any, callback?: (...params: any[]) => any) => void;

/**
 * Get user data
 * @typedef {Function} User
 * @param {string} [key] - dot.prop.path of user data. Example: 'traits.company.name'
 * @returns {string|object} value of user data or null
 *
 * @example
 *
 * // Get all user data
 * const userData = analytics.user()
 *
 * // Get user id
 * const userId = analytics.user('userId')
 *
 * // Get user company name
 * const companyName = analytics.user('traits.company.name')
 */
declare type User = (key?: string) => string | any;

/**
 * Clear all information about the visitor & reset analytic state.
 * @typedef {Function} Reset
 * @param {Function} [callback] - Handler to run after reset
 *
 * @example
 *
 * // Reset current visitor
 * analytics.reset()
 */
declare type Reset = (callback?: (...params: any[]) => any) => void;

/**
 * Fire callback on analytics ready event
 * @typedef {Function} Ready
 * @param  {Function} callback - function to trigger when all providers have loaded
 * @returns {DetachListeners} - Function to detach listener
 *
 * @example
 *
 * analytics.ready() => {
 *   console.log('all plugins have loaded or were skipped', payload)
 * })
 */
declare type Ready = (callback: (...params: any[]) => any) => DetachListeners;

/**
 * Attach an event handler function for analytics lifecycle events.
 * @typedef {Function} On
 * @param  {String}   name - Name of event to listen to
 * @param  {Function} callback - function to fire on event
 * @return {DetachListeners} - Function to detach listener
 *
 * @example
 *
 * // Fire function when 'track' calls happen
 * analytics.on('track', ({ payload }) => {
 *   console.log('track call just happened. Do stuff')
 * })
 *
 * // Remove listener before it is called
 * const removeListener = analytics.on('track', ({ payload }) => {
 *   console.log('This will never get called')
 * })
 *
 * // cleanup .on listener
 * removeListener()
 */
declare type On = (name: string, callback: (...params: any[]) => any) => DetachListeners;

/**
 * Detach listeners
 * @typedef {Function} DetachListeners
 */
declare type DetachListeners = () => void;

/**
 * Attach a handler function to an event and only trigger it only once.
 * @typedef {Function} Once
 * @param  {String} name - Name of event to listen to
 * @param  {Function} callback - function to fire on event
 * @return {DetachListeners} - Function to detach listener
 *
 * @example
 *
 * // Fire function only once 'track'
 * analytics.once('track', ({ payload }) => {
 *   console.log('This will only triggered once when analytics.track() fires')
 * })
 *
 * // Remove listener before it is called
 * const listener = analytics.once('track', ({ payload }) => {
 *   console.log('This will never get called b/c listener() is called')
 * })
 *
 * // cleanup .once listener before it fires
 * listener()
 */
declare type Once = (name: string, callback: (...params: any[]) => any) => DetachListeners;

/**
 * Get data about user, activity, or context. Access sub-keys of state with `dot.prop` syntax.
 * @typedef {Function} GetState
 * @param  {string} [key] - dot.prop.path value of state
 * @return {any}
 *
 * @example
 *
 * // Get the current state of analytics
 * analytics.getState()
 *
 * // Get a subpath of state
 * analytics.getState('context.offline')
 */
declare type GetState = (key?: string) => any;

/**
 * Enable analytics plugin
 * @typedef {Function} EnablePlugin
 * @param  {String|Array} plugins - name of plugins(s) to disable
 * @param  {Function} [callback] - callback after enable runs
 * @example
 *
 * analytics.enablePlugin('google')
 *
 * // Enable multiple plugins at once
 * analytics.enablePlugin(['google', 'segment'])
 */
declare type EnablePlugin = (plugins: string | any[], callback?: (...params: any[]) => any) => void;

/**
 * Disable analytics plugin
 * @typedef {Function} DisablePlugin
 * @param  {String|Array} name - name of integration(s) to disable
 * @param  {Function} callback - callback after disable runs
 * @example
 *
 * analytics.disablePlugin('google')
 *
 * analytics.disablePlugin(['google', 'segment'])
 */
declare type DisablePlugin = (name: string | any[], callback: (...params: any[]) => any) => void;

/**
 * Storage utilities for persisting data.
 * These methods will allow you to save data in localStorage, cookies, or to the window.
 * @typedef {Object} Storage
 * @property {GetItem} getItem - Get value from storage
 * @property {SetItem} setItem - Set storage value
 * @property {RemoveItem} removeItem - Remove storage value
 *
 * @example
 *
 * // Pull storage off analytics instance
 * const { storage } = analytics
 *
 * // Get value
 * storage.getItem('storage_key')
 *
 * // Set value
 * storage.setItem('storage_key', 'value')
 *
 * // Remove value
 * storage.removeItem('storage_key')
 */
declare type Storage = {
    getItem: GetItem;
    setItem: SetItem;
    removeItem: RemoveItem;
};

/**
 * Get value from storage
 * @typedef {Function} GetItem
 * @param {String} key - storage key
 * @param {Object} [options] - storage options
 * @return {Any}
 *
 * @example
 *
 * analytics.storage.getItem('storage_key')
 */
declare type GetItem = (key: string, options?: any) => any;

/**
 * Set storage value
 * @typedef {Function} SetItem
 * @param {String} key - storage key
 * @param {any} value - storage value
 * @param {Object} [options] - storage options
 *
 * @example
 *
 * analytics.storage.setItem('storage_key', 'value')
 */
declare type SetItem = (key: string, value: any, options?: any) => void;

/**
 * Remove storage value
 * @typedef {Function} RemoveItem
 * @param {String} key - storage key
 * @param {Object} [options] - storage options
 *
 * @example
 *
 * analytics.storage.removeItem('storage_key')
 */
declare type RemoveItem = (key: string, options?: any) => void;

/**
 * Async reduce over matched plugin methods
 * Fires plugin functions
 */
declare function processEvent(): void;

/**
 * Return array of event names
 * @param  {String} eventType - original event type
 * @param  {String} namespace - optional namespace postfix
 * @return {array} - type, method, end
 */
declare function getEventNames(eventType: string, namespace: string): any[];

/**
 * Generate arguments to pass to plugin methods
 * @param  {Object} instance - analytics instance
 * @param  {array} abortablePlugins - plugins that can be cancelled by caller
 * @return {*} function to inject plugin params
 */
declare function argumentFactory(instance: any, abortablePlugins: any[]): any;

/**
 * Verify plugin is not calling itself with whatever:myPluginName self refs
 */
declare function validateMethod(): void;

/**
 * Return the canonical URL and rmove the hash.
 * @param  {string} search - search param
 * @return {string} return current canonical URL
 */
declare function currentUrl(search: string): string;

/**
 * Page data for overides
 * @typedef {object} PageData
 * @property {string} [title] - Page title
 * @property {string} [url] - Page url
 * @property {string} [path] - Page path
 * @property {string} [search] - Page search
 * @property {string} [width] - Page width
 * @property {string} [height] - Page height
 */
declare type PageData = {
    title?: string;
    url?: string;
    path?: string;
    search?: string;
    width?: string;
    height?: string;
};

/**
 * Get information about current page
 * @typedef {Function} getPageData
 * @param  {PageData} [pageData = {}] - Page data overides
 * @return {PageData} resolved page data
 */
declare type getPageData = (pageData?: PageData) => PageData;

/**
 * @typedef {Object} AnalyticsPlugin
 * @property {string} NAMESPACE - Name of plugin
 * @property {Object} [EVENTS] - exposed events of plugin
 * @property {Object} [config] - Configuration of plugin
 * @property {function} [initialize] - Load analytics scripts method
 * @property {function} [page] - Page visit tracking method
 * @property {function} [track] - Custom event tracking method
 * @property {function} [identify] - User identify method
 * @property {function} [loaded] - Function to determine if analytics script loaded
 * @property {function} [ready] - Fire function when plugin ready
 */
declare type AnalyticsPlugin = {
    NAMESPACE: string;
    EVENTS?: any;
    config?: any;
    initialize?: (...params: any[]) => any;
    page?: (...params: any[]) => any;
    track?: (...params: any[]) => any;
    identify?: (...params: any[]) => any;
    loaded?: (...params: any[]) => any;
    ready?: (...params: any[]) => any;
};


export const CONSTANTS: constants;

export const init = analytics;

export const Analytics = analytics;

export = analytics;

@DavidWells
Copy link
Owner

What is the proper way to type these other named exports? https://github.com/DavidWells/analytics/blob/master/packages/analytics-core/scripts/types.js#L15-L19

I added those (via trial/error) in VScode verifying stuff working over on that front. I might be missing something tho.

@katsar0v
Copy link
Author

My editor says that export = ... is wrong. I use tsc from npm install and have es6 as a target with commonjs module (tsconfig). If you need more info, let me know. You can check the error number in my last comment. I also use the latest typescript version from npm. Honestly I have no idea how these should be exported or how to write a typing file, that's why I was surprised when I saw types.d.ts in the npm package analytics.

@katsar0v
Copy link
Author

Here is my tsconfig.json if it helps you (I ignored types.d.ts on purpose, because it would not compile, I forked it with my own changes):

{
    "compilerOptions": {
        "declaration": true,
        "noEmitOnError": true,
        "skipLibCheck": true,
        "esModuleInterop": true,
        "noUnusedLocals": true,
        "module": "commonjs",
        "moduleResolution": "node",
        "target": "ES6",
        "outDir": "./lib",
        "lib": ["es6", "dom"]
    },
    "include": ["src/*"],
    "exclude": ["node_modules/analytics/lib/types.d.ts"]
}

DavidWells added a commit that referenced this issue Oct 21, 2019
@chrisdrackett
Copy link
Contributor

what is the best way to help on this?

@chrisdrackett
Copy link
Contributor

@DavidWells if I put the output you have above at index.d.ts within node_modules/analytics I get types for the core library, FYI!

@DavidWells
Copy link
Owner

Thanks, @chrisdrackett! This will work great on the core API!


Adding some additional context on where I got stuck with this last time I tried implementing typescript across the board for all plugins.

Certain plugins run in node.js and in the browser. I'm using this universal module pattern described here to have a single code base and module import.

So in the browser code, we do:

import googleAnalytics from '@analytics/google-analytics'

And in node.js we do the same:

import googleAnalytics from '@analytics/google-analytics'

Here is the tricky part where I'm stumped:

This particular plugin (and likely many others) have a different set of initialization parameters that need to be typed separately. The browser config and the server config are different.

From what I can tell, Typescript cannot infer the correct types if the same import path is used.

I tried using "references" to no avail https://twitter.com/DavidWells/status/1180349883368247297 & naming the type to their output didn't work either.

Is there a way to solve this problem and have types for both client AND serverside code in the same module?

Or is the only way building to 2 separate module paths and requiring users to know which to import? E.g.

import googleAnalytics from '@analytics/google-analytics' // client side
import googleAnalytics from '@analytics/server/google-analytics' // node side
// This to me is ugly 😅

Reference links:

@chrisdrackett
Copy link
Contributor

@DavidWells my first thought would be to type these as the full set but note in the docs ("node.js only" or "browser only") values that only work or are used on a single platform. You could then potentially also throw if a server config value is sent to the browser code. This isn't 100% ideal, but IMO its better than having two separate modules. I'll keep thinking on it!

@saiichihashimoto
Copy link

Have yall @DavidWells @chrisdrackett gotten anywhere with this? There's quite a few missing bits of type that makes these libraries a bit of a headache to use with strict typescript. I'm happy to start putting types into DefinitelyTyped but, considering you're only missing pieces of types that needs a build solution, it's awkward to throw in types for your packages in there.

@chrisdrackett
Copy link
Contributor

I don't use this package anymore, so I'm sorry to say I can't be much help!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Extra attention is needed
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants