xref: /plugin/statistics/script.js (revision d40a62919b0661e8ac2124588f3f26da5dd7bab6)
1/* DOKUWIKI:include_once lib/chart.js */
2/* DOKUWIKI:include_once lib/chartjs-plugin-datalabels.js */
3
4/* globals JSINFO, DOKU_BASE, DokuCookie */
5
6/**
7 * Modern Statistics Plugin
8 */
9class StatisticsPlugin {
10    constructor() {
11        this.data = {};
12        this.sessionTimeout = 15 * 60 * 1000; // 15 minutes
13        this.uid = this.initializeUserTracking();
14        this.sessionId = this.getSession();
15    }
16
17    /**
18     * Initialize the statistics plugin
19     */
20    async init() {
21        try {
22            this.buildTrackingData();
23            await this.logPageView();
24            this.attachEventListeners();
25        } catch (error) {
26            console.error('Statistics plugin initialization failed:', error);
27        }
28    }
29
30    /**
31     * Initialize user tracking with visitor cookie
32     * @returns {string} User ID (UUID)
33     */
34    initializeUserTracking() {
35        let uid = DokuCookie.getValue('plgstats');
36        if (!uid) {
37            uid = this.generateUUID();
38            DokuCookie.setValue('plgstats', uid);
39        }
40        return uid;
41    }
42
43
44    /**
45     * Build tracking data object
46     */
47    buildTrackingData() {
48        const now = Date.now();
49        this.data = {
50            uid: this.uid,
51            ses: this.sessionId,
52            p: JSINFO.id,
53            r: document.referrer,
54            sx: screen.width,
55            sy: screen.height,
56            vx: window.innerWidth,
57            vy: window.innerHeight,
58            js: 1,
59            rnd: now
60        };
61    }
62
63    /**
64     * Log page view based on action
65     */
66    async logPageView() {
67        const action = JSINFO.act === 'show' ? 'v' : 's';
68        await this.logView(action);
69    }
70
71    /**
72     * Attach event listeners for tracking
73     */
74    attachEventListeners() {
75        // Track external link clicks
76        document.querySelectorAll('a.urlextern').forEach(link => {
77            link.addEventListener('click', this.logExternal.bind(this));
78        });
79
80        // Track page unload
81        window.addEventListener('beforeunload', this.logExit.bind(this));
82    }
83
84    /**
85     * Log a view or session
86     * @param {string} action 'v' = view, 's' = session
87     */
88    async logView(action) {
89        const params = new URLSearchParams(this.data);
90        const url = `${DOKU_BASE}lib/plugins/statistics/log.php?do=${action}&${params}`;
91
92        try {
93            // Use fetch with keepalive for better reliability
94            await fetch(url, {
95                method: 'GET',
96                keepalive: true,
97                cache: 'no-cache'
98            });
99        } catch (error) {
100            // Fallback to image beacon for older browsers
101            const img = new Image();
102            img.src = url;
103        }
104    }
105
106    /**
107     * Log clicks to external URLs
108     * @param {Event} event Click event
109     */
110    logExternal(event) {
111        const params = new URLSearchParams(this.data);
112        const url = `${DOKU_BASE}lib/plugins/statistics/log.php?do=o&ol=${encodeURIComponent(event.target.href)}&${params}`;
113
114        // Use sendBeacon for reliable tracking
115        if (navigator.sendBeacon) {
116            navigator.sendBeacon(url);
117        } else {
118            // Fallback for older browsers
119            const img = new Image();
120            img.src = url;
121        }
122
123        return true;
124    }
125
126    /**
127     * Log page exit as session info
128     */
129    logExit() {
130        const currentSession = this.getSession();
131        if (currentSession !== this.sessionId) {
132            return; // Session expired, don't log
133        }
134
135        const params = new URLSearchParams(this.data);
136        const url = `${DOKU_BASE}lib/plugins/statistics/log.php?do=s&${params}`;
137
138        if (navigator.sendBeacon) {
139            navigator.sendBeacon(url);
140        }
141    }
142
143    /**
144     * Get current session identifier
145     * Auto clears expired sessions and creates new ones after 15 min idle time
146     * @returns {string} Session ID
147     */
148    getSession() {
149        const now = Date.now();
150
151        // Load session cookie
152        let sessionData = DokuCookie.getValue('plgstatsses');
153        let sessionId = '';
154
155        if (sessionData) {
156            const [timestamp, id] = sessionData.split('-', 2);
157            if (now - parseInt(timestamp, 10) <= this.sessionTimeout) {
158                sessionId = id;
159            }
160        }
161
162        // Generate new session if needed
163        if (!sessionId) {
164            sessionId = this.generateUUID();
165        }
166
167        // Update session cookie
168        DokuCookie.setValue('plgstatsses', `${now}-${sessionId}`);
169        return sessionId;
170    }
171
172    /**
173     * Generate a UUID v4
174     * @returns {string} UUID
175     */
176    generateUUID() {
177        function s4() {
178            return Math.floor((1 + Math.random()) * 0x10000)
179                .toString(16)
180                .substring(1);
181        }
182
183        return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
184    }
185}
186
187// Initialize when DOM is ready
188if (document.readyState === 'loading') {
189    document.addEventListener('DOMContentLoaded', () => {
190        new StatisticsPlugin().init();
191    });
192} else {
193    // DOM already loaded
194    new StatisticsPlugin().init();
195}
196
197class ChartComponent extends HTMLElement {
198    connectedCallback() {
199        this.renderChart();
200    }
201
202    renderChart() {
203        const chartType = this.getAttribute('type');
204        const data = JSON.parse(this.getAttribute('data'));
205
206        console.log('data', data);
207
208        const canvas = document.createElement("canvas");
209        canvas.id = "myChart";
210        canvas.style.height = chartType === "pie" ? "300px" : "100%";
211        canvas.style.width = chartType === "pie" ? "300px" : "100%";
212
213        this.appendChild(canvas);
214
215        const ctx = canvas.getContext('2d');
216
217        // basic config
218        const config = {
219            type: chartType,
220            data: data
221        };
222
223        // percentage labels and tooltips for pie charts
224        if (chartType === "pie") {
225            // chartjs-plugin-datalabels needs to be registered
226            Chart.register(ChartDataLabels);
227
228            config.options =  {
229                plugins: {
230                    datalabels: {
231                        formatter: (value, context) => {
232                            const total = context.chart.data.datasets[0].data.reduce((a, b) => a + b, 0);
233                            const percentage = ((value / total) * 100).toFixed(2) + '%';
234                            return percentage;
235                        },
236                        color: '#fff',
237                    }
238                }
239            };
240        }
241
242
243        new Chart(ctx, config);
244    }
245}
246
247customElements.define('chart-component', ChartComponent);
248