Skip to content

Commit d9f12f1

Browse files
Adding telemetry (#144)
* Adding telemetry * update telemetry endpoint * now using a Set
1 parent e82f192 commit d9f12f1

File tree

5 files changed

+129
-0
lines changed

5 files changed

+129
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## NEXT
44
### New Features
55
- New `MaptilerProjectionControl` to toggle globe/Mercator projection
6+
- Add metric collection and plugin registration feature
67

78
### Bug Fixes
89
- Navigation now relies on `Map` methods instead of `Transform` methods for bearing due to globe projection being available

src/Map.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import Minimap from "./Minimap";
4646
import type { MinimapOptionsInput } from "./Minimap";
4747
import { CACHE_API_AVAILABLE, registerLocalCacheProtocol } from "./caching";
4848
import { MaptilerProjectionControl } from "./MaptilerProjectionControl";
49+
import { Telemetry } from "./Telemetry";
4950

5051
export type LoadWithTerrainEvent = {
5152
type: "loadWithTerrain";
@@ -204,6 +205,7 @@ export type MapOptions = Omit<MapOptionsML, "style" | "maplibreLogo"> & {
204205
*/
205206
// biome-ignore lint/suspicious/noShadowRestrictedNames: we want to keep consitency with MapLibre
206207
export class Map extends maplibregl.Map {
208+
public readonly telemetry: Telemetry;
207209
private isTerrainEnabled = false;
208210
private terrainExaggeration = 1;
209211
private primaryLanguage: LanguageInfo;
@@ -219,6 +221,7 @@ export class Map extends maplibregl.Map {
219221
private curentProjection: ProjectionTypes = undefined;
220222
private originalLabelStyle = new window.Map<string, ExpressionSpecification | string>();
221223
private isStyleLocalized = false;
224+
private languageIsUpdated = false;
222225

223226
constructor(options: MapOptions) {
224227
displayNoWebGlWarning(options.container);
@@ -690,6 +693,8 @@ export class Map extends maplibregl.Map {
690693
this.fire("webglContextLost", { error: e });
691694
});
692695
});
696+
697+
this.telemetry = new Telemetry(this);
693698
}
694699

695700
/**
@@ -1208,6 +1213,8 @@ export class Map extends maplibregl.Map {
12081213
this.setLayoutProperty(id, "text-field", newReplacer);
12091214
}
12101215
}
1216+
1217+
this.languageIsUpdated = true;
12111218
}
12121219

12131220
/**
@@ -1587,4 +1594,14 @@ export class Map extends maplibregl.Map {
15871594

15881595
this.curentProjection = "mercator";
15891596
}
1597+
1598+
/**
1599+
* Returns `true` is the language was ever updated, meaning changed
1600+
* from what is delivered in the style.
1601+
* Returns `false` if language in use is the language from the style
1602+
* and has never been changed.
1603+
*/
1604+
isLanguageUpdated(): boolean {
1605+
return this.languageIsUpdated;
1606+
}
15901607
}

src/Telemetry.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import type { Map as MapSDK } from "./Map";
2+
import { config, MAPTILER_SESSION_ID } from "./config";
3+
import { defaults } from "./defaults";
4+
import packagejson from "../package.json";
5+
6+
/**
7+
* A Telemetry instance sends some usage and merics to a dedicated endpoint at MapTiler Cloud.
8+
*/
9+
export class Telemetry {
10+
private map: MapSDK;
11+
private registeredModules = new Set<string>();
12+
13+
/**
14+
*
15+
* @param map : a Map instance
16+
* @param delay : a delay in milliseconds after which the payload is sent to MapTiler cloud (cannot be less than 1000ms)
17+
*/
18+
constructor(map: MapSDK, delay: number = 2000) {
19+
this.map = map;
20+
21+
setTimeout(
22+
async () => {
23+
if (!config.telemetry) {
24+
return;
25+
}
26+
const endpointURL = this.preparePayload();
27+
28+
try {
29+
const response = await fetch(endpointURL, { method: "POST" });
30+
if (!response.ok) {
31+
console.warn("The metrics could not be sent to MapTiler Cloud");
32+
}
33+
} catch (e) {
34+
console.warn("The metrics could not be sent to MapTiler Cloud", e);
35+
}
36+
},
37+
Math.max(1000, delay),
38+
);
39+
}
40+
41+
/**
42+
* Register a module to the telemetry system of the SDK.
43+
* The arguments `name` and `version` likely come from the package.json
44+
* of each module.
45+
*/
46+
registerModule(name: string, version: string) {
47+
// The telemetry is using a Set (and not an array) to avoid duplicates
48+
// of same module + same version. Yet we want to track is the same module
49+
// is being used with multiple version in a given project as this
50+
// could be a source of incompatibility
51+
this.registeredModules.add(`${name}:${version}`);
52+
}
53+
54+
private preparePayload(): string {
55+
const telemetryUrl = new URL(defaults.telemetryURL);
56+
57+
// Adding the version of the SDK
58+
telemetryUrl.searchParams.append("sdk", packagejson.version);
59+
60+
// Adding the API key
61+
telemetryUrl.searchParams.append("key", config.apiKey);
62+
63+
// Adding MapTiler Cloud session ID
64+
telemetryUrl.searchParams.append("mtsid", MAPTILER_SESSION_ID);
65+
66+
// Is the app using session?
67+
telemetryUrl.searchParams.append("session", config.session ? "1" : "0");
68+
69+
// Is the app using tile caching?
70+
telemetryUrl.searchParams.append("caching", config.caching ? "1" : "0");
71+
72+
// Is the langauge updated from the original style?
73+
telemetryUrl.searchParams.append("lang-updated", this.map.isLanguageUpdated() ? "1" : "0");
74+
75+
// Is terrain enabled?
76+
telemetryUrl.searchParams.append("terrain", this.map.getTerrain() ? "1" : "0");
77+
78+
// Is globe enabled?
79+
telemetryUrl.searchParams.append("globe", this.map.isGlobeProjection() ? "1" : "0");
80+
81+
// Adding the modules
82+
// the list of modules are separated by a "|". For each module, a ":" is used to separate the name and the version:
83+
// "@maptiler/module-foo:1.1.0|@maptiler/module-bar:3.4.0|@maptiler/module-baz:9.0.3|@maptiler/module-quz:0.0.2-rc.1"
84+
// then the `.append()` function is in charge of URL-encoding the argument
85+
if (this.registeredModules.size > 0) {
86+
telemetryUrl.searchParams.append("modules", Array.from(this.registeredModules).join("|"));
87+
}
88+
89+
return telemetryUrl.href;
90+
}
91+
}

src/config.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,25 @@ class SdkConfig extends EventEmitter {
3838
*/
3939
caching = true;
4040

41+
/**
42+
* Telemetry is enabled by default but can be opted-out by setting this value to `false`.
43+
* The telemetry is very valuable to the team at MapTiler because it shares information
44+
* about where to add the extra effort. It also helps spotting some incompatibility issues
45+
* that may arise between the SDK and a specific version of a module.
46+
*
47+
* It consists in sending metrics about usage of the following features:
48+
* - SDK version [string]
49+
* - API key [string]
50+
* - MapTiler sesion ID (if opted-in) [string]
51+
* - if tile caching is enabled [boolean]
52+
* - if language specified at initialization [boolean]
53+
* - if terrain is activated at initialization [boolean]
54+
* - if globe projection is activated at initialization [boolean]
55+
*
56+
* In addition, each official module will be add added to a list, alongside its version number.
57+
*/
58+
telemetry = true;
59+
4160
/**
4261
* Unit to be used
4362
*/

src/defaults.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const defaults = {
77
maptilerLogoURL: "https://api.maptiler.com/resources/logo.svg",
88
maptilerURL: "https://www.maptiler.com/",
99
maptilerApiHost: "api.maptiler.com",
10+
telemetryURL: "https://api.maptiler.com/metrics",
1011
rtlPluginURL: "https://cdn.maptiler.com/mapbox-gl-rtl-text/v0.2.3/mapbox-gl-rtl-text.min.js",
1112
primaryLanguage: Language.STYLE,
1213
secondaryLanguage: Language.LOCAL,

0 commit comments

Comments
 (0)