diff --git a/.github/workflows/weekly-stats.yml b/.github/workflows/weekly-stats.yml new file mode 100644 index 000000000000..3166c36336b6 --- /dev/null +++ b/.github/workflows/weekly-stats.yml @@ -0,0 +1,31 @@ +name: Publish Weekly Stats + +on: + schedule: + # https://crontab.guru/#0_12_*_*_1 + - cron: "0 12 * * 1" + workflow_dispatch: + +permissions: + contents: read + comments: write + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + with: + node-version: "18.x" + + - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 + + - run: pnpm install + + # Update the site stats + - run: node packages/typescriptlang-org/scripts/updateAppInsightsGitHubIssue.js + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + APP_INSIGHTS_API_KEY: ${{ secrets.APP_INSIGHTS_API_KEY }} + APP_INSIGHTS_ID: ${{ secrets.APP_INSIGHTS_ID }} \ No newline at end of file diff --git a/packages/playground/src/getExample.ts b/packages/playground/src/getExample.ts index 35cbeade0111..aa6e4298d7c6 100644 --- a/packages/playground/src/getExample.ts +++ b/packages/playground/src/getExample.ts @@ -25,6 +25,11 @@ export const getExampleSourceCode = async (prefix: string, lang: string, example code = code.split("\n").slice(1).join("\n").trim() } + // @ts-ignore + window.appInsights && + // @ts-ignore + window.appInsights.trackEvent({ name: "Read Playground Example", properties: { id: exampleID, lang } }) + return { example, code, diff --git a/packages/playground/src/index.ts b/packages/playground/src/index.ts index 3c12e1f86a6d..25e2a9f90cb9 100644 --- a/packages/playground/src/index.ts +++ b/packages/playground/src/index.ts @@ -241,6 +241,8 @@ export const setupPlayground = ( // When any compiler flags are changed, trigger a potential change to the URL sandbox.setDidUpdateCompilerSettings(async () => { playgroundDebouncedMainFunction() + // @ts-ignore + window.appInsights && window.appInsights.trackEvent({ name: "Compiler Settings changed" }) const model = sandbox.editor.getModel() const plugin = getCurrentPlugin() diff --git a/packages/playground/src/sidebar/plugins.ts b/packages/playground/src/sidebar/plugins.ts index 6468b828a03d..5b5e35cecfdd 100644 --- a/packages/playground/src/sidebar/plugins.ts +++ b/packages/playground/src/sidebar/plugins.ts @@ -23,6 +23,10 @@ export const addCustomPlugin = (mod: string) => { const newPlugins = customPlugins() newPlugins.push(mod) localStorage.setItem("custom-plugins-playground", JSON.stringify(newPlugins)) + // @ts-ignore + window.appInsights && + // @ts-ignore + window.appInsights.trackEvent({ name: "Added Custom Module", properties: { id: mod } }) } const customPlugins = (): string[] => { @@ -143,6 +147,10 @@ export const optionsPlugin: PluginFactory = (i, utils) => { const ds = utils.createDesignSystem(div) ds.declareRestartRequired(i) if (input.checked) { + // @ts-ignore + window.appInsights && + // @ts-ignore + window.appInsights.trackEvent({ name: "Added Registry Plugin", properties: { id: key } }) localStorage.setItem(key, "true") } else { localStorage.removeItem(key) diff --git a/packages/typescriptlang-org/gatsby-browser.js b/packages/typescriptlang-org/gatsby-browser.js new file mode 100644 index 000000000000..07b6fc8c4b20 --- /dev/null +++ b/packages/typescriptlang-org/gatsby-browser.js @@ -0,0 +1,61 @@ +// This hooks ups client-side app analytics +// it's based on how the google analytics plugin works for gatsby +// https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-plugin-google-analytics/src/gatsby-browser.js + +exports.onRouteUpdate = ({ location, prevLocation }) => { + // Run both clear and app insights for a bit, then drop app insights + + // prettier-ignore + // ;(function(c,l,a,r,i,t,y){ + // c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)}; + // t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i; + // y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y); + // })(window, document, "clarity", "script", "3w5kyel345"); + + var sdkInstance = "appInsightsSDK" + window[sdkInstance] = "appInsights" + const config = { + instrumentationKey: "78a8fb52-a225-4c66-ac08-92fad1c1ade1", + // loggingLevelConsole: 1 + } + + let hasLocalStorage = false + try { + hasLocalStorage = typeof localStorage !== `undefined` + } catch (error) {} + + try { + // prettier-ignore + // @ts-ignore + var aiName = window[sdkInstance], aisdk = window[aiName] || function (e) { function n(e) { t[e] = function () { var n = arguments; t.queue.push(function () { t[e].apply(t, n) }) } } var t = { config: e }; t.initialize = !0; var i = document, a = window; setTimeout(function () { var n = i.createElement("script"); n.async = true; n.src = e.url || "https://az416426.vo.msecnd.net/scripts/b/ai.2.min.js", i.getElementsByTagName("script")[0].parentNode.appendChild(n) }); try { t.cookie = i.cookie } catch (e) { } t.queue = [], t.version = 2; for (var r = ["Event", "PageView", "Exception", "Trace", "DependencyData", "Metric", "PageViewPerformance"]; r.length;)n("track" + r.pop()); n("startTrackPage"), n("stopTrackPage"); var s = "Track" + r[0]; if (n("start" + s), n("stop" + s), n("setAuthenticatedUserContext"), n("clearAuthenticatedUserContext"), n("flush"), !(!0 === e.disableExceptionTracking || e.extensionConfig && e.extensionConfig.ApplicationInsightsAnalytics && !0 === e.extensionConfig.ApplicationInsightsAnalytics.disableExceptionTracking)) { n("_" + (r = "onerror")); var o = a[r]; a[r] = function (e, n, i, a, s) { var c = o && o(e, n, i, a, s); return !0 !== c && t["_" + r]({ message: e, url: n, lineNumber: i, columnNumber: a, error: s }), c }, e.autoExceptionInstrumented = !0 } return t }(config); + window[aiName] = aisdk + + const locationWithoutPlaygroundCode = location.pathname + .split("#code")[0] + .split("#src")[0] + + const prevHref = (prevLocation && prevLocation.pathname) || "" + const previousLocationWithoutPlaygroundCode = prevHref + .split("#code")[0] + .split("#src")[0] + + const referrerWithoutPlaygroundCode = + document.referrer && document.referrer.split("#code")[0].split("#src")[0] + + // @ts-ignore + aisdk.trackPageView({ + uri: locationWithoutPlaygroundCode, + refUri: referrerWithoutPlaygroundCode, + properties: { + uri: locationWithoutPlaygroundCode, + prev: previousLocationWithoutPlaygroundCode, + lang: document.documentElement.lang, + visitedPlayground: + hasLocalStorage && localStorage.getItem("sandbox-history") !== null, + }, + }) + } catch (error) { + console.error("Error with Application Insights") + console.error(error) + } + } \ No newline at end of file diff --git a/packages/typescriptlang-org/scripts/makeMarkdownForAppInsights.js b/packages/typescriptlang-org/scripts/makeMarkdownForAppInsights.js new file mode 100644 index 000000000000..0b182df10782 --- /dev/null +++ b/packages/typescriptlang-org/scripts/makeMarkdownForAppInsights.js @@ -0,0 +1,148 @@ +// @ts-check + +/* Creates a markdown summary of the last week's worth of analytics, + * run with: + APP_INSIGHTS_ID="X" APP_INSIGHTS_API_KEY="Y" node packages/typescriptlang-org/scripts/makeMarkdownForAppInsights.js + */ + + const nodeFetch = require("node-fetch").default + const querystring = require("querystring") + + // Get these from: https://ms.portal.azure.com/#@microsoft.onmicrosoft.com/resource/subscriptions/57bfeeed-c34a-4ffd-a06b-ccff27ac91b8/resourceGroups/typescriptlang-org/providers/microsoft.insights/components/TypeScriptLang-Prod-Ai/apiKeys + if (!process.env.APP_INSIGHTS_ID) + throw new Error("No App Insights ID at process.env.APP_INSIGHTS_ID") + + if (!process.env.APP_INSIGHTS_API_KEY) + throw new Error("No App Insights ID at process.env.APP_INSIGHTS_API_KEY") + + const getJSON = async (query, params) => { + const headers = { + "x-api-key": process.env.APP_INSIGHTS_API_KEY, + } + + const queryParams = querystring.stringify(params) + const root = `https://api.applicationinsights.io/v1/apps/${process.env.APP_INSIGHTS_ID}` + const href = `${root}${query}?${queryParams}` + const response = await nodeFetch(href, { headers }) + + if (!response.ok) { + console.error("Error in API call to app insights") + console.error(response) + } + + const json = await response.json() + return json + } + + const makeQuery = query => getJSON("/query", { query }) + + const makeAToSitePath = path => + `${path}` + + const makeAToPlaygroundSample = path => + `${path}` + + const makeAnchorAsNPMModule = path => + `${path}` + + const toMDList = (rows, anchorFunc) => { + return rows + .sort((a, b) => b[1] - a[1]) + .map(e => "- " + anchorFunc(e[0]) + ` (${e[1]})`) + .join("\n") + } + + const makeMarkdownOfWeeklyAppInsightsInfo = async () => { + // You'll be looking at this stuff and think? Err how do I make these complex queries. + // + // It's actually pretty trivial, you do it all in the portal, then there is a button to get it: + // "Run last query in logs view" (it's like 9 dots, then a speech bubble above the bar graph) + // Which gives you the exact query for the data you see. + + const likedPages = await makeQuery( + `let mainTable = union pageViews,customEvents | where timestamp > ago(1d) | where iif('*' in ("Liked Page"), 1==1, name in ("Liked Page")) | where customDimensions["slug"] startswith "/" ; let byTable = mainTable; let queryTable = () {byTable | extend dimension = customDimensions["slug"] | extend dimension = iif(isempty(dimension), "", dimension)}; let byCohortTable = queryTable | project dimension, timestamp; let topSegments = byCohortTable | summarize Events = count() by dimension | top 10 by Events | summarize makelist(dimension); let topEventMetrics = byCohortTable | where dimension in (topSegments); let otherEventUsers = byCohortTable | where dimension !in (topSegments) | extend dimension = "Other"; otherEventUsers | union topEventMetrics | summarize Events = count() by dimension | order by dimension asc` + ) + + const dislikedPagesTable = await makeQuery( + `let mainTable = union pageViews,customEvents | where timestamp > ago(1d) | where iif('*' in ("Disliked Page"), 1==1, name in ("Disliked Page")) | where customDimensions["slug"] startswith "/" ; let byTable = mainTable; let queryTable = () {byTable | extend dimension = customDimensions["slug"] | extend dimension = iif(isempty(dimension), "", dimension)}; let byCohortTable = queryTable | project dimension, timestamp; let topSegments = byCohortTable | summarize Events = count() by dimension | top 10 by Events | summarize makelist(dimension); let topEventMetrics = byCohortTable | where dimension in (topSegments); let otherEventUsers = byCohortTable | where dimension !in (topSegments) | extend dimension = "Other"; otherEventUsers | union topEventMetrics | summarize Events = count() by dimension | order by dimension asc` + ) + + const usedExamples = await makeQuery( + `let mainTable = union pageViews,customEvents | where timestamp > ago(7d) | where iif('*' in ("Read Playground Example"), 1==1, name in ("Read Playground Example")) | where true; let byTable = mainTable; let queryTable = () {byTable | extend dimension = customDimensions["id"] | extend dimension = iif(isempty(dimension), "", dimension)}; let byCohortTable = queryTable | project dimension, timestamp; let topSegments = byCohortTable | summarize Events = count() by dimension | top 10 by Events | summarize makelist(dimension); let topEventMetrics = byCohortTable | where dimension in (topSegments); let otherEventUsers = byCohortTable | where dimension !in (topSegments) | extend dimension = "Other"; otherEventUsers | union topEventMetrics | summarize Events = count() by dimension | order by dimension asc` + ) + + const playgroundPluginsTable = await makeQuery(`let mainTable = union customEvents + | where timestamp > ago(7d) + | where iif('*' in ("Added Registry Plugin"), 1==1, name in ("Added Registry Plugin")) + | where true; + let byTable = mainTable; + let queryTable = () + { + byTable + | extend dimension = customDimensions["id"] + | extend dimension = iif(isempty(dimension), "", dimension) + }; + let byCohortTable = queryTable + | project dimension, timestamp; + let topSegments = byCohortTable + | summarize Events = count() by dimension + | top 10 by Events + | summarize makelist(dimension); + let topEventMetrics = byCohortTable + | where dimension in (topSegments); + let otherEventUsers = byCohortTable + | where dimension !in (topSegments) + | extend dimension = "Other"; + otherEventUsers + | union topEventMetrics + | summarize Events = count() by dimension + | order by dimension asc`) + + const mds = [] + + mds.push( + `Hello! This is an always updating GitHub Issue which pulls out the last week of interesting eco-system analytics from the TypeScript website and makes it available for everyone. If you have ideas for things you'd like to see in here, feel free to comment. Microsoft staff can find the [PM focused version here](https://dev.azure.com/devdiv/DevDiv/_dashboards/dashboard/bf4dee3f-7c4b-42b0-805b-670de64052e5).` + ) + + const mostLikedPages = likedPages.tables[0].rows + mds.push(`#### Most liked/disliked pages`) + mds.push( + "All documentation pages have :+1: and :-1: next to them asking _'Is this page helpful?'_. This is the aggregate of the last week for the results." + ) + mds.push(`###### Most Helpful`) + mds.push(toMDList(mostLikedPages, makeAToSitePath)) + + const mostdisLikedPages = dislikedPagesTable.tables[0].rows + mds.push(`###### Least Helpful`) + mds.push(toMDList(mostdisLikedPages, makeAToSitePath)) + + const examples = usedExamples.tables[0].rows.filter(a => a[0] !== "Other") + + mds.push(`#### Playground Examples`) + mds.push("What code samples in the playground are getting used?") + mds.push(toMDList(examples, makeAToPlaygroundSample)) + + const plugins = playgroundPluginsTable.tables[0].rows + .sort((a, b) => b[1] - a[1]) + .filter(a => a[0] !== "Other") + + mds.push(`#### Playground Plugins`) + mds.push( + "What Playground Plugins are being used? This only counts folks clicking in the registry in the sidebar" + ) + mds.push(toMDList(plugins, makeAnchorAsNPMModule)) + + const today = new Date() + mds.push( + `This was last updated ${today.getDate()}/${today.getMonth()}/${today.getFullYear()}. Created with [this script](https://github.com/microsoft/TypeScript-website/blob/v2/packages/typescriptlang-org/scripts/makeMarkdownForAppInsights.js).` + ) + + return mds.join("\n\n") + } + + // @ts-ignore + if (!module.parent) { + makeMarkdownOfWeeklyAppInsightsInfo().then(console.log) + } + + module.exports = { makeMarkdownOfWeeklyAppInsightsInfo } diff --git a/packages/typescriptlang-org/scripts/updateAppInsightsGitHubIssue.js b/packages/typescriptlang-org/scripts/updateAppInsightsGitHubIssue.js new file mode 100644 index 000000000000..f5df05217c6d --- /dev/null +++ b/packages/typescriptlang-org/scripts/updateAppInsightsGitHubIssue.js @@ -0,0 +1,34 @@ +// @ts-check + +// Uses the App Insights API to grab useful analytics + +// See: https://dev.applicationinsights.io/reference/get-events +// https://ms.portal.azure.com/#@microsoft.onmicrosoft.com/resource/subscriptions/57bfeeed-c34a-4ffd-a06b-ccff27ac91b8/resourceGroups/typescriptlang-org/providers/microsoft.insights/components/TypeScriptLang-Prod-Ai/events +// + +const { + makeMarkdownOfWeeklyAppInsightsInfo, + } = require("./makeMarkdownForAppInsights") + const Octokit = require("@octokit/rest") + + // Get this from OneNote + if (!process.env.GITHUB_TOKEN) + throw new Error("No GitHub Token at process.env.GITHUB_TOKEN") + + const go = async () => { + const octokit = new Octokit({ + auth: process.env.GITHUB_TOKEN, + userAgent: "TS AppInsights Issue Updater", + }) + + const md = await makeMarkdownOfWeeklyAppInsightsInfo() + + await octokit.issues.update({ + owner: "Microsoft", + repo: "TypeScript-Website", + issue_number: 1014, + body: md, + }) + } + + go() \ No newline at end of file diff --git a/packages/typescriptlang-org/src/components/index/AboveTheFold.tsx b/packages/typescriptlang-org/src/components/index/AboveTheFold.tsx index 5eda8c3e3c81..a785a75b13ba 100644 --- a/packages/typescriptlang-org/src/components/index/AboveTheFold.tsx +++ b/packages/typescriptlang-org/src/components/index/AboveTheFold.tsx @@ -9,6 +9,13 @@ const Row = (props: { children: any, className?: string }) =>
{props.children}
const Col2 = (props: { children: any }) =>
{props.children}
+const event = (name: string, options?: any) => { + // @ts-ignore + window.appInsights && + // @ts-ignore + window.appInsights.trackEvent({ name }, options) +} + const FluidButton = (props: { href?: string, onClick?: any, title: string, subtitle?: string, icon: JSX.Element, className?: string }) => (
@@ -29,6 +36,7 @@ export const AboveTheFold = () => { const onclick = (e) => { setShowCTALinks(true) e.preventDefault() + event("Home Page CTA Started") return false } @@ -68,7 +76,7 @@ export const AboveTheFold = () => { title={i("index_2_cta_play")} subtitle={i("index_2_cta_play_subtitle")} href="/play" - onClick={() => ({ link: "playground" })} + onClick={() => event("Home Page CTA Exited", { link: "playground" })} icon={ @@ -92,7 +100,7 @@ export const AboveTheFold = () => { title={i("index_2_cta_download")} subtitle={i("index_2_cta_download_subtitle")} href="/download" - onClick={() => ({ link: "download" })} + onClick={() => event("Home Page CTA Exited", { link: "download" })} icon={ diff --git a/packages/typescriptlang-org/src/templates/documentation.tsx b/packages/typescriptlang-org/src/templates/documentation.tsx index b5feadd3ed44..49ff71463fdc 100644 --- a/packages/typescriptlang-org/src/templates/documentation.tsx +++ b/packages/typescriptlang-org/src/templates/documentation.tsx @@ -18,8 +18,8 @@ import { createIntlLink } from "../components/IntlLink" import { handbookCopy } from "../copy/en/handbook" import { Contributors } from "../components/handbook/Contributors" import { overrideSubNavLinksWithSmoothScroll, updateSidebarOnScroll } from "./scripts/setupSubNavigationSidebar" -// import { setupLikeDislikeButtons } from "./scripts/setupLikeDislikeButtons" -// import { DislikeUnfilledSVG, LikeUnfilledSVG } from "../components/svgs/documentation" +import { setupLikeDislikeButtons } from "./scripts/setupLikeDislikeButtons" +import { DislikeUnfilledSVG, LikeUnfilledSVG } from "../components/svgs/documentation" import { Popup, useQuickInfoPopup } from "../components/Popup" import Helmet from "react-helmet" @@ -69,7 +69,7 @@ const HandbookTemplate: React.FC = (props) => { // Sets current selection updateSidebarOnScroll() - // setupLikeDislikeButtons(props.pageContext.slug, i) + setupLikeDislikeButtons(props.pageContext.slug, i) return () => { @@ -95,13 +95,13 @@ const HandbookTemplate: React.FC = (props) => {
- {/*
+

Was this page helpful?

-
*/} +