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

[Analytics] Adds typings for react-tracking and specific to Artsy. #1

Merged
merged 1 commit into from
Sep 19, 2017
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
2 changes: 1 addition & 1 deletion src/components/publishing/article.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { cloneDeep, includes, map } from "lodash"
import * as PropTypes from "prop-types"
import * as React from "react"
import track from "react-tracking"
import styled from "styled-components"
import Events from "../../utils/events"
import track from "../../utils/track"
import Header from "./header/header"
import FeatureLayout from "./layouts/feature_layout"
import Sidebar from "./layouts/sidebar"
Expand Down
33 changes: 31 additions & 2 deletions src/components/publishing/share.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,41 @@
import React from "react"
import track from "react-tracking"
import styled from "styled-components"
import Icon from "../icon"

import { track as _track, TrackingInfo, trackWithoutProps, trackWithProps } from "../../utils/track"

interface ShareProps extends React.HTMLProps<HTMLDivElement> {
url: string
title: string
}

/**
* An example of a typed `track` function that does not have types for the props used by this component and only uses
* the default global tracking-info types.
*
* @note This weird `import { track as _track } from "../../utils/track"` dance is only done to have this comment in the
* same format as the ones below.
*/
const track = _track

/**
* An example of a typed `track` function that does not have types for the props used by this component, but does allow
* types for the tracking-info.
*/
// const track = trackWithoutProps<TrackingInfo.Shareable>()

/**
* An example of a typed `track` function that has types for the props used by this component, but only uses the default
* global tracking-info types.
*/
// const track = trackWithProps<ShareProps>()

/**
* An example of a typed `track` function that has types for the props used by this component and specifies extended
* types for tracking-info, in this case the centralized types shareable tracking-info.
*/
// const track = trackWithProps<ShareProps, TrackingInfo.Shareable>()
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are all just examples of the various possible permutations with out own track module, which augments the generic react-tracking types.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@craigspaeth So when you add actual types for all publishing components, please remove these 🙏


@track()
class Share extends React.Component<ShareProps, null> {
constructor(props) {
Expand All @@ -16,7 +44,8 @@ class Share extends React.Component<ShareProps, null> {
this.trackShare = this.trackShare.bind(this)
}

@track({ action: "share article" })
// FIXME: This is obviously wrong, a slug is not a URL, but it’s just for illustration purposes.
@track(props => ({ action: "share article", slug: props.url }))
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another example that’s supposed to be cleaned up.

trackShare(e) {
e.preventDefault()
window.open(e.currentTarget.attributes.href.value, "Share", "width = 600,height = 300")
Expand Down
77 changes: 77 additions & 0 deletions src/typings/react-tracking.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Type definitions for react-tracking 4.2
// Project: https://github.com/NYTimes/react-tracking
// Definitions by: Eloy Durán <https://github.com/alloy>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped

declare module "react-tracking" {
import * as React from "react"

export interface TrackingProp {
trackEvent: ({}) => any

/**
* This method returns all of the contextual tracking data up until this point in the component hierarchy.
*/
getTrackingData: () => {}
}

type Falsy = false | null | undefined | ""

interface Options<T> {
/**
* By default, data tracking objects are pushed to `window.dataLayer[]`. This is a good default if you use Google
* Tag Manager. You can override this by passing in a dispatch function as a second parameter to the tracking
* decorator `{ dispatch: fn() }` on some top-level component high up in your app (typically some root-level
* component that wraps your entire app).
*/
dispatch?: (data: T) => any

/**
* To dispatch tracking data when a component mounts, you can pass in `{ dispatchOnMount: true }` as the second
* parameter to `@track()`. This is useful for dispatching tracking data on "Page" components.
*
* If you pass in a function, the function will be called with all of the tracking data from the app's context when
* the component mounts. The return value of this function will be dispatched in `componentDidMount()`. The object
* returned from this function call will be merged with the context data and then dispatched. A use case for this
* would be that you want to provide extra tracking data without adding it to the context.
*/
dispatchOnMount?: boolean | ((contextData: T) => T)

/**
* When there's a need to implicitly dispatch an event with some data for every component, you can define an
* `options.process` function. This function should be declared once, at some top-level component. It will get
* called with each component's tracking data as the only argument. The returned object from this function will be
* merged with all the tracking context data and dispatched in `componentDidMount()`. If a falsy value is returned
* (`false`, `null`, `undefined`, ...), nothing will be dispatched.
*
* A common use case for this is to dispatch a `pageview` event for every component in the application that has a
* `page` property on its `trackingData`.
*/
process?: (ownTrackingData: T) => T | Falsy
}

export type TrackingInfo<T, P> = T | ((props: P) => T)

// Duplicated from ES6 lib to remove the `void` typing, otherwise `track` can’t be used as a HOC function that passes
// through a JSX component that be used wihtout casting.
type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction
type MethodDecorator = <T>(
target: Object,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T>
type Decorator = ClassDecorator & MethodDecorator

/**
* This is the type of the `track` function. It’s declared as an interface so that consumers can extend the typing and
* specify defaults, such as a global analytics schema for the tracking-info.
*
* For examples of such extensions see: https://github.com/artsy/reaction/blob/master/src/utils/track.ts
*/
export interface Track<T = {}, P = {}> {
(trackingInfo?: TrackingInfo<Partial<T>, P>, options?: Options<Partial<T>>): Decorator
}

export const track: Track
export default track
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These is the ‘ambient’ declaration for the react-tracking module.

130 changes: 130 additions & 0 deletions src/utils/track.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { Track, track as _track } from "react-tracking"

// tslint:disable-next-line:no-namespace
export namespace TrackingInfo {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain the export namespace bit?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I got it :)

/**
* The global tracking-info keys in Artsy’s schema.
*/
export interface Global {
/**
* The name of an event.
*/
action: string

/**
* The root container component should specify this as the screen context.
*/
page: string
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even though you would use either page or action depending on if you’re decorating a class or a method, I felt like separating the two was overdoing it.


export interface Shareable extends Global {
/**
* The public slug for this entity.
*/
slug: string
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mennenia This is an example of what the tracking-info schema could be for items that are ‘shareable’. Please update it to reflect our actual schema.

}

/**
* A typed tracking-info and props alias of the default react-tracking `track` function.
*
* Use this version when you’re going to use a callback function to generate the tracking info.
*
* @example
*
* import { trackWithProps } from "src/utils/track"
*
* interface Props {
* artist: {
* slug: string
* }
* }
*
* const track = trackWithProps<Props>()
*
* @track()
* class Artist extends React.Component<Props, null> {
* render() {
* return (
* <div onClick={this.handleFollow.bind(this)}>
* ...
* </div>
* )
* }
*
* @track(props => ({ action: "Follow Artist", slug: props.slug }))
* handleFollow() {
* // ...
* }
* }
*
*/
export function trackWithProps<P, T extends TrackingInfo.Global = TrackingInfo.Global>(): Track<T, P> {
return _track
}

/**
* A typed tracking-info alias of the default react-tracking `track` function.
*
* Use this version when you don’t use a callback function to generate the tracking info based on props, but you do want
* to extend the tracking-info schema.
*
* @example
*
* import { trackWithoutProps as track } from "src/utils/track"
*
* interface TrackingInfo {
* slug: string
* }
*
* const track = trackWithoutProps<TrackingInfo>()
*
* @track()
* class Artist extends React.Component<{}, null> {
* render() {
* return (
* <div onClick={this.handleFollow.bind(this)}>
* ...
* </div>
* )
* }
*
* @track({ action: "Follow Artist", slug: "banksy" })
* handleFollow() {
* // ...
* }
* }
*/
export function trackWithoutProps<T extends TrackingInfo.Global = TrackingInfo.Global>(): Track<T, any> {
return _track
}

/**
* A typed tracking-info alias of the default react-tracking `track` function.
*
* Use this version when you don’t use a callback function to generate the tracking info based on props. You can also
* use this if you don’t want to deal with props types, but you want to be cool too, right?
*
* @example
*
* import { track } from "src/utils/track"
*
* @track()
* class Artist extends React.Component<{}, null> {
* render() {
* return (
* <div onClick={this.handleFollow.bind(this)}>
* ...
* </div>
* )
* }
*
* @track({ action: "Follow Artist" })
* handleFollow() {
* // ...
* }
* }
*/
export const track = trackWithoutProps()
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me know if the documentation and examples is clear enough.


export default track