diff --git a/.vscode/settings.json b/.vscode/settings.json index 5458b91..0ed404e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { "jest.pathToJest": "npm run test:coverage --", - "jest.runAllTestsFirst": true + "jest.runAllTestsFirst": true, + "editor.formatOnSave": true } diff --git a/@types/index.d.ts b/@types/index.d.ts index 99a33db..6c3cfc5 100644 --- a/@types/index.d.ts +++ b/@types/index.d.ts @@ -8,3 +8,7 @@ declare module NodeJS { ENV: typeof ENV; } } + +interface Window { + dataLayer: GoogleAnalyticsCode; +} diff --git a/jestSetup.js b/jestSetup.js index a749f76..50c5d95 100644 --- a/jestSetup.js +++ b/jestSetup.js @@ -4,3 +4,6 @@ const Enzyme = require("enzyme"); const Adapter = require("enzyme-adapter-react-16"); // React 16 Enzyme adapter Enzyme.configure({ adapter: new Adapter() }); + +// mock google analytics +window.ga = () => {}; diff --git a/package.json b/package.json index 37da5b4..1f344e4 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "license": "MIT", "devDependencies": { "@types/enzyme": "^3.9.3", + "@types/google.analytics": "^0.0.40", "@types/jest": "^24.0.13", "@types/naver-whale": "^0.0.0", "@types/react": "^16.0.40", diff --git a/src/analytics/index.ts b/src/analytics/index.ts new file mode 100644 index 0000000..3f84bea --- /dev/null +++ b/src/analytics/index.ts @@ -0,0 +1,33 @@ +(function(i, s, o, g, r, a, m) { + i["GoogleAnalyticsObject"] = r; + (i[r] = + i[r] || + function() { + (i[r].q = i[r].q || []).push(arguments); + }), + // @ts-ignore + (i[r].l = 1 * new Date()); + (a = s.createElement(o)), (m = s.getElementsByTagName(o)[0]); + a.async = 1; + a.src = g; + m.parentNode.insertBefore(a, m); +})( + window, + document, + "script", + `https://www.google-analytics.com/analytics${ENV === "development" ? "_debug" : ""}.js`, + "ga" +); +ga("create", "UA-150779267-1", { + storage: "none", + clientId: ENV === "development" ? "localhost" : localStorage.getItem("_clientId") +}); +if (ENV === "development") { + ga("set", "sendHitTask", null); +} +ga(function() { + window.localStorage.setItem("_clientId", ga.getAll()[0].get("clientId")); +}); +ga("set", "dimension1", BROWSER); +ga("set", "checkProtocolTask", function() {}); // Removes failing protocol check. @see: http://stackoverflow.com/a/22152353/1958200 +ga("require", "displayfeatures"); diff --git a/src/background/index.ts b/src/background/index.ts index 97a5de6..b6dea7a 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -1,8 +1,10 @@ -import RootStore from "../store"; -import history from "./tab/history"; -import onInstall from "./runtime/onInstall"; -import onMessage from "./runtime/onMessage"; -import { addContextClickListener, addLinkContext } from "../tools/contextMenu"; +import '../analytics'; + +import RootStore from '../store'; +import { addContextClickListener, addLinkContext } from '../tools/contextMenu'; +import onInstall from './runtime/onInstall'; +import onMessage from './runtime/onMessage'; +import history from './tab/history'; const store = new RootStore(true, () => { history(store.webtoon, store.option); @@ -17,3 +19,10 @@ const store = new RootStore(true, () => { }); onInstall(store.webtoon, store.option); + +// analytics +if (BROWSER === "whale") { + whale.sidebarAction.onClicked.addListener(() => { + ga("send", "event", "extension", "whale-sidebar-clicked"); + }); +} diff --git a/src/background/runtime/onInstall.ts b/src/background/runtime/onInstall.ts index 60806b9..b1c1c0f 100644 --- a/src/background/runtime/onInstall.ts +++ b/src/background/runtime/onInstall.ts @@ -1,6 +1,6 @@ -import WebtoonStore from "../../store/webtoon"; -import OptionStore from "../../store/option"; -import migration from "./migration"; +import OptionStore from '../../store/option'; +import WebtoonStore from '../../store/webtoon'; +import migration from './migration'; export default function(webtoon: WebtoonStore, option: OptionStore) { chrome.runtime.onInstalled.addListener(details => { @@ -8,9 +8,15 @@ export default function(webtoon: WebtoonStore, option: OptionStore) { if (details.reason === "install") { console.log("Init Start"); + ga("send", "event", "extension", "install", BROWSER); webtoon.setVisitsFromChrome(); } else if (details.reason === "update") { const currentVersion = chrome.runtime.getManifest().version; + ga("send", "event", { + eventCategory: "extension", + eventAction: "update", + eventLabel: `${details.previousVersion}>${currentVersion} (${BROWSER})` + }); if (details.previousVersion != currentVersion) { console.log("update ", details.previousVersion, currentVersion); if (BROWSER === "whale") { diff --git a/src/background/runtime/onMessage.ts b/src/background/runtime/onMessage.ts index 22f3bfa..09fbe02 100644 --- a/src/background/runtime/onMessage.ts +++ b/src/background/runtime/onMessage.ts @@ -1,7 +1,7 @@ -import WebtoonStore from "../../store/webtoon"; -import OptionStore from "../../store/option"; -import Link from "../../tools/link"; -import { addContextClickListener } from "../../tools/contextMenu"; +import OptionStore from '../../store/option'; +import WebtoonStore from '../../store/webtoon'; +import { addContextClickListener } from '../../tools/contextMenu'; +import Link from '../../tools/link'; export interface ChromeMessage { command?: "openTab" | "reload" | "addContextMenu"; diff --git a/src/background/tab/utility.ts b/src/background/tab/utility.ts index 0bc2b3c..16f0a0f 100644 --- a/src/background/tab/utility.ts +++ b/src/background/tab/utility.ts @@ -1,4 +1,4 @@ -import { VisitType } from "../../store/webtoon"; +import { VisitType } from '../../store/webtoon'; export function hiddenComment(tabId: number, isMobile: boolean) { const code = `document.getElementById("${ @@ -105,10 +105,11 @@ export function displayHistory( }`; if (!tabId) { eval(code); - } else + } else { chrome.tabs.executeScript(tabId, { code: code }); + } }); } } diff --git a/src/manifest.chrome.json b/src/manifest.chrome.json index 11271dd..6b0f791 100644 --- a/src/manifest.chrome.json +++ b/src/manifest.chrome.json @@ -36,5 +36,6 @@ "run_at": "document_end", "all_frames": true } - ] + ], + "content_security_policy": "script-src 'self' https://www.googletagmanager.com https://ssl.google-analytics.com https://www.google-analytics.com https://mustsee-earth.firebaseio.com; object-src 'self'" } diff --git a/src/manifest.whale.json b/src/manifest.whale.json index 6e5a4f0..e55427f 100644 --- a/src/manifest.whale.json +++ b/src/manifest.whale.json @@ -37,5 +37,6 @@ "run_at": "document_end", "all_frames": true } - ] + ], + "content_security_policy": "script-src 'self' https://www.googletagmanager.com https://ssl.google-analytics.com https://www.google-analytics.com https://mustsee-earth.firebaseio.com; object-src 'self'" } diff --git a/src/popup/Popup.tsx b/src/popup/Popup.tsx index d31d031..8b6ad31 100644 --- a/src/popup/Popup.tsx +++ b/src/popup/Popup.tsx @@ -1,17 +1,21 @@ -import * as React from "react"; -import { Provider } from "mobx-react"; -// Store Import -import Store from "../store"; +import { Provider } from 'mobx-react'; +import * as React from 'react'; -import Wlink from "./components/Wlink"; -import Switcher from "./components/Switcher"; -import { Tabs } from "./Tabs"; -import ErrorHandler from "./components/ErrorHandler"; -import UpdateCheck from "./components/UpdateCheck"; +import Store from '../store'; +import ErrorHandler from './components/ErrorHandler'; +import Switcher from './components/Switcher'; +import UpdateCheck from './components/UpdateCheck'; +import Wlink from './components/Wlink'; +import { Tabs } from './Tabs'; +// Store Import const store = new Store(false); export default class App extends React.Component { + componentDidMount() { + ga("send", "pageview"); + } + render() { return ( diff --git a/src/popup/Tabs/ListDaily.tsx b/src/popup/Tabs/ListDaily.tsx index 4009981..40e7907 100644 --- a/src/popup/Tabs/ListDaily.tsx +++ b/src/popup/Tabs/ListDaily.tsx @@ -1,11 +1,12 @@ -import * as React from "react"; -import { observer, inject } from "mobx-react"; -import OptionStore from "../../store/option"; -import { weekDay, Week } from "../../tools/request"; -import WebtoonStore from "../../store/webtoon"; -import DailyItemList, { MovedEvent } from "../components/Daily/DailyItemList"; -import { toJS } from "mobx"; -import SearchWebtoon from "../components/Daily/SearchWebtoon"; +import { toJS } from 'mobx'; +import { inject, observer } from 'mobx-react'; +import * as React from 'react'; + +import OptionStore from '../../store/option'; +import WebtoonStore from '../../store/webtoon'; +import { Week, weekDay } from '../../tools/request'; +import DailyItemList, { MovedEvent } from '../components/Daily/DailyItemList'; +import SearchWebtoon from '../components/Daily/SearchWebtoon'; export interface IListDailyProps { option?: OptionStore; @@ -105,14 +106,28 @@ export default class ListDaily extends React.Component {option.saveFavorate ? (
  • - this.changeDay("favo")}>★ + { + ga("send", "event", "ListDaily", "dayChanged", "favorate"); + this.changeDay("favo"); + }} + > + ★ +
  • ) : null} {weekDay.map((week, index) => { return (
  • - this.changeDay(week)}>{this.day[index]} + { + ga("send", "event", "ListDaily", "dayChanged", week); + this.changeDay(week); + }} + > + {this.day[index]} +
  • ); })} diff --git a/src/popup/Tabs/ListHistory.tsx b/src/popup/Tabs/ListHistory.tsx index 358ca40..d49ace7 100644 --- a/src/popup/Tabs/ListHistory.tsx +++ b/src/popup/Tabs/ListHistory.tsx @@ -1,8 +1,8 @@ -import * as React from "react"; -import { inject, observer } from "mobx-react"; -import WebtoonStore from "../../store/webtoon"; -import HistoryItem from "../components/History/HistoryItem"; -import HistoryItemContext from "../components/History/HistoryItemContext"; +import { inject, observer } from 'mobx-react'; +import * as React from 'react'; + +import WebtoonStore from '../../store/webtoon'; +import HistoryItem from '../components/History/HistoryItem'; export interface IListHistoryProps { webtoon?: WebtoonStore; diff --git a/src/popup/Tabs/Option.tsx b/src/popup/Tabs/Option.tsx index 9aa37c2..fcb01b8 100644 --- a/src/popup/Tabs/Option.tsx +++ b/src/popup/Tabs/Option.tsx @@ -1,15 +1,15 @@ -import * as React from "react"; -import { observer, inject } from "mobx-react"; -import OptionStore, { ChromeStore, WebtoonOrder, LinkTarget } from "../../store/option"; -import WebtoonStore from "../../store/webtoon"; -import Wlink from "../components/Wlink"; -import RecentSetting from "../components/Setting/RecentSetting"; -import WebtoonSetting from "../components/Setting/WebtoonSetting"; -import PageSetting from "../components/Setting/PageSetting"; -import SpecialSetting from "../components/Setting/SpecialSetting"; -import StorageSetting from "../components/Setting/StorageSetting"; -import DevelopInfo from "../components/Setting/DevelopInfo"; -import SettingButton from "../components/Setting/Inputs/SettingButton"; +import { inject, observer } from 'mobx-react'; +import * as React from 'react'; + +import OptionStore from '../../store/option'; +import DevelopInfo from '../components/Setting/DevelopInfo'; +import SettingButton from '../components/Setting/Inputs/SettingButton'; +import PageSetting from '../components/Setting/PageSetting'; +import RecentSetting from '../components/Setting/RecentSetting'; +import SpecialSetting from '../components/Setting/SpecialSetting'; +import StorageSetting from '../components/Setting/StorageSetting'; +import WebtoonSetting from '../components/Setting/WebtoonSetting'; +import Wlink from '../components/Wlink'; export interface IOptionProps { option?: OptionStore; diff --git a/src/popup/components/Daily/DailyItem.tsx b/src/popup/components/Daily/DailyItem.tsx index 6150583..0b88a7e 100644 --- a/src/popup/components/Daily/DailyItem.tsx +++ b/src/popup/components/Daily/DailyItem.tsx @@ -1,11 +1,13 @@ -import * as React from "react"; -import { WebtoonInfoType } from "../../../tools/request"; -import Wlink from "../Wlink"; -import OptionStore from "../../../store/option"; -import { observer, inject } from "mobx-react"; -import WebtoonStore from "../../../store/webtoon"; -import * as distance from "date-fns/distance_in_words_to_now"; -import * as ko from "date-fns/locale/ko"; +import * as distance from 'date-fns/distance_in_words_to_now'; +import * as ko from 'date-fns/locale/ko'; +import { inject, observer } from 'mobx-react'; +import * as React from 'react'; + +import OptionStore from '../../../store/option'; +import WebtoonStore from '../../../store/webtoon'; +import { WebtoonInfoType } from '../../../tools/request'; +import Wlink from '../Wlink'; + export interface IDailyItemProps { item: WebtoonInfoType; option?: OptionStore; @@ -20,8 +22,10 @@ export default class DailyItem extends React.Component { const { webtoon, item } = this.props; const idx = webtoon.starWebtoons.indexOf(item.id); if (idx != -1) { + ga("send", "event", "DailyItem", "unStarWebtoon", item.id); webtoon.starWebtoons.splice(idx, 1); } else { + ga("send", "event", "DailyItem", "starWebtoon", item.id); webtoon.starWebtoons.push(item.id); } @@ -41,7 +45,18 @@ export default class DailyItem extends React.Component { addSuffix: true }) + "에 봄"; return ( - + { + ga( + "send", + "event", + "DailyItem", + "openRecentWebtoon", + `${item.title}(${find.id}/${find.no})` + ); + }} + > {find.noname} @@ -63,7 +78,12 @@ export default class DailyItem extends React.Component { {item.isRest ? : null}
    - + { + ga("send", "event", "DailyItem", "openWebtoon", `${item.title}(${item.id})`); + }} + > {item.title}
    diff --git a/src/popup/components/ErrorHandler.tsx b/src/popup/components/ErrorHandler.tsx index 1f8625c..6cd1000 100644 --- a/src/popup/components/ErrorHandler.tsx +++ b/src/popup/components/ErrorHandler.tsx @@ -1,7 +1,8 @@ -import * as React from "react"; -import Wlink from "./Wlink"; -import { observer, inject } from "mobx-react"; -import OptionStore from "../../store/option"; +import { inject, observer } from 'mobx-react'; +import * as React from 'react'; + +import OptionStore from '../../store/option'; +import Wlink from './Wlink'; export interface IErrorHandlerProps { option?: OptionStore; @@ -36,6 +37,9 @@ export default class ErrorHandler extends React.Component오류신고
    나{" "} - this.resetStore()}> + { + ga("send", "event", "popup", "resetStore"); + this.resetStore(); + }} + > 데이터 초기화 를 해주시기 바랍니다. diff --git a/src/popup/components/History/HistoryItem.tsx b/src/popup/components/History/HistoryItem.tsx index 8bf4e1b..70e768d 100644 --- a/src/popup/components/History/HistoryItem.tsx +++ b/src/popup/components/History/HistoryItem.tsx @@ -1,10 +1,11 @@ -import * as React from "react"; -import Wlink from "../Wlink"; -import WebtoonStore, { RecentWebtoon } from "../../../store/webtoon"; -import { observer, inject } from "mobx-react"; -import * as format from "date-fns/format"; -import * as distanceInWordsToNow from "date-fns/distance_in_words_to_now"; -import * as ko from "date-fns/locale/ko"; +import * as distanceInWordsToNow from 'date-fns/distance_in_words_to_now'; +import * as format from 'date-fns/format'; +import * as ko from 'date-fns/locale/ko'; +import { inject, observer } from 'mobx-react'; +import * as React from 'react'; + +import WebtoonStore, { RecentWebtoon } from '../../../store/webtoon'; +import Wlink from '../Wlink'; export interface IHistoryItemProps { webtoon?: WebtoonStore; @@ -48,11 +49,25 @@ export default class HistoryItem extends React.Component return ( - + { + ga("send", "event", "HistoryItem", "openWebtoon", `${item.name}(${item.id})`); + }} + > {item.name} { + ga( + "send", + "event", + "HistoryItem", + "openRecentWebtoon", + `${item.name}/${item.noname}(${item.id}/${item.no})` + ); + }} >
    - + { + ga("send", "event", "popup", "openGithub"); + }} + > - + { + ga("send", "event", "popup", "openNaverBlog"); + }} + > Naver Blog @@ -25,6 +38,7 @@ export default class DevelopInfo extends React.Component { + ga("send", "event", "popup", "showUpdatePopup"); window.dispatchEvent(new Event("extensionUpdate")); }} tooltip="업데이트 내용 팝업을 띄웁니다." diff --git a/src/popup/components/Setting/Inputs/SettingButton.tsx b/src/popup/components/Setting/Inputs/SettingButton.tsx index 1aa7e74..6acb0a9 100644 --- a/src/popup/components/Setting/Inputs/SettingButton.tsx +++ b/src/popup/components/Setting/Inputs/SettingButton.tsx @@ -1,4 +1,4 @@ -import * as React from "react"; +import * as React from 'react'; export interface ISettingButtonProps { onClick: (event: React.MouseEvent) => void; @@ -15,7 +15,10 @@ export default class SettingButton extends React.Component onClick(e)} + onClick={e => { + ga("send", "event", "popup", `SettingButtonClick`, children || ""); + return onClick(e); + }} uk-tooltip={tooltip} disabled={disabled} > diff --git a/src/popup/components/Setting/RecentSetting.tsx b/src/popup/components/Setting/RecentSetting.tsx index 456b6b5..27c9951 100644 --- a/src/popup/components/Setting/RecentSetting.tsx +++ b/src/popup/components/Setting/RecentSetting.tsx @@ -1,8 +1,9 @@ -import * as React from "react"; -import OptionStore, { LinkTarget } from "../../../store/option"; -import WebtoonStore from "../../../store/webtoon"; -import { observer, inject } from "mobx-react"; -import SettingButton from "./Inputs/SettingButton"; +import { inject, observer } from 'mobx-react'; +import * as React from 'react'; + +import OptionStore, { LinkTarget } from '../../../store/option'; +import WebtoonStore from '../../../store/webtoon'; +import SettingButton from './Inputs/SettingButton'; export interface IRecentSettingProps { webtoon?: WebtoonStore; diff --git a/src/popup/components/Setting/SpecialSetting.tsx b/src/popup/components/Setting/SpecialSetting.tsx index b5f6c63..7967662 100644 --- a/src/popup/components/Setting/SpecialSetting.tsx +++ b/src/popup/components/Setting/SpecialSetting.tsx @@ -1,7 +1,6 @@ -import * as React from "react"; -import OptionStore from "../../../store/option"; -import WebtoonStore from "../../../store/webtoon"; -import SettingCheckBox from "./Inputs/SettingCheckBox"; +import * as React from 'react'; + +import SettingCheckBox from './Inputs/SettingCheckBox'; export default class SpecialSetting extends React.Component<{}, null> { public render() { diff --git a/src/popup/components/Switcher.tsx b/src/popup/components/Switcher.tsx index a583783..bb2672c 100644 --- a/src/popup/components/Switcher.tsx +++ b/src/popup/components/Switcher.tsx @@ -1,4 +1,4 @@ -import * as React from "react"; +import * as React from 'react'; export interface ISwitcherProps { webtoonComponents: { @@ -8,6 +8,12 @@ export interface ISwitcherProps { } export default class Switcher extends React.Component { + private onSwitcherItemClick = (name: string) => { + return () => { + ga("send", "event", "popup", "changeSwitch", name); + }; + }; + public render() { const { webtoonComponents } = this.props; return ( @@ -15,7 +21,9 @@ export default class Switcher extends React.Component { diff --git a/src/popup/components/Wlink.tsx b/src/popup/components/Wlink.tsx index 8aec164..9bc2842 100644 --- a/src/popup/components/Wlink.tsx +++ b/src/popup/components/Wlink.tsx @@ -1,7 +1,8 @@ -import * as React from "react"; -import { inject, observer } from "mobx-react"; -import OptionStore from "../../store/option"; -import Link from "../../tools/link"; +import { inject, observer } from 'mobx-react'; +import * as React from 'react'; + +import OptionStore from '../../store/option'; +import Link from '../../tools/link'; export interface IWlinkProps { /** @@ -18,6 +19,8 @@ export interface IWlinkProps { * [Mobx] Option Store */ option?: OptionStore; + + onClick?: React.MouseEventHandler; } @inject("option") @@ -25,7 +28,9 @@ export interface IWlinkProps { export default class Wlink extends React.Component { public clickHandler(event: React.MouseEvent) { event.preventDefault(); - const { link, option, forceTab } = this.props; + const { link, option, forceTab, onClick } = this.props; + ga("send", "event", "wlink", "linkOpen", link); + onClick && onClick(event); if (forceTab) { return Link.openNewTab(link); } diff --git a/src/popup/index.tsx b/src/popup/index.tsx index fe26846..75477e6 100644 --- a/src/popup/index.tsx +++ b/src/popup/index.tsx @@ -1,5 +1,9 @@ -import * as React from "react"; -import * as ReactDOM from "react-dom"; -import Popup from "./Popup"; +import '../analytics'; + +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; + +import Popup from './Popup'; + ReactDOM.render(, document.getElementById("root")); diff --git a/src/store/option.ts b/src/store/option.ts index 35cd9e0..6ae93e5 100644 --- a/src/store/option.ts +++ b/src/store/option.ts @@ -1,4 +1,4 @@ -import { observable, action, computed } from "mobx"; +import { computed, observable } from 'mobx'; export const storeKeys = [ "_storeLocation", @@ -32,6 +32,8 @@ export default class OptionStore { } private saveToStore() { + ga("send", "event", "store", "option"); + ga("set", "dimension2", JSON.stringify(this.optionObject)); chrome.storage.sync.set({ option: JSON.stringify(this.optionObject) }); diff --git a/src/store/webtoon.ts b/src/store/webtoon.ts index 0132d48..936f1dd 100644 --- a/src/store/webtoon.ts +++ b/src/store/webtoon.ts @@ -1,6 +1,7 @@ -import { observable, computed, action, observe, toJS } from "mobx"; -import OptionStore from "./option"; -import WebtoonRequest, { WebtoonInfoType, WebtoonInfo, Week } from "../tools/request"; +import { action, computed, observable, observe } from 'mobx'; + +import WebtoonRequest, { WebtoonInfo, WebtoonInfoType, Week } from '../tools/request'; +import OptionStore from './option'; // export type SaveType = "imglog" | "favorate" | "scrolls" | "visits" | "webtoon"; @@ -458,6 +459,7 @@ export default class WebtoonStore { if (this.option.useImgLog && !this.option.isBackground) this.imglog = this._imglog; this._visits = this._visits; console.log(webtoons); + ga("send", "event", "webtoon", "recentWebtoon", "updated", webtoons.length); } @action setVisitsFromChrome() { diff --git a/src/tools/contextMenu/index.ts b/src/tools/contextMenu/index.ts index 157f50f..4af4d7e 100644 --- a/src/tools/contextMenu/index.ts +++ b/src/tools/contextMenu/index.ts @@ -1,5 +1,5 @@ -import Link from "../link"; -import WebtoonStore from "../../store/webtoon"; +import WebtoonStore from '../../store/webtoon'; +import Link from '../link'; export const CONTEXT_MENU_ID_SIDEBAR = "OPEN_IN_SIDEBAR"; export const CONTEXT_MENU_ID_TAB = "OPEN_IN_TAB"; @@ -49,6 +49,7 @@ export function addLinkContext() { export function addContextClickListener(webtoon: WebtoonStore) { chrome.contextMenus.onClicked.addListener(info => { + ga("send", "event", "ContextMenu", "menu clicked", info.menuItemId); switch (info.menuItemId) { case CONTEXT_MENU_ID_SIDEBAR: Link.openSidebar(info.linkUrl); diff --git a/yarn.lock b/yarn.lock index 3174576..252ae0f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -388,6 +388,11 @@ resolved "https://registry.yarnpkg.com/@types/filewriter/-/filewriter-0.0.28.tgz#c054e8af4d9dd75db4e63abc76f885168714d4b3" integrity sha1-wFTor02d11205jq8dviFFocU1LM= +"@types/google.analytics@^0.0.40": + version "0.0.40" + resolved "https://registry.yarnpkg.com/@types/google.analytics/-/google.analytics-0.0.40.tgz#35526e9d78333423c430ade1c821ce54d0f0f850" + integrity sha512-R3HpnLkqmKxhUAf8kIVvDVGJqPtaaZlW4yowNwjOZUTmYUQEgHh8Nh5wkSXKMroNAuQM8gbXJHmNbbgA8tdb7Q== + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff"