Skip to content

Commit

Permalink
Adding telemetry (#144)
Browse files Browse the repository at this point in the history
* Adding telemetry

* update telemetry endpoint

* now using a Set
  • Loading branch information
jonathanlurie authored Dec 13, 2024
1 parent e82f192 commit d9f12f1
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## NEXT
### New Features
- New `MaptilerProjectionControl` to toggle globe/Mercator projection
- Add metric collection and plugin registration feature

### Bug Fixes
- Navigation now relies on `Map` methods instead of `Transform` methods for bearing due to globe projection being available
Expand Down
17 changes: 17 additions & 0 deletions src/Map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import Minimap from "./Minimap";
import type { MinimapOptionsInput } from "./Minimap";
import { CACHE_API_AVAILABLE, registerLocalCacheProtocol } from "./caching";
import { MaptilerProjectionControl } from "./MaptilerProjectionControl";
import { Telemetry } from "./Telemetry";

export type LoadWithTerrainEvent = {
type: "loadWithTerrain";
Expand Down Expand Up @@ -204,6 +205,7 @@ export type MapOptions = Omit<MapOptionsML, "style" | "maplibreLogo"> & {
*/
// biome-ignore lint/suspicious/noShadowRestrictedNames: we want to keep consitency with MapLibre
export class Map extends maplibregl.Map {
public readonly telemetry: Telemetry;
private isTerrainEnabled = false;
private terrainExaggeration = 1;
private primaryLanguage: LanguageInfo;
Expand All @@ -219,6 +221,7 @@ export class Map extends maplibregl.Map {
private curentProjection: ProjectionTypes = undefined;
private originalLabelStyle = new window.Map<string, ExpressionSpecification | string>();
private isStyleLocalized = false;
private languageIsUpdated = false;

constructor(options: MapOptions) {
displayNoWebGlWarning(options.container);
Expand Down Expand Up @@ -690,6 +693,8 @@ export class Map extends maplibregl.Map {
this.fire("webglContextLost", { error: e });
});
});

this.telemetry = new Telemetry(this);
}

/**
Expand Down Expand Up @@ -1208,6 +1213,8 @@ export class Map extends maplibregl.Map {
this.setLayoutProperty(id, "text-field", newReplacer);
}
}

this.languageIsUpdated = true;
}

/**
Expand Down Expand Up @@ -1587,4 +1594,14 @@ export class Map extends maplibregl.Map {

this.curentProjection = "mercator";
}

/**
* Returns `true` is the language was ever updated, meaning changed
* from what is delivered in the style.
* Returns `false` if language in use is the language from the style
* and has never been changed.
*/
isLanguageUpdated(): boolean {
return this.languageIsUpdated;
}
}
91 changes: 91 additions & 0 deletions src/Telemetry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import type { Map as MapSDK } from "./Map";
import { config, MAPTILER_SESSION_ID } from "./config";
import { defaults } from "./defaults";
import packagejson from "../package.json";

/**
* A Telemetry instance sends some usage and merics to a dedicated endpoint at MapTiler Cloud.
*/
export class Telemetry {
private map: MapSDK;
private registeredModules = new Set<string>();

/**
*
* @param map : a Map instance
* @param delay : a delay in milliseconds after which the payload is sent to MapTiler cloud (cannot be less than 1000ms)
*/
constructor(map: MapSDK, delay: number = 2000) {
this.map = map;

setTimeout(
async () => {
if (!config.telemetry) {
return;
}
const endpointURL = this.preparePayload();

try {
const response = await fetch(endpointURL, { method: "POST" });
if (!response.ok) {
console.warn("The metrics could not be sent to MapTiler Cloud");
}
} catch (e) {
console.warn("The metrics could not be sent to MapTiler Cloud", e);
}
},
Math.max(1000, delay),
);
}

/**
* Register a module to the telemetry system of the SDK.
* The arguments `name` and `version` likely come from the package.json
* of each module.
*/
registerModule(name: string, version: string) {
// The telemetry is using a Set (and not an array) to avoid duplicates
// of same module + same version. Yet we want to track is the same module
// is being used with multiple version in a given project as this
// could be a source of incompatibility
this.registeredModules.add(`${name}:${version}`);
}

private preparePayload(): string {
const telemetryUrl = new URL(defaults.telemetryURL);

// Adding the version of the SDK
telemetryUrl.searchParams.append("sdk", packagejson.version);

// Adding the API key
telemetryUrl.searchParams.append("key", config.apiKey);

// Adding MapTiler Cloud session ID
telemetryUrl.searchParams.append("mtsid", MAPTILER_SESSION_ID);

// Is the app using session?
telemetryUrl.searchParams.append("session", config.session ? "1" : "0");

// Is the app using tile caching?
telemetryUrl.searchParams.append("caching", config.caching ? "1" : "0");

// Is the langauge updated from the original style?
telemetryUrl.searchParams.append("lang-updated", this.map.isLanguageUpdated() ? "1" : "0");

// Is terrain enabled?
telemetryUrl.searchParams.append("terrain", this.map.getTerrain() ? "1" : "0");

// Is globe enabled?
telemetryUrl.searchParams.append("globe", this.map.isGlobeProjection() ? "1" : "0");

// Adding the modules
// the list of modules are separated by a "|". For each module, a ":" is used to separate the name and the version:
// "@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"
// then the `.append()` function is in charge of URL-encoding the argument
if (this.registeredModules.size > 0) {
telemetryUrl.searchParams.append("modules", Array.from(this.registeredModules).join("|"));
}

return telemetryUrl.href;
}
}
19 changes: 19 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,25 @@ class SdkConfig extends EventEmitter {
*/
caching = true;

/**
* Telemetry is enabled by default but can be opted-out by setting this value to `false`.
* The telemetry is very valuable to the team at MapTiler because it shares information
* about where to add the extra effort. It also helps spotting some incompatibility issues
* that may arise between the SDK and a specific version of a module.
*
* It consists in sending metrics about usage of the following features:
* - SDK version [string]
* - API key [string]
* - MapTiler sesion ID (if opted-in) [string]
* - if tile caching is enabled [boolean]
* - if language specified at initialization [boolean]
* - if terrain is activated at initialization [boolean]
* - if globe projection is activated at initialization [boolean]
*
* In addition, each official module will be add added to a list, alongside its version number.
*/
telemetry = true;

/**
* Unit to be used
*/
Expand Down
1 change: 1 addition & 0 deletions src/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const defaults = {
maptilerLogoURL: "https://api.maptiler.com/resources/logo.svg",
maptilerURL: "https://www.maptiler.com/",
maptilerApiHost: "api.maptiler.com",
telemetryURL: "https://api.maptiler.com/metrics",
rtlPluginURL: "https://cdn.maptiler.com/mapbox-gl-rtl-text/v0.2.3/mapbox-gl-rtl-text.min.js",
primaryLanguage: Language.STYLE,
secondaryLanguage: Language.LOCAL,
Expand Down

0 comments on commit d9f12f1

Please sign in to comment.