Skip to content

Commit

Permalink
Merge pull request #344 from cofacts/add-gtm
Browse files Browse the repository at this point in the history
Add Google Tag Manager
  • Loading branch information
MrOrz committed Mar 20, 2023
2 parents ad015bd + 4807d10 commit 0bc402e
Show file tree
Hide file tree
Showing 14 changed files with 103 additions and 104 deletions.
4 changes: 2 additions & 2 deletions .env.sample
Expand Up @@ -13,8 +13,8 @@ ROLLBAR_ENV=localhost
# Rollbar post_client_item, for LIFF
ROLLBAR_CLIENT_TOKEN=

# Google analytics ID
GA_ID=
# Google tag manager ID GTM-XXXXX
GTM_ID=

# LIFF url with pattern "https://liff.line.me/<liff-id>"
LIFF_URL=https://liff.line.me/1563196602-X6mLdDkW
Expand Down
35 changes: 26 additions & 9 deletions README.md
Expand Up @@ -36,7 +36,7 @@ Other customizable env vars are:

* `REDIS_URL`: If not given, `redis://127.0.0.1:6379` is used.
* `PORT`: Which port the line bot server will listen at.
* `GA_ID`: Google analytics tracking ID, for tracking events. You should also add custom dimensions and metrics, see "Google Analytics Custom dimensions and metrics" section below.
* `GTM_ID`: Google Tag Manager ID. For the events and variables we push to `dataLayer`, see "Google Tag Manager" section below.
* `DEBUG_LIFF`: Disables external browser check in LIFF. Useful when debugging LIFF in external browser. Don't enable this on production.
* `RUMORS_LINE_BOT_URL`: Server public url which is used to generate tutorial image urls and auth callback url of LINE Notify.

Expand Down Expand Up @@ -245,11 +245,34 @@ You can test the built image locally using the `docker-compose.yml`; just uncomm

For production, please see [rumors-deploy](https://github.com/cofacts/rumors-deploy/) for sample `docker-coompose.yml` that runs such image.

## Google Tag Manager

We push variables and events in Google Tag Manager's `dataLayer` when the user interacts with LIFF.

You can prepare the following setup in `.env` file:
- `GTM_ID`: Google Tag Manager Container ID (`GTM-XXXXXXX`)

The application will fire the following custom events in GTM `dataLayer`:

- `dataLoaded` - when data is loaded in article, comment or feedback LIFF.
- `routeChangeComplete` - when LIFF is loaded or changes path.
- `feedbackVote` - when the user submits a feedback.
- Fires once when user opens Feedback LIFF, and can fire again when user updates vote or comments.
- Also fires when user submits feedback on Article LIFF.
- `chooseArticle` - when the user chooses an article in Articles LIFF.

Also, it will push the following custom variable to `dataLayer`;

- `pagePath` - Set when `routeChangeComplete` event fires. The page path from LIFF's router.
- `userId` - Set after LIFF gets ID token and decodes LINE user ID inside.
- `articleId` and `replyId`: set on Article, Comment and Feedback `onMount()` lifecycle is called. Or when `chooseArticle` event is fired.
- `doc` - Set when `dataLoaded` event fires. The loaded content itself in object (article in Article LIFF, comment in Comment LIFF and feedback in feedback LIFF).

## Google Analytics Events table

Sent event format: `Event category` / `Event action` / `Event label`

We use dimemsion `Message Source` (Custom Dimemsion1) to classify different event sources
We use dimension `Message Source` (Custom Dimemsion1) to classify different event sources
- `user` for 1 on 1 messages
- `room` | `group` for group messages

Expand Down Expand Up @@ -311,13 +334,7 @@ We use dimemsion `Message Source` (Custom Dimemsion1) to classify different even
- If opened after sending reply requests: `utm_source=rumors-line-bot&utm_medium=reply-request`
- If opened in tutorial: `&utm_source=rumors-line-bot&utm_medium=tutorial`

11. Other LIFF operations
- `LIFF` / `page_redirect` / `App` is sent on LIFF redirect, with value being redirect count.
- `LIFF` / `ViewArticle` / `<articleId>` when article page with `articleId` is opened
- `LIFF` / `Comment` / `<articleId>` when comment page with `articleId` is opened
- `LIFF` / `ViewReply` / `<replyId>` for each displayed reply in article page

12. Tutorial
11. Tutorial
- If it's triggered by follow event (a.k.a add-friend event)
- `Tutorial` / `Step` / `ON_BOARDING`
- If it's triggered by rich menu
Expand Down
2 changes: 1 addition & 1 deletion src/liff/.eslintrc.js
Expand Up @@ -28,7 +28,7 @@ module.exports = {
// global scripts include in index.html
rollbar: 'readonly',
liff: 'readonly',
gtag: 'readonly',
dataLayer: 'readonly',

// Define plugin
LIFF_ID: 'readonly',
Expand Down
23 changes: 2 additions & 21 deletions src/liff/App.svelte
@@ -1,5 +1,4 @@
<script>
import { onMount } from 'svelte';
import { page } from './lib';
import Article from './pages/Article.svelte';
import Articles from './pages/Articles.svelte';
Expand All @@ -16,30 +15,12 @@
};
// Send pageview with correct path on each page change.
// delay a bit for page components to change page title
// delay a bit for page components for onMount() to be invoked.
page.subscribe(p => {
setTimeout(() => {
gtag('event', 'page_view', { page_path: p });
dataLayer.push({ event: 'routeChangeComplete', pagePath: p });
}, 10)
});
onMount(() => {
if(window.performance) {
gtag('event', 'timing_complete', {
name: 'App mounted',
value: performance.now(),
event_category: 'LIFF',
event_label: 'App',
});
if(performance.navigation) {
gtag('event', 'page_redirect', {
event_category: 'LIFF',
event_label: 'App',
value: performance.navigation.redirectCount,
});
}
}
})
</script>

<svelte:component this={routes[$page]} />
10 changes: 10 additions & 0 deletions src/liff/Redirect.svelte
@@ -1,5 +1,6 @@
<script>
import { t } from 'ttag';
import { onMount } from 'svelte';
import Button from './components/Button.svelte';
import AppBar from './components/AppBar.svelte';
import multipleRepliesBanner from './assets/multiple-replies.png';
Expand All @@ -17,6 +18,15 @@
);
const LIFF_URL = `https://liff.line.me/${LIFF_ID}?${newParams.toString()}`;
onMount(() => {
dataLayer.push({
event: 'routeChangeComplete',
pagePath: 'redirect',
articleId: currentParams.get('articleId'),
replyId: currentParams.get('replyId'),
});
});
const handleClick = () => {
location.href = LIFF_URL;
}
Expand Down
6 changes: 6 additions & 0 deletions src/liff/components/ArticleReplyCard.svelte
Expand Up @@ -26,6 +26,12 @@
* @returns {Promise<ArticleReplyCard_articleReply>}
*/
const submitVote = async (vote, comment = null) => {
dataLayer.push({
event: 'feedbackVote',
articleId: articleReply.articleId,
replyId: articleReply.reply.id
});
const resp = await gql`
mutation VoteInArticleLIFF(
$articleId: String!
Expand Down
22 changes: 13 additions & 9 deletions src/liff/index.html
Expand Up @@ -2,10 +2,17 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Cofacts 真的假的</title>
<!-- Google Tag Manager -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','<%= htmlWebpackPlugin.options.GTM_ID %>');</script>
<!-- End Google Tag Manager -->
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;700&display=swap" rel="stylesheet">
<link rel="icon" href="/static/favicon.png" type="image/png" />
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml" />
<title>Cofacts 真的假的</title>
<script>
var _rollbarConfig = {
accessToken: "<%= htmlWebpackPlugin.options.ROLLBAR_CLIENT_TOKEN %>",
Expand All @@ -18,16 +25,13 @@
// End Rollbar Snippet
</script>
<script charset="utf-8" src="https://static.line-scdn.net/liff/edge/2.1/sdk.js"></script>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=<%= htmlWebpackPlugin.options.GA_ID %>"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '<%= htmlWebpackPlugin.options.GA_ID %>', { send_page_view: false });
</script>
</head>
<body>
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=<%= htmlWebpackPlugin.options.GTM_ID %>"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->

<div id="loading">
<!-- Removed by index.js -->
<style>
Expand Down
8 changes: 5 additions & 3 deletions src/liff/index.js
Expand Up @@ -11,14 +11,16 @@ liff.init({ liffId: LIFF_ID }).then(() => {

document.getElementById('loading').remove(); // Cleanup loading

// Kickstart app loading; fire assertions
new App({ target: document.body });

// For devs (and users on LINE desktop, which is rare)
if (!liff.isLoggedIn()) {
liff.login({
// https://github.com/line/line-liff-v2-starter/issues/4
redirectUri: location.href,
});
} else {
dataLayer.push({ userId: liff.getDecodedIDToken().sub });

// Kickstart app loading; fire assertions
new App({ target: document.body });
}
});
19 changes: 5 additions & 14 deletions src/liff/pages/Article.svelte
Expand Up @@ -2,7 +2,7 @@
import { onMount } from 'svelte';
import { t } from 'ttag';
import { gql } from '../lib';
import { gaTitle, getArticleURL, VIEW_ARTICLE_PREFIX } from 'src/lib/sharedUtils';
import { getArticleURL, VIEW_ARTICLE_PREFIX } from 'src/lib/sharedUtils';
import AppBar from '../components/AppBar.svelte';
import SingleColorLogo from '../components/icons/SingleColorLogo.svelte';
import FullpagePrompt from '../components/FullpagePrompt.svelte';
Expand Down Expand Up @@ -57,25 +57,14 @@
return;
}
dataLayer.push({ event: 'dataLoaded', doc: GetArticle });
const {articleReplies: list, ...rest} = GetArticle;
articleReplies = !replyId ? list : list.filter(({reply}) => reply.id === replyId);
collapsedArticleReplies = !replyId ? [] : list.filter(({reply}) => reply.id !== replyId);
articleData = rest;
createdAt = new Date(articleData.createdAt);
// Send event to Google Analytics
gtag('set', { page_title: gaTitle(articleData.text) });
gtag('event', 'ViewArticle', {
event_category: 'LIFF',
event_label: articleId,
});
articleReplies.forEach(({reply}) => {
gtag('event', 'ViewReply', {
event_category: 'LIFF',
event_label: reply.id,
});
})
}
const setViewed = async () => {
Expand All @@ -87,6 +76,8 @@
}
onMount(() => {
dataLayer.push({ articleId, replyId });
loadData();
setViewed();
});
Expand Down
23 changes: 9 additions & 14 deletions src/liff/pages/Articles.svelte
Expand Up @@ -14,20 +14,15 @@
let articleMap = {};
let selectArticle = async articleId => {
await Promise.all([
sendMessages([{
type: 'text',
text: `${VIEW_ARTICLE_PREFIX}${getArticleURL(articleId)}`,
}]),
new Promise(
resolve =>
gtag('event', 'ChooseArticle', {
event_category: 'LIFF',
event_label: articleId,
event_callback: () => resolve(),
})
)
]);
dataLayer.push({
event: 'chooseArticle',
articleId,
});
await sendMessages([{
type: 'text',
text: `${VIEW_ARTICLE_PREFIX}${getArticleURL(articleId)}`,
}]);
liff.closeWindow();
}
Expand Down
10 changes: 5 additions & 5 deletions src/liff/pages/Comment.svelte
@@ -1,7 +1,6 @@
<script>
import { onMount } from 'svelte';
import { t, ngettext, msgid } from 'ttag';
import { gaTitle } from 'src/lib/sharedUtils';
import ReplyRequestForm from '../components/ReplyRequestForm.svelte';
import { gql } from '../lib';
Expand All @@ -14,6 +13,8 @@
let reason = '';
onMount(async () => {
dataLayer.push({articleId});
// Load searchedText from API
const {data, errors} = await gql`
query GetCurrentUserRequestInLIFF($articleId: String) {
Expand Down Expand Up @@ -48,10 +49,9 @@
searchedText = data.ListReplyRequests.edges[0].node.article.text;
reason = data.ListReplyRequests.edges[0].node.reason;
gtag('set', { page_title: gaTitle(searchedText) });
gtag('event', 'Comment', {
event_category: 'LIFF',
event_label: articleId,
dataLayer.push({
event: 'dataLoaded',
doc: data.ListReplyRequests.edges[0].node,
});
});
Expand Down
19 changes: 4 additions & 15 deletions src/liff/pages/Feedback.svelte
@@ -1,7 +1,6 @@
<script>
import { onMount } from 'svelte';
import { t } from 'ttag';
import { gaTitle } from 'src/lib/sharedUtils';
import { gql } from '../lib';
import FeedbackForm from '../components/FeedbackForm.svelte' ;
Expand All @@ -22,6 +21,8 @@
// Submitting feedback with existing comment first
//
onMount(async () => {
dataLayer.push({articleId, replyId});
// Load searchedText and previous comment
const { data, errors } = await gql`
query GetCurrentUserFeedbackInLIFF($articleId: String!, $replyId: String!) {
Expand All @@ -39,9 +40,6 @@
}
}
}
GetArticle(id: $articleId) {
text
}
}
`({articleId, replyId});
Expand All @@ -52,16 +50,11 @@
return;
}
if(!data.GetArticle) {
alert('Article not found');
return;
}
// Loads previous comment
// (previous vote will be overwritten by current vote)
comment = data.ListArticleReplyFeedbacks.edges[0]?.node.comment ?? '';
gtag('set', { page_title: gaTitle(data.GetArticle.text) });
dataLayer.push({event: 'dataLoaded', doc: data.ListArticleReplyFeedbacks.edges[0]?.node})
await submitFeedback();
});
Expand Down Expand Up @@ -96,11 +89,7 @@
* Submit feedback with current parameters
*/
function submitFeedback() {
// Use UserInput category for data consistency
gtag('event', 'Feedback-Vote', {
event_category: 'UserInput',
event_label: `${articleId}/${replyId}`,
});
dataLayer.push({event: 'feedbackVote'});
return gql`
mutation SubmitFeedbackInLIFF(
Expand Down

0 comments on commit 0bc402e

Please sign in to comment.