@sesamy/sesamy-js 1.3.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env +2 -0
- package/.env.production +2 -0
- package/.eslintignore +4 -0
- package/.eslintrc +18 -0
- package/.github/workflows/release.yml +23 -0
- package/.nvmrc +1 -0
- package/.prettierignore +4 -0
- package/.prettierrc +5 -0
- package/CHANGELOG.md +132 -0
- package/article.html +65 -0
- package/contracts.html +23 -0
- package/dts-bundle-generator.config.ts +11 -0
- package/entitlements.html +23 -0
- package/index.html +24 -0
- package/package.json +1 -5
- package/public/sesamy.png +0 -0
- package/renovate.json +4 -0
- package/src/SesamyContracts.ts +86 -0
- package/src/SesamyEntitlements.ts +85 -0
- package/src/app.ts +101 -0
- package/src/constants.ts +2 -0
- package/src/controllers/index.ts +1 -0
- package/src/controllers/paywall.ts +14 -0
- package/src/entitlementsStyle.css +147 -0
- package/src/events/ready.ts +12 -0
- package/src/index.ts +36 -0
- package/src/javascript-api.ts +84 -0
- package/src/services/analytics/element-tracker.ts +117 -0
- package/src/services/analytics/index.ts +234 -0
- package/src/services/analytics/listeners/index.ts +2 -0
- package/src/services/analytics/listeners/route.ts +9 -0
- package/src/services/analytics/listeners/scroll.ts +62 -0
- package/src/services/analytics/session-id.ts +11 -0
- package/src/services/analytics/types/analytics-activity-utils.d.ts +54 -0
- package/src/services/analytics/types/analytics-router-utils.d.ts +7 -0
- package/src/services/analytics/types/analytics-scroll-utils.d.ts +4 -0
- package/src/services/analytics/types/track-event.ts +70 -0
- package/src/services/auth/index.ts +74 -0
- package/src/services/sesamy/index.ts +160 -0
- package/src/state.ts +3 -0
- package/src/style.css +99 -0
- package/src/types/Bills.ts +12 -0
- package/src/types/Config.ts +11 -0
- package/src/types/Contracts.ts +12 -0
- package/src/types/Entitlement.ts +9 -0
- package/src/types/Events.ts +6 -0
- package/src/types/Tag.ts +16 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +23 -0
- package/vite.config.ts +43 -0
- package/vite.dev.config.ts +14 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { triggerEvent } from '../events/ready';
|
|
2
|
+
import { accessArticle as accessArticleService } from '../services/sesamy';
|
|
3
|
+
import { Events } from '../types/Events';
|
|
4
|
+
|
|
5
|
+
export async function accessArticle(articleId: string) {
|
|
6
|
+
// Call the API to access the article
|
|
7
|
+
const response = await accessArticleService({ articleId });
|
|
8
|
+
|
|
9
|
+
if (response.status !== 'ok') {
|
|
10
|
+
triggerEvent(Events.SOFT_PAYWALL, {});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return response;
|
|
14
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
background-color: var(--background, transparent);
|
|
3
|
+
font-family: var(--font-family, 'Helvetica');
|
|
4
|
+
}
|
|
5
|
+
ul {
|
|
6
|
+
list-style-type: none;
|
|
7
|
+
display: flex;
|
|
8
|
+
flex-wrap: wrap;
|
|
9
|
+
gap: var(--items-gap, 16px);
|
|
10
|
+
margin-top: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
}
|
|
13
|
+
ul > * {
|
|
14
|
+
flex-grow: 1;
|
|
15
|
+
flex-shrink: 1;
|
|
16
|
+
flex-basis: var(--item-width, 300px);
|
|
17
|
+
}
|
|
18
|
+
button {
|
|
19
|
+
appearance: none;
|
|
20
|
+
display: inline-block;
|
|
21
|
+
text-decoration: none;
|
|
22
|
+
text-align: left;
|
|
23
|
+
background: none;
|
|
24
|
+
border: none;
|
|
25
|
+
width: 100%;
|
|
26
|
+
display: flex;
|
|
27
|
+
align-items: stretch;
|
|
28
|
+
text-decoration: none;
|
|
29
|
+
cursor: pointer;
|
|
30
|
+
}
|
|
31
|
+
button::-moz-focus-inner {
|
|
32
|
+
border: 0;
|
|
33
|
+
padding: 0;
|
|
34
|
+
}
|
|
35
|
+
button > * {
|
|
36
|
+
flex-shrink: 0;
|
|
37
|
+
}
|
|
38
|
+
.image-container {
|
|
39
|
+
padding-bottom: 14px;
|
|
40
|
+
}
|
|
41
|
+
img {
|
|
42
|
+
width: var(--image-size, 95px);
|
|
43
|
+
height: var(--image-size, 95px);
|
|
44
|
+
border-radius: var(--image-border-radius, 12px);
|
|
45
|
+
object-fit: cover;
|
|
46
|
+
overflow: hidden;
|
|
47
|
+
display: -webkit-box;
|
|
48
|
+
-webkit-line-clamp: 2;
|
|
49
|
+
-webkit-box-orient: vertical;
|
|
50
|
+
}
|
|
51
|
+
.details {
|
|
52
|
+
margin-left: var(--details-margin-left, 8px);
|
|
53
|
+
border-bottom-color: var(--divider-color, #70707023);
|
|
54
|
+
border-bottom-width: var(--divider-width, 1px);
|
|
55
|
+
padding-bottom: 14px;
|
|
56
|
+
border-bottom-style: solid;
|
|
57
|
+
display: flex;
|
|
58
|
+
flex-direction: column;
|
|
59
|
+
justify-content: var(--details-justify-content, space-between);
|
|
60
|
+
flex: 1;
|
|
61
|
+
}
|
|
62
|
+
p {
|
|
63
|
+
font-weight: normal;
|
|
64
|
+
margin: 0;
|
|
65
|
+
text-overflow: ellipsis;
|
|
66
|
+
overflow: hidden;
|
|
67
|
+
display: -webkit-box;
|
|
68
|
+
-webkit-box-orient: vertical;
|
|
69
|
+
}
|
|
70
|
+
.title {
|
|
71
|
+
color: var(--title-color, #131313);
|
|
72
|
+
font-size: 15px;
|
|
73
|
+
-webkit-line-clamp: 1;
|
|
74
|
+
}
|
|
75
|
+
.summary {
|
|
76
|
+
color: var(--summary-color, #22222260);
|
|
77
|
+
font-size: 12px;
|
|
78
|
+
-webkit-line-clamp: 2;
|
|
79
|
+
}
|
|
80
|
+
span {
|
|
81
|
+
overflow: hidden;
|
|
82
|
+
text-overflow: ellipsis;
|
|
83
|
+
display: -webkit-box;
|
|
84
|
+
-webkit-line-clamp: 2;
|
|
85
|
+
-webkit-box-orient: vertical;
|
|
86
|
+
}
|
|
87
|
+
span.type {
|
|
88
|
+
color: var(--type-color, #22222270);
|
|
89
|
+
text-transform: uppercase;
|
|
90
|
+
font-size: var(--type-font-size, 11px);
|
|
91
|
+
font-weight: medium;
|
|
92
|
+
margin-top: 4px;
|
|
93
|
+
font-family: var(--type-font-family, var(--font-family, 'Helvetica'));
|
|
94
|
+
}
|
|
95
|
+
span.date {
|
|
96
|
+
color: var(--date-color, #22222260);
|
|
97
|
+
font-size: var(--date-font-size, 12px);
|
|
98
|
+
font-weight: normal;
|
|
99
|
+
font-family: var(--date-font-family, var(--font-family, 'Helvetica'));
|
|
100
|
+
}
|
|
101
|
+
.modal {
|
|
102
|
+
display: none;
|
|
103
|
+
position: fixed;
|
|
104
|
+
z-index: var(--modal-zindex, 100);
|
|
105
|
+
padding-top: 20px;
|
|
106
|
+
left: 0;
|
|
107
|
+
top: 0;
|
|
108
|
+
width: 100%;
|
|
109
|
+
height: 100%;
|
|
110
|
+
overflow: auto;
|
|
111
|
+
}
|
|
112
|
+
.modal-blackout {
|
|
113
|
+
position: fixed;
|
|
114
|
+
top: 0;
|
|
115
|
+
left: 0;
|
|
116
|
+
width: 100%;
|
|
117
|
+
height: 100%;
|
|
118
|
+
background-color: rgba(0, 0, 0, 0.4);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.modal-container {
|
|
122
|
+
position: relative;
|
|
123
|
+
background-color: white;
|
|
124
|
+
margin: auto;
|
|
125
|
+
padding: 0;
|
|
126
|
+
max-width: 500px;
|
|
127
|
+
-webkit-animation-name: animatetop;
|
|
128
|
+
-webkit-animation-duration: 0.4s;
|
|
129
|
+
animation-name: animatetop;
|
|
130
|
+
animation-duration: 0.4s;
|
|
131
|
+
}
|
|
132
|
+
.close-container {
|
|
133
|
+
position: absolute;
|
|
134
|
+
right: 16px;
|
|
135
|
+
top: 16px;
|
|
136
|
+
cursor: pointer;
|
|
137
|
+
z-index: 900;
|
|
138
|
+
width: auto;
|
|
139
|
+
}
|
|
140
|
+
@media only screen and (max-width: 768px) {
|
|
141
|
+
.modal {
|
|
142
|
+
padding: 10px 0;
|
|
143
|
+
}
|
|
144
|
+
.modal-container {
|
|
145
|
+
max-width: 100%;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Events } from '../types/Events';
|
|
2
|
+
|
|
3
|
+
// TODO: Separate the event into a function per event so they can be typed
|
|
4
|
+
export function triggerEvent(eventType: Events, payload: any) {
|
|
5
|
+
const customEvent = new CustomEvent(eventType, {
|
|
6
|
+
detail: payload,
|
|
7
|
+
bubbles: true,
|
|
8
|
+
composed: true,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
dispatchEvent(customEvent);
|
|
12
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { triggerEvent } from './events/ready';
|
|
2
|
+
import { registerAPI } from './javascript-api';
|
|
3
|
+
import { Config } from './types/Config';
|
|
4
|
+
import { init as initAuth } from './services/auth';
|
|
5
|
+
import { initAnalytics } from './services/analytics';
|
|
6
|
+
import { Events } from './types/Events';
|
|
7
|
+
|
|
8
|
+
export async function init(config: Config) {
|
|
9
|
+
initAnalytics({
|
|
10
|
+
clientId: config.clientId,
|
|
11
|
+
// The default client id can be overridden by the config
|
|
12
|
+
...config.analytics,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Auth needs to be initated after the analytics so that the auth events can be tracked
|
|
16
|
+
await initAuth(config);
|
|
17
|
+
|
|
18
|
+
const api = registerAPI(config.namespace);
|
|
19
|
+
|
|
20
|
+
triggerEvent(Events.READY, {});
|
|
21
|
+
|
|
22
|
+
return api;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// This is for checking if the script is being loaded in the browser
|
|
26
|
+
if (typeof document !== 'undefined') {
|
|
27
|
+
const configElement = document.getElementById('sesamy-js');
|
|
28
|
+
if (configElement?.textContent) {
|
|
29
|
+
try {
|
|
30
|
+
const config = JSON.parse(configElement.textContent);
|
|
31
|
+
init(config);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
console.error('Failed to parse config', err);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { loginWithRedirect, getUser, isAuthenticated, logout } from './services/auth';
|
|
2
|
+
import {
|
|
3
|
+
getEntitlement,
|
|
4
|
+
getEntitlements,
|
|
5
|
+
getContract,
|
|
6
|
+
getContracts,
|
|
7
|
+
getBill,
|
|
8
|
+
getBills,
|
|
9
|
+
getTags,
|
|
10
|
+
setTag,
|
|
11
|
+
pushTag,
|
|
12
|
+
} from './services/sesamy';
|
|
13
|
+
import { accessArticle } from './controllers/paywall';
|
|
14
|
+
import { version } from '../package.json';
|
|
15
|
+
|
|
16
|
+
function getVersion() {
|
|
17
|
+
return version;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SesamyApi {
|
|
21
|
+
articles: {
|
|
22
|
+
access: typeof accessArticle;
|
|
23
|
+
};
|
|
24
|
+
auth: {
|
|
25
|
+
getUser: typeof getUser;
|
|
26
|
+
isAuthenticated: typeof isAuthenticated;
|
|
27
|
+
loginWithRedirect: typeof loginWithRedirect;
|
|
28
|
+
logout: typeof logout;
|
|
29
|
+
};
|
|
30
|
+
tags: {
|
|
31
|
+
list: typeof getTags;
|
|
32
|
+
set: typeof setTag;
|
|
33
|
+
push: typeof pushTag;
|
|
34
|
+
};
|
|
35
|
+
getEntitlement: typeof getEntitlement;
|
|
36
|
+
getEntitlements: typeof getEntitlements;
|
|
37
|
+
getContract: typeof getContract;
|
|
38
|
+
getContracts: typeof getContracts;
|
|
39
|
+
getBill: typeof getBill;
|
|
40
|
+
getBills: typeof getBills;
|
|
41
|
+
getVersion: typeof getVersion;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface SesamyWindow {
|
|
45
|
+
sesamy: SesamyApi;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Extend the global Window type with your SesamyWindow interface
|
|
49
|
+
declare global {
|
|
50
|
+
interface Window extends SesamyWindow {}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function registerAPI(namespace: string | null = 'sesamy') {
|
|
54
|
+
const api: SesamyApi = {
|
|
55
|
+
auth: {
|
|
56
|
+
getUser,
|
|
57
|
+
isAuthenticated,
|
|
58
|
+
loginWithRedirect,
|
|
59
|
+
logout,
|
|
60
|
+
},
|
|
61
|
+
articles: {
|
|
62
|
+
access: accessArticle,
|
|
63
|
+
},
|
|
64
|
+
tags: {
|
|
65
|
+
list: getTags,
|
|
66
|
+
set: setTag,
|
|
67
|
+
push: pushTag,
|
|
68
|
+
},
|
|
69
|
+
getEntitlement,
|
|
70
|
+
getEntitlements,
|
|
71
|
+
getContract,
|
|
72
|
+
getContracts,
|
|
73
|
+
getBill,
|
|
74
|
+
getBills,
|
|
75
|
+
getVersion,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
if (namespace !== null) {
|
|
79
|
+
// This makes it possible to use a different namespace
|
|
80
|
+
(window as any)[namespace] = api;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return api;
|
|
84
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/// <reference path="./types/analytics-activity-utils.d.ts" />
|
|
2
|
+
import { onUserActivity } from '@analytics/activity-utils';
|
|
3
|
+
|
|
4
|
+
// IDLE_TIME is in ms
|
|
5
|
+
const timeout = 5000;
|
|
6
|
+
|
|
7
|
+
export interface ElementTrackerOptions {
|
|
8
|
+
element: HTMLElement;
|
|
9
|
+
viewCallback?: () => void;
|
|
10
|
+
activeDurationCallback?: (duration: number) => void;
|
|
11
|
+
idleDurationCallback?: (duration: number) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default class ElementTracker {
|
|
15
|
+
element: HTMLElement;
|
|
16
|
+
|
|
17
|
+
isInViewport = false;
|
|
18
|
+
|
|
19
|
+
isAwake = false;
|
|
20
|
+
|
|
21
|
+
isFlushing?: boolean;
|
|
22
|
+
|
|
23
|
+
observer: IntersectionObserver;
|
|
24
|
+
|
|
25
|
+
lastEventAt = Date.now();
|
|
26
|
+
|
|
27
|
+
registeredView = false;
|
|
28
|
+
|
|
29
|
+
viewCallback?: () => void;
|
|
30
|
+
|
|
31
|
+
activeDurationCallback?: (duration: number, flushing?: boolean) => void;
|
|
32
|
+
|
|
33
|
+
idleDurationCallback?: (duration: number, flushing?: boolean) => void;
|
|
34
|
+
|
|
35
|
+
constructor(options: ElementTrackerOptions) {
|
|
36
|
+
this.element = options.element;
|
|
37
|
+
// We NEED to have callbacks here rather than using events as events will be lost when the browser is unloading
|
|
38
|
+
this.viewCallback = options.viewCallback;
|
|
39
|
+
this.activeDurationCallback = options.activeDurationCallback;
|
|
40
|
+
this.idleDurationCallback = options.idleDurationCallback;
|
|
41
|
+
|
|
42
|
+
// Setup the interaction observer
|
|
43
|
+
this.observer = new IntersectionObserver(
|
|
44
|
+
entries => {
|
|
45
|
+
// I guess there will only be one entry? Verify?
|
|
46
|
+
entries.forEach(entry => {
|
|
47
|
+
this.handleInViewPort(entry.isIntersecting);
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
threshold: 0,
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
this.observer.observe(this.element);
|
|
55
|
+
|
|
56
|
+
// Setup the active/idle
|
|
57
|
+
onUserActivity({
|
|
58
|
+
onIdle: (elapsedTime: number) => this.handleAwake(false, elapsedTime),
|
|
59
|
+
onWakeUp: (elapsedTime: number) => this.handleAwake(true, elapsedTime),
|
|
60
|
+
timeout,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Flush the awake timer in case the page is unloading
|
|
66
|
+
*/
|
|
67
|
+
flush() {
|
|
68
|
+
this.isFlushing = true;
|
|
69
|
+
this.handleAwake(!this.isAwake, Math.round((Date.now() - this.lastEventAt) / 1000));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
handleInViewPort(isInViewport: boolean) {
|
|
73
|
+
if (isInViewport) {
|
|
74
|
+
// A view envent only occurs if the element is active.
|
|
75
|
+
this.isAwake = true;
|
|
76
|
+
this.trackInViewport();
|
|
77
|
+
} else {
|
|
78
|
+
// An article outside the viewport is not active
|
|
79
|
+
this.handleAwake(false, Math.round((Date.now() - this.lastEventAt) / 1000));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.isInViewport = isInViewport;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
handleAwake(isAwake: boolean, elapsedTime: number) {
|
|
86
|
+
// Store the current awake state
|
|
87
|
+
this.isAwake = isAwake;
|
|
88
|
+
this.lastEventAt = isAwake ? Date.now() - elapsedTime * timeout : Date.now();
|
|
89
|
+
|
|
90
|
+
if (!this.isInViewport) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
this.trackAwake(isAwake, elapsedTime);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
trackAwake(awake: boolean, elapsedTime: number) {
|
|
97
|
+
if (!awake && this.activeDurationCallback) {
|
|
98
|
+
this.activeDurationCallback(elapsedTime, this.isFlushing);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (awake && this.idleDurationCallback) {
|
|
102
|
+
this.idleDurationCallback(elapsedTime, this.isFlushing);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
trackInViewport() {
|
|
107
|
+
if (this.registeredView) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.registeredView = true;
|
|
112
|
+
|
|
113
|
+
if (this.viewCallback) {
|
|
114
|
+
this.viewCallback();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import Analytics, { AnalyticsInstance } from 'analytics';
|
|
2
|
+
import saturated from 'saturated';
|
|
3
|
+
import { name, version } from '../../../package.json';
|
|
4
|
+
import { TrackEvent } from './types/track-event';
|
|
5
|
+
import { AnalyticsConfig } from '../../types/Config';
|
|
6
|
+
import { routeChangeListener } from './listeners';
|
|
7
|
+
import ElementTracker from './element-tracker';
|
|
8
|
+
import { ANALYTICS_BASE_URL } from '../../constants';
|
|
9
|
+
import { getSessionId } from './session-id';
|
|
10
|
+
import { Events } from '../../types/Events';
|
|
11
|
+
|
|
12
|
+
type Analytic = {
|
|
13
|
+
anonymousId: string;
|
|
14
|
+
userId?: string;
|
|
15
|
+
properties: { [key: string]: string | number };
|
|
16
|
+
event?: string;
|
|
17
|
+
type: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
let flushing = false;
|
|
21
|
+
|
|
22
|
+
let _clientId: string;
|
|
23
|
+
let _enabled: boolean;
|
|
24
|
+
let _endpoint: string;
|
|
25
|
+
|
|
26
|
+
export interface AnalyticsEventWithType extends Analytic {
|
|
27
|
+
type: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function initAnalytics({ clientId, enabled = true, endpoint = ANALYTICS_BASE_URL }: AnalyticsConfig) {
|
|
31
|
+
_clientId = clientId;
|
|
32
|
+
_enabled = enabled;
|
|
33
|
+
_endpoint = endpoint;
|
|
34
|
+
|
|
35
|
+
if (!_enabled) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Register route change listener
|
|
40
|
+
routeChangeListener();
|
|
41
|
+
|
|
42
|
+
// Register body events
|
|
43
|
+
const bodyTracker = new ElementTracker({
|
|
44
|
+
element: document.body,
|
|
45
|
+
viewCallback: () => {
|
|
46
|
+
analytics.page();
|
|
47
|
+
},
|
|
48
|
+
activeDurationCallback: (duration: number, flushing?: boolean) => {
|
|
49
|
+
analytics.track('activeDuration', {
|
|
50
|
+
duration,
|
|
51
|
+
flushing,
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
idleDurationCallback: (duration: number, flushing?: boolean) => {
|
|
55
|
+
analytics.track('idleDuration', {
|
|
56
|
+
duration,
|
|
57
|
+
flushing,
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
addUnloadPageListener(document.body, () => {
|
|
63
|
+
bodyTracker.flush();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
window.addEventListener(Events.AUTHENTICATED, async (event: Event) => {
|
|
67
|
+
const customEvent = event as CustomEvent;
|
|
68
|
+
await analytics.identify(customEvent.detail.sub);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
window.addEventListener(Events.LOGOUT, async () => {
|
|
72
|
+
await analytics.track('logout', {});
|
|
73
|
+
queue.flush();
|
|
74
|
+
await analytics.reset();
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function createMessage(payload: AnalyticsEventWithType[]): string {
|
|
79
|
+
return JSON.stringify(
|
|
80
|
+
payload.map(item => ({
|
|
81
|
+
...item,
|
|
82
|
+
clientId: _clientId,
|
|
83
|
+
requestId: Math.random().toString(36).slice(2, 9),
|
|
84
|
+
sessionId: getSessionId(),
|
|
85
|
+
timestamp: new Date().toISOString(),
|
|
86
|
+
version,
|
|
87
|
+
event: item.event,
|
|
88
|
+
context: {
|
|
89
|
+
page: {
|
|
90
|
+
url: window.location.hostname,
|
|
91
|
+
path: window.location.pathname,
|
|
92
|
+
title: document.title,
|
|
93
|
+
search: window.location.search,
|
|
94
|
+
referrer: document.referrer,
|
|
95
|
+
},
|
|
96
|
+
locale: navigator.language,
|
|
97
|
+
library: name,
|
|
98
|
+
userAgent: navigator.userAgent,
|
|
99
|
+
clientId: _clientId,
|
|
100
|
+
},
|
|
101
|
+
})),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const queue = saturated(
|
|
106
|
+
async arr => {
|
|
107
|
+
if (arr.length > 0) {
|
|
108
|
+
const payload = createMessage(arr);
|
|
109
|
+
|
|
110
|
+
if (flushing) {
|
|
111
|
+
navigator.sendBeacon(_endpoint, payload);
|
|
112
|
+
} else {
|
|
113
|
+
const response = await fetch(_endpoint, {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
body: payload,
|
|
116
|
+
headers: {
|
|
117
|
+
'Content-Type': 'text/plain',
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
if (!response.ok) {
|
|
122
|
+
// TODO: Retry, maybe store data in local storage
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
max: 10, // limit
|
|
129
|
+
interval: 3000, // 3s
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
function send(payload: AnalyticsEventWithType) {
|
|
134
|
+
if (!payload.anonymousId) {
|
|
135
|
+
// When resetting the analytics, the anonymousId is set to null. Skip sending these events
|
|
136
|
+
} else if (payload.properties?.flushing) {
|
|
137
|
+
const payloadWithoutFlushing = { ...payload };
|
|
138
|
+
delete payloadWithoutFlushing.properties.flushing;
|
|
139
|
+
|
|
140
|
+
navigator.sendBeacon(_endpoint, createMessage([payloadWithoutFlushing]));
|
|
141
|
+
} else {
|
|
142
|
+
queue.push(payload);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export const analytics: AnalyticsInstance = Analytics({
|
|
147
|
+
app: name,
|
|
148
|
+
version,
|
|
149
|
+
plugins: [
|
|
150
|
+
{
|
|
151
|
+
name: 'custom-analytics-plugin',
|
|
152
|
+
page: ({ payload }: { payload: Analytic }) => {
|
|
153
|
+
const { properties, anonymousId, userId, event } = payload;
|
|
154
|
+
const analyticsEvent = {
|
|
155
|
+
anonymousId,
|
|
156
|
+
userId,
|
|
157
|
+
properties,
|
|
158
|
+
event,
|
|
159
|
+
type: 'page',
|
|
160
|
+
};
|
|
161
|
+
// Send data to custom collection endpoint
|
|
162
|
+
send(analyticsEvent);
|
|
163
|
+
},
|
|
164
|
+
track: ({ payload }: { payload: Analytic }) => {
|
|
165
|
+
const { properties, anonymousId, userId, event } = payload;
|
|
166
|
+
const analyticsEvent: AnalyticsEventWithType = {
|
|
167
|
+
anonymousId,
|
|
168
|
+
userId,
|
|
169
|
+
properties,
|
|
170
|
+
event,
|
|
171
|
+
type: 'track',
|
|
172
|
+
};
|
|
173
|
+
send(analyticsEvent);
|
|
174
|
+
},
|
|
175
|
+
identify: ({ payload }: { payload: Analytic }) => {
|
|
176
|
+
const { properties, anonymousId, userId } = payload;
|
|
177
|
+
const analyticsEvent: AnalyticsEventWithType = {
|
|
178
|
+
anonymousId,
|
|
179
|
+
userId,
|
|
180
|
+
properties,
|
|
181
|
+
type: 'identify',
|
|
182
|
+
};
|
|
183
|
+
send(analyticsEvent);
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
export function getAnonymousUserId() {
|
|
190
|
+
return analytics.user().anonymousId;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
194
|
+
export function track(eventName: string, payload: any): void {
|
|
195
|
+
analytics.track(eventName, payload);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function trackEvent(event: TrackEvent): void {
|
|
199
|
+
analytics.track(event.name, event);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function flushQueue() {
|
|
203
|
+
flushing = true;
|
|
204
|
+
return queue.flush();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function enableInteractions(callback?: () => void) {
|
|
208
|
+
analytics.plugins.enable('custom-analytics-plugin', callback);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function disableInteractions(callback?: () => void) {
|
|
212
|
+
analytics.plugins.disable('custom-analytics-plugin', callback || (() => true));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export type Action = () => void;
|
|
216
|
+
const unloadPageListeners = new Map<Element, Action>();
|
|
217
|
+
|
|
218
|
+
export function addUnloadPageListener(element: Element, action: Action) {
|
|
219
|
+
unloadPageListeners.set(element, action);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function removeUnloadPageListener(element: Element) {
|
|
223
|
+
unloadPageListeners.delete(element);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
window.addEventListener('beforeunload', () => {
|
|
227
|
+
// Flush the queues for all the registered element trackers
|
|
228
|
+
unloadPageListeners.forEach((action, element) => {
|
|
229
|
+
action.bind(element)();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Flush the outbound queue
|
|
233
|
+
flushQueue();
|
|
234
|
+
});
|