Commit 57a6f05e authored by Laurent Sittler's avatar Laurent Sittler ©
Browse files

Merge branch 'dev' into 'master'

v1.1.0

Closes #4, #5, and #3

See merge request !9
parents 8a00a516 f49d51cd
Pipeline #1046 passed with stage
in 1 minute and 17 seconds
......@@ -5,6 +5,9 @@ npm-debug.log*
# Dependency directories
node_modules
package-lock.json
pnpm-lock.yaml
# Build generated files
dist
......
......@@ -8,10 +8,9 @@ extension:
only:
- tags
script:
- npm i -g gulp
- npm i
- gulp bundle --ship
- gulp package-solution --ship
- npm i -g pnpm
- pnpm i
- npm run build
- mv ./sharepoint/solution/matomo-analytics.sppkg ././matomo-analytics.sppkg
artifacts:
paths:
......
10
\ No newline at end of file
{
"@microsoft/generator-sharepoint": {
"isCreatingSolution": true,
"environment": "spo",
"version": "1.8.1",
"environment": "onprem19",
"version": "1.11.0",
"libraryName": "matomo-analytics",
"libraryId": "d6983821-bc57-4ef7-8f9e-0114173adc7a",
"packageManager": "npm",
"isDomainIsolated": false,
"componentType": "extension",
"extensionType": "ApplicationCustomizer"
}
......
......@@ -66,14 +66,14 @@ In this example, deploy and install the package only (the custom action will be
1. A Matomo Analytics Tracking Site ID
2. Site Collection App Catalog or Tenant App Catalog
3. Install [Office365 CLI](https://pnp.github.io/office365-cli/user-guide/installing-cli)
3. Install [CLI for Microsoft 365](https://pnp.github.io/cli-microsoft365/user-guide/installing-cli/)
```bash
npm i -g @pnp/office365-cli
npm install -g @pnp/cli-microsoft365
```
4. Execute
* login to your target SharePoint Site Collection (more information about [spo login](https://pnp.github.io/office365-cli/cmd/spo/login))
* login to your target SharePoint Site Collection (more information about [m365 login](https://pnp.github.io/cli-microsoft365/cmd/login/))
```bash
o365 spo login https://contoso.sharepoint.com/sites/target-site
m365 login
```
* allow execution setup script
```bash
......
......@@ -3,10 +3,9 @@
"solution": {
"name": "Matomo Analytics for SharePoint",
"id": "d6983821-bc57-4ef7-8f9e-0114173adc7a",
"version": "1.0.2.0",
"version": "1.1.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"features": [
{
"title": "Application Extension - Deployment of custom action.",
......@@ -15,8 +14,7 @@
"version": "1.0.0.0",
"assets": {
"elementManifests": [
"elements.xml",
"clientsideinstance.xml"
"elements.xml"
]
}
}
......
'use strict';
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
build.addSuppression(`Warning - MatomoApplicationCustomizer: Admins can make this solution available to all sites in the organization, but extensions won’t automatically appear. SharePoint Framework extensions must be specifically associated to sites, lists, and fields programmatically to be visible to site users.`);
build.addSuppression(`Warning - Admins can make this solution available to all sites immediately, but the solution also contains feature.xml elements for provisioning. Feature.xml elements are not automatically applied unless the solution is explicitly installed on a site.`);
build.initialize(gulp);
build.initialize(require('gulp'));
/**
* This JavaScript file has been created in order to track usage of the client-side components.
* The metrics server is based on an Anaytics tools that does not support multiple configurations
* and/or account keys. Due to this, please do no use it for your own projects.
*
* Only the bellow information are registered:
* - Package name
* - Package version
* - Properties used (no plain text vallues are stored)
* - Domain Only (ex: contoso.sharepoint.com)
* - Unique ID based on the hash of the domain
* - Tracked error from the code
*/
'use strict';
const p = require('./package.json');
const s = require('./config/package-solution.json');
var metrics = (function () {
const server = 'https://matomo.lsonline.fr/matomo.php';
const trackerId = 'dq8jNL692gWBQEpo';
var _instance;
function track(options) {
var url = new URL(server);
options.idsite = trackerId;
options.rec = 1;
url.search = new URLSearchParams(options);
var xhr = typeof XMLHttpRequest != 'undefined' ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');
try {
xhr.open('GET', url.href, true);
xhr.onreadystatechange = function () {
if (!/^(200|30[12478])$/.test(xhr.statusCode)) { /* if error - no action cause of CORS error currently */ }
};
xhr.send();
} catch (er) {
}
}
function formatcvar(cv) {
var f = {};
for (var k = 0; k < Object.keys(cv).length; k++) {
f[k + 1] = [Object.keys(cv)[k], Object.values(cv)[k]];
}
return f;
}
function hashString(str) {
var hash = 0;
if (str.length == 0) return hash;
for (var i = 0; i < str.length; i++) {
var char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return hash;
}
return {
getTelemetry: function (props) {
if (p.metrics.enable) {
if (!_instance) {
_instance = this;
}
track({
uid: hashString(document.location.origin),
url: document.location.origin,
action_name: p.name,
dimension1: p.version,
dimension2: s.solution.version,
dimension3: JSON.stringify(props),
scope: "action"
});
}
},
error: function (e) {
if (p.metrics.enable) {
if (!_instance) {
_instance = this;
}
track({
uid: hashString(document.location.origin),
url: document.location.origin,
e_c: 'Error',
e_a: e.name,
e_n: e.message,
e_v: ''
});
}
}
}
})();
export default metrics;
This diff is collapsed.
{
"name": "matomo-analytics",
"version": "1.0.2",
"version": "1.1.0",
"private": true,
"main": "lib/index.js",
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"build": "gulp bundle --ship && gulp package-solution --ship",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@microsoft/decorators": "1.8.1",
"@microsoft/sp-application-base": "^1.8.1",
"@microsoft/sp-core-library": "1.8.1",
"@microsoft/sp-dialog": "1.8.1",
"@types/es6-promise": "0.0.33",
"@types/webpack-env": "1.13.1",
"ajv-keywords": "^3.4.0"
"@microsoft/decorators": "~1.4.0",
"@microsoft/sp-application-base": "~1.4.0",
"@microsoft/sp-core-library": "~1.4.0"
},
"devDependencies": {
"@microsoft/rush-stack-compiler-2.7": "0.4.0",
"@microsoft/sp-build-web": "1.8.1",
"@microsoft/sp-module-interfaces": "1.8.1",
"@microsoft/sp-tslint-rules": "1.8.1",
"@microsoft/sp-webpart-workbench": "1.8.1",
"@microsoft/sp-build-web": "~1.4.1",
"@microsoft/sp-module-interfaces": "~1.4.1",
"@microsoft/sp-webpart-workbench": "~1.4.1",
"@types/chai": "3.4.34",
"@types/es6-promise": "0.0.33",
"@types/mocha": "2.2.38",
"ajv": "^5.2.5",
"@types/webpack-env": "1.13.1",
"ajv": "~5.2.2",
"gulp": "~3.9.1"
},
"metrics": {
"enable": true
}
}
......@@ -101,7 +101,7 @@ if [ "$tenantSolutionDeployment" = true ]; then
# First, find the Tenant App Catalog
msg "Retrieving tenant app catalog URL...\n"
appCatalogUrl=$(o365 spo tenant appcatalogurl get)
appCatalogUrl=$(m365 spo tenant appcatalogurl get)
if [ -z "$appCatalogUrl" ]; then
error "Couldn't retrieve Tenant App Catalog"
exit 1
......@@ -117,7 +117,7 @@ if [ "$tenantSolutionDeployment" = true ]; then
if [ "$verbose" = true ]; then
msg "Adding the Matomo Analytics Package to the Tenant AppCatalog...\n"
fi
appId=$(o365 spo app add --filePath ./matomo-analytics.sppkg)
appId=$(m365 spo app add --filePath ./matomo-analytics.sppkg)
if [ "$verbose" = true ]; then
msg "App ID: $appId...\n"
......@@ -130,7 +130,7 @@ if [ "$tenantSolutionDeployment" = true ]; then
if [ "$verbose" = true ]; then
msg "Deploying the Matomo Analytics Package...\n"
fi
o365 spo app deploy --name matomo-analytics.sppkg --skipFeatureDeployment
m365 spo app deploy --name matomo-analytics.sppkg --skipFeatureDeployment
checkPoint=200
fi
......@@ -139,7 +139,7 @@ if [ "$tenantSolutionDeployment" = true ]; then
if [ "$verbose" = true ]; then
msg "Installing the Matomo Analytics Package with ID: $appId...\n"
fi
o365 spo app install --id $appId --siteUrl $siteUrl
m365 spo app install --id $appId --siteUrl $siteUrl
checkPoint=300
fi
......@@ -150,12 +150,12 @@ else
if [ "$verbose" = true ]; then
msg "Adding the Matomo Analytics Package to the Site AppCatalog $siteUrl...\n"
fi
appId=$(o365 spo app add --filePath ./matomo-analytics.sppkg --scope sitecollection --appCatalogUrl $siteUrl)
appId=$(m365 spo app add --filePath ./matomo-analytics.sppkg --scope sitecollection --appCatalogUrl $siteUrl)
appId="${appId/UniqueId: /}"
if [ "$verbose" = true ]; then
msg "App ID: $appId...\n"
fi
checkPoint=100
fi
if (( $checkPoint < 200 )); then
......@@ -163,7 +163,7 @@ else
if [ "$verbose" = true ]; then
msg "Deploying the Matomo Analytics Package...\n"
fi
o365 spo app deploy --name matomo-analytics.sppkg --scope sitecollection --appCatalogUrl $siteUrl
m365 spo app deploy --name matomo-analytics.sppkg --scope sitecollection --appCatalogUrl $siteUrl
checkPoint=200
fi
......@@ -172,7 +172,7 @@ else
if [ "$verbose" = true ]; then
msg "Installing the Matomo Analytics Package with ID: $appId...\n"
fi
o365 spo app install --id $appId --siteUrl $siteUrl --scope sitecollection
m365 spo app install -s $siteUrl --id $appId --scope sitecollection
checkPoint=300
fi
......@@ -182,7 +182,7 @@ if [ "$skipCustomAction" = false ]; then
msg "Enabling the Matomo Analytics extension...\n"
# Build SPFx extension propreties
str="'{\"trackingUrl\":\"$trackingUrl\",\"trackingSiteId\":\"$trackingSiteId\""
str="{\"trackingUrl\":\"$trackingUrl\",\"trackingSiteId\":\"$trackingSiteId\""
if [ -n "$trackHeartBeatTimer" ]; then
str="$str,\"trackHeartBeatTimer\":\"$trackHeartBeatTimer\""
fi
......@@ -192,12 +192,12 @@ if [ "$skipCustomAction" = false ]; then
if [ -n "$cdnEndPoint" ]; then
str="$str,\"cdnEndPoint\":\"$cdnEndPoint\""
fi
str="$str}'"
str="$str}"
if [ "$verbose" = true ]; then
msg "SPFx properties: $str\n"
fi
# Add Custom action to site collection
o365 spo customaction add --url $siteUrl --clientSideComponentId a66f9d15-fcd2-4d37-8f8d-f76a4a7cae02 --name 'Matomo Analytics' --title 'Matomo Analytics' --location 'ClientSideExtension.ApplicationCustomizer' --scope Site -p $str
m365 spo customaction add --url $siteUrl --clientSideComponentId a66f9d15-fcd2-4d37-8f8d-f76a4a7cae02 --name 'Matomo Analytics' --title 'Matomo Analytics' --location 'ClientSideExtension.ApplicationCustomizer' --scope Site -p $str
fi
<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<!--<ClientSideComponentInstance
Title="Matomo Analytics for SharePoint"
<!--ClientSideComponentInstance
Title="Matomo"
Location="ClientSideExtension.ApplicationCustomizer"
ComponentId="a66f9d15-fcd2-4d37-8f8d-f76a4a7cae02"
Properties="{&quot;trackingUrl&quot;:&quot;//matomo.my-domain.com&quot;,&quot;trackingSiteId&quot;:&quot;2&quot;,&quot;trackHeartBeatTimer&quot;:&quot;true&quot;,&quot;trackUserId&quot;:&quot;false&quot;}">
</ClientSideComponentInstance>-->
Properties="{&quot;trackingUrl&quot;:&quot;//matomo.lsonline.fr&quot;,&quot;trackingSiteId&quot;:&quot;4&quot;,&quot;trackHeartBeatTimer&quot;:&quot;true&quot;,&quot;trackUserId&quot;:&quot;true&quot;}">
</ClientSideComponentInstance-->
</Elements>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<!--<CustomAction
Title="Matomo Analytics for SharePoint"
<!--CustomAction
Title="Matomo"
Location="ClientSideExtension.ApplicationCustomizer"
ClientSideComponentId="a66f9d15-fcd2-4d37-8f8d-f76a4a7cae02"
ClientSideComponentProperties="{&quot;trackingUrl&quot;:&quot;//matomo.my-domain.com&quot;,&quot;trackingSiteId&quot;:&quot;2&quot;,&quot;trackHeartBeatTimer&quot;:&quot;true&quot;,&quot;trackUserId&quot;:&quot;false&quot;}">
</CustomAction>-->
ClientSideComponentProperties="{&quot;trackingUrl&quot;:&quot;//matomo.lsonline.Fr&quot;,&quot;trackingSiteId&quot;:&quot;4&quot;,&quot;trackHeartBeatTimer&quot;:&quot;true&quot;,&quot;trackUserId&quot;:&quot;true&quot;}">
</CustomAction-->
</Elements>
\ No newline at end of file
import { override } from '@microsoft/decorators';
import { Log } from '@microsoft/sp-core-library';
import { BaseApplicationCustomizer } from '@microsoft/sp-application-base';
import metrics from './../../../metrics';
import * as strings from 'MatomoApplicationCustomizerStrings';
const LOG_SOURCE: string = 'MatomoApplicationCustomizer';
/**
* If your command set uses the ClientSideComponentProperties JSON input,
* it will be deserialized into the BaseExtension.properties object.
* You can define an interface to describe it.
* @interface
*/
export interface IMatomoApplicationCustomizerProperties {
trackingUrl: string;
trackingSiteId: string;
trackHeartBeatTimer: string;
trackUserId: string;
cdnEndPoint: string;
}
/** Deployement of Matomo Analytics through Custom Action
* @class
*/
export default class MatomoApplicationCustomizer extends BaseApplicationCustomizer<IMatomoApplicationCustomizerProperties> {
/** ID of the JavaScript script provided by Matomo + custom function for partial reload
* @private
*/
private initScriptId: string = 'matomo-trackerScript';
/** Current SharePoint page navigation
* @private
*/
private currentPage = "";
/** Statement for trigger ony once the initialization of analytics script
* @private
*/
private isInitialLoad = true;
/** Get Current page URL
* @private
*/
private getCurrentPage(): string {
return window.location.pathname + window.location.search;
}
/** Update current page navigation
* @private
*/
private updateCurrentPage(): void {
this.currentPage = this.getCurrentPage();
}
/** Navigation between SharePoint pages event
* @private
*/
private navigatedEvent(): void {
if (this.isInitialLoad) {
this.realInitialNavigatedEvent();
this.isInitialLoad = false;
}
}
/** Check that all mandatory properties are set
* @return False if one or all mandatory properties are not set
* @private
*/
private validate(): boolean {
let result: boolean = true;
if (!this.properties.trackingSiteId) {
Log.info(LOG_SOURCE, `${strings.MissingSiteID}`);
result = false;
}
if (!this.properties.trackingUrl) {
Log.info(LOG_SOURCE, `${strings.MissingServerUrl}`);
result = false;
}
return result;
}
/** Inital Page load - init analytics
* @param trackingSiteId
* @private
*/
private realInitialNavigatedEvent(): void {
Log.info(LOG_SOURCE, `Tracking full page load...`);
/* Code tracker provided by Matomo */
let codeTracker = `
var _paq = window._paq || [];
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);`;
codeTracker += this.getEnableHeartBeatTimer() ? `_paq.push(['enableHeartBeatTimer']);` : ``;
codeTracker += this.getTrackUserId() ? `_paq.push(['setUserId', '${this.context.pageContext.user.loginName}']);` : ``;
codeTracker += `_paq.push(['setTrackerUrl', '${this.properties.trackingUrl.replace(/\/\s*$/, '')}/matomo.php']);
_paq.push(['setSiteId', '${this.properties.trackingSiteId}']);
`;
var codeScript = document.createElement('script');
codeScript.type = 'text/javascript';
codeScript.id = this.initScriptId;
try {
codeScript.appendChild(document.createTextNode(codeTracker));
} catch (e) {
// IE has funky script nodes
codeScript.text = codeTracker;
}
document.head.appendChild(codeScript);
var mpaScript = document.createElement('script');
mpaScript.type = 'text/javascript';
mpaScript.src = `${this.properties.cdnEndPoint ? this.properties.cdnEndPoint.replace(/\/\s*$/, '') : this.properties.trackingUrl.replace(/\/\s*$/, '')}/matomo.js`;
mpaScript.async = true;
document.head.appendChild(mpaScript);
this.updateCurrentPage();
}
/** Partial Page load
* @private
*/
private partialEvent(): void {
Log.info(LOG_SOURCE, `Tracking partial page load...`);
if (!this.isInitialLoad && this.currentPage != this.getCurrentPage()) {
const _paq = window['_paq'] || [];
_paq.push(['setReferrerUrl', this.currentPage]);
_paq.push(['setCustomUrl', this.getCurrentPage()]);
_paq.push(['setDocumentTitle', document.title]);
_paq.push(['setGenerationTimeMs', 0]);
_paq.push(['setSiteId', this.properties.trackingSiteId]);
_paq.push(['trackPageView']);
this.updateCurrentPage();
}
}
/** Get if enable Heart Timer have to be enable
* @see https://developer.matomo.org/guides/tracking-javascript-guide#accurately-measure-the-time-spent-on-each-page
* @returns True by default and false if specified
*/
private getEnableHeartBeatTimer() {
if (this.properties.trackHeartBeatTimer !== undefined && this.properties.trackHeartBeatTimer != null) {
switch (this.properties.trackHeartBeatTimer) {
case '0':
return false;
case 'false':
return false;
default:
return true;
}
} else {
return true;
}
}
/** Get if the user ID have to be tracked
* @see https://developer.matomo.org/guides/tracking-javascript-guide#user-id
* @returns False by default and true if specified
*/
private getTrackUserId() {
if (this.properties.trackUserId !== undefined && this.properties.trackUserId != null) {
switch (this.properties.trackUserId) {
case '1':
return true;
case 'true':
return true;
default:
return false;
}
} else {
return false;
}
}
private getTelemetry() {
metrics.getTelemetry({
trackingUrl: (!(!this.properties.trackingUrl)).toString(),
trackingSiteId: (!(!this.properties.trackingSiteId)).toString(),
trackHeartBeatTimer: (!(!this.properties.trackHeartBeatTimer)).toString(),
trackUserId: (!(!this.properties.trackUserId)).toString(),
cdnEndPoint: (!(!this.properties.cdnEndPoint)).toString(),
});
}
@override
public onInit(): Promise<void> {
Log.info(LOG_SOURCE, `Initialized Matomo Analytics`);
this.getTelemetry();
if (this.validate()) {
/* This event is triggered when user performed a search from the header of SharePoint */
this.context.placeholderProvider.changedEvent.add(this, this.partialEvent);
/* This event is triggered when user navigate between the pages */
this.context.application.navigatedEvent.add(this, this.partialEvent);
this.navigatedEvent();
}
return Promise.resolve();
}
}
import { override } from '@microsoft/decorators';
import { Log } from '@microsoft/sp-core-library';
import {
BaseApplicationCustomizer
} from '@microsoft/sp-application-base';
import * as strings from 'MatomoApplicationCustomizerStrings';
const LOG_SOURCE: string = 'MatomoApplicationCustomizer';
/**
* If your command set uses the ClientSideComponentProperties JSON input,
* it will be deserialized into the BaseExtension.properties object.
* You can define an interface to describe it.
*/
export interface IMatomoApplicationCustomizerProperties {
trackingUrl: string;
trackingSiteId: string;
trackHeartBeatTimer: string;
trackUserId: string;
cdnEndPoint: string;
}
/** A Custom Action which can be run during execution of a Client Side Application */
export default class MatomoApplicationCustomizer
extends BaseApplicationCustomizer<IMatomoApplicationCustomizerProperties> {
/** ID of the JavaScript script provided by Matomo + custom function for partial reload