Skip to content

Commit

Permalink
Merge pull request #1850 from rosahbruno/1862955-sessions
Browse files Browse the repository at this point in the history
Bug 1862955 - Gleanjs session implementation
  • Loading branch information
rosahbruno authored Feb 22, 2024
2 parents 702c810 + 04ccc1f commit 3027e1e
Show file tree
Hide file tree
Showing 11 changed files with 226 additions and 14 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
* [#1848](https://github.com/mozilla/glean.js/pull/1848): Support for automatically collecting element click events (first version)
* [#1849](https://github.com/mozilla/glean.js/pull/1849): Truncate event extra strings to 500 bytes. This also updates other string-based metrics to truncate based on max bytes rather than a set number of characters.

* [#1850](https://github.com/mozilla/glean.js/pull/1850): Automatically record basic session information (`session_id` & `session_count`) for web properties.

# v4.0.0-pre.2 (2023-12-06)

[Full changelog](https://github.com/mozilla/glean.js/compare/v4.0.0-pre.1...v4.0.0-pre.2)
Expand Down
3 changes: 2 additions & 1 deletion automation/compat/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
<title>Glean Benchmarks Sample</title>
</head>
<body>
<p id="msg"></p>
<p id="ping_msg"></p>
<p id="session_msg"></p>
<script src="./dist/index.js"></script>
</body>
</html>
46 changes: 36 additions & 10 deletions automation/compat/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,12 @@ import Glean from "@mozilla/glean/web";
import { benchmark } from "./generated/pings.js";
import * as metrics from "./generated/sample.js";

Glean.setSourceTags(["automation"]);
Glean.initialize("glean-compat-benchmark", true, {
enableAutoPageLoadEvents: true
});

metrics.pageLoaded.set();
benchmark.submit();

// !BIG HACK!
//
// Overwrite the console.info function in order to know when (and if) the benchmark ping was sent.
// If a success ping message is logged we show that in the document.
let pingSubmissionCount = 0;
let sessionId = "";
console.info = function () {
var message = "";
for (var i = 0; i < arguments.length; i++) {
Expand All @@ -30,6 +23,7 @@ console.info = function () {
message += " ";
}
}

console.log(message);
if (/successfully sent 200.$/.test(message)) {
pingSubmissionCount++;
Expand All @@ -38,8 +32,40 @@ console.info = function () {
// 1. The built-in page_load event, which submits an events ping.
// 2. The benchmark ping.
if (pingSubmissionCount == 2) {
var elem = document.getElementById("msg");
elem.innerHTML = "Pings submitted successfully.";
var elem = document.getElementById("ping_msg");
elem.innerText = "Pings submitted successfully.";
}
}

const sessionRegex = /"session_id": .+"/;
const sessionInfo = sessionRegex.exec(message);
if (!!sessionInfo) {
const currSessionId = sessionInfo?.[0].split(`"`)?.[3];
if (!!sessionId) {
if (currSessionId !== sessionId) {
var elem = document.getElementById("session_msg");
elem.innerText = "Session IDs updated successfully.";
} else {
console.log("Something went wrong...");
}
}

sessionId = currSessionId;
}
}

Glean.setSourceTags(["automation"]);

// This needs to be set so we can pull the session ID from the log messages.
Glean.setLogPings(true);

Glean.initialize("glean-compat-benchmark", true, {
enableAutoPageLoadEvents: true,
// Setting the override to 0 means every action will trigger
// a new session. We use this to check that the session ID
// changes whenever a session has expired.
sessionLengthInMinutesOverride: 0
});

metrics.pageLoaded.set();
benchmark.submit();
11 changes: 9 additions & 2 deletions automation/compat/tests/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export async function runWebTest(driver) {
// will receive the text "Ping submitted successfully."
await driver.get(`http://localhost:${PORT}/`);
// Give it time to send the ping request.
const successTextContainer = await driver.findElement(By.id("msg"));
const pingTextContainer = await driver.findElement(By.id("ping_msg"));

const areGleanWindowVarsSet = await driver.executeScript(() => {
// Verify that all Glean `window` vars are properly set.
Expand All @@ -100,10 +100,17 @@ export async function runWebTest(driver) {

await driver.wait(
until.elementTextIs(
successTextContainer,
pingTextContainer,
"Pings submitted successfully."
), 11_000); // 1s more than the default upload timeout in Glean.

const sessionTextContainer = await driver.findElement(By.id("session_msg"));
await driver.wait(
until.elementTextIs(
sessionTextContainer,
"Session IDs updated successfully."
), 1000);

console.log("Test passed.");
} catch(e) {
console.log("Test failed.", e);
Expand Down
4 changes: 4 additions & 0 deletions glean/src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ export interface ConfigurationInterface {
readonly enableAutoElementClickEvents?: boolean,
// Experimentation identifier to be set in all pings
experimentationId?: string,
// Allows custom session length, in minutes. The default value is 30 minutes.
readonly sessionLengthInMinutesOverride?: number,
}

// Important: the `Configuration` should only be used internally by the Glean singleton.
Expand All @@ -69,6 +71,7 @@ export class Configuration implements ConfigurationInterface {
readonly enableAutoPageLoadEvents?: boolean;
readonly enableAutoElementClickEvents?: boolean;
experimentationId?: string;
readonly sessionLengthInMinutesOverride?: number;

// Debug configuration.
debug: DebugOptions;
Expand All @@ -85,6 +88,7 @@ export class Configuration implements ConfigurationInterface {
this.enableAutoPageLoadEvents = config?.enableAutoPageLoadEvents;
this.enableAutoElementClickEvents = config?.enableAutoElementClickEvents;
this.experimentationId = config?.experimentationId;
this.sessionLengthInMinutesOverride = config?.sessionLengthInMinutesOverride;

this.debug = {};

Expand Down
74 changes: 74 additions & 0 deletions glean/src/core/internal_metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import { KNOWN_CLIENT_ID, CLIENT_INFO_STORAGE } from "./constants.js";
import { InternalUUIDMetricType as UUIDMetricType } from "./metrics/types/uuid.js";
import { InternalDatetimeMetricType as DatetimeMetricType } from "./metrics/types/datetime.js";
import { InternalStringMetricType as StringMetricType } from "./metrics/types/string.js";
import { InternalCounterMetricType as CounterMetricType } from "./metrics/types/counter.js";
import { createMetric } from "./metrics/utils.js";
import TimeUnit from "./metrics/time_unit.js";
import { generateUUIDv4, isWindowObjectUnavailable } from "./utils.js";
import { Lifetime } from "./metrics/lifetime.js";
import log, { LoggingLevel } from "./log.js";
import { Context } from "./context.js";
import { isSessionInactive } from "./sessions.js";

const LOG_TAG = "core.InternalMetrics";

Expand All @@ -28,6 +30,8 @@ export class CoreMetrics {
readonly osVersion: StringMetricType;
readonly architecture: StringMetricType;
readonly locale: StringMetricType;
readonly sessionId: UUIDMetricType;
readonly sessionCount: CounterMetricType;
// Provided by the user
readonly appChannel: StringMetricType;
readonly appBuild: StringMetricType;
Expand Down Expand Up @@ -120,6 +124,22 @@ export class CoreMetrics {
},
"second"
);

this.sessionId = new UUIDMetricType({
name: "session_id",
category: "",
sendInPings: ["glean_client_info"],
lifetime: Lifetime.User,
disabled: false
});

this.sessionCount = new CounterMetricType({
name: "session_count",
category: "",
sendInPings: ["glean_client_info"],
lifetime: Lifetime.User,
disabled: false
});
}

initialize(migrateFromLegacyStorage?: boolean): void {
Expand All @@ -145,6 +165,8 @@ export class CoreMetrics {
this.initializeUserLifetimeMetrics();
}

this.updateSessionInfo();

this.os.set(Context.platform.info.os());
this.osVersion.set(Context.platform.info.osVersion());
this.architecture.set(Context.platform.info.arch());
Expand All @@ -159,6 +181,53 @@ export class CoreMetrics {
}
}

/**
* Update local stored session information for Glean. This is called whenever
* the app on initialization and just after every read/write/delete to/from
* storage.
*
* There are a few scenarios to handle depending on what we already have
* stored about the session and how long it has been since the last action.
*
* SCENARIOS:
*
* 1. If this is the first session (there is no existing session ID),
* then we set a new session ID and a lastActive timestamp.
*
* 2. If the session is not expired, then we only update the lastActive time.
*
* 3. If the session is expired (inactive threshold is more recent than lastActive)
* then we update the session ID, the session sequence number, and the lastActive time.
*/
updateSessionInfo(): void {
if (isWindowObjectUnavailable()) {
return;
}

const existingSessionId = Context.metricsDatabase.getMetric(
CLIENT_INFO_STORAGE,
this.sessionId
);

if (existingSessionId) {
try {
// If the session has timed out, then we create a new session.
if (isSessionInactive(Context.config.sessionLengthInMinutesOverride)) {
this.generateNewSession();
}
} catch (e) {
// Error parsing the last active timestamp, create a new session.
this.generateNewSession();
}
} else {
// There is no previous session information, create a new session.
this.generateNewSession();
}

// Update the last-active timestamp in LocalStorage to the current time.
localStorage.setItem("glean_session_last_active", Date.now().toString());
}

/**
* Generates and sets the client_id if it is not set,
* or if the current value is corrupted.
Expand Down Expand Up @@ -202,6 +271,11 @@ export class CoreMetrics {
}
}

private generateNewSession(): void {
this.sessionId.generateAndSet();
this.sessionCount.add();
}

/**
* Initializes the Glean internal user-lifetime metrics.
*/
Expand Down
3 changes: 3 additions & 0 deletions glean/src/core/pings/maker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,9 @@ export function buildClientInfoSection(ping: CommonPingData): ClientInfo {

if (!ping.includeClientId) {
delete finalClientInfo["client_id"];

// If the ping doesn't include the client_id, we also should exclude session_id.
delete finalClientInfo["session_id"];
}

return finalClientInfo;
Expand Down
4 changes: 3 additions & 1 deletion glean/src/core/pings/ping_payload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ export interface ClientInfo extends JSONObject {
first_run_date?: string,
os?: string,
os_version?: string,
telemetry_sdk_build: string
telemetry_sdk_build: string,
session_id?: string,
session_count?: number
}

/**
Expand Down
22 changes: 22 additions & 0 deletions glean/src/core/sessions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/**
* Check if the current session is inactive - the default timeout is 30 minutes.
*
* @param sessionLengthInMinutes Length of the session in minutes - defaults to 30.
* @returns {boolean} If the current session is inactive.
*/
export function isSessionInactive(sessionLengthInMinutes = 30): boolean {
const lastActive = localStorage.getItem("glean_session_last_active");
const lastActiveDate = new Date(Number(lastActive));

// Subtract the session length from the current date.
const inactiveThreshold = new Date();
inactiveThreshold.setMinutes(inactiveThreshold.getMinutes() - sessionLengthInMinutes);

// If the inactiveThreshold is more recent than the lastActiveDate, then the
// current session is expired.
return inactiveThreshold > lastActiveDate;
}
45 changes: 45 additions & 0 deletions glean/src/metrics.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,51 @@ glean.internal.metrics:
- [email protected]
expires: never

session_id:
type: uuid
description: |
A UUID uniquely identifying the client's current session. A session is
the period of time in which a user interacts with the application. After
a period of inactivity (default being 30 minutes) a new session will be
created the next time the user interacts with the application. On each
new session, the session_id will be updated.
This metric WILL NOT be included for pings where `include_client_id` is `false`.
send_in_pings:
- glean_client_info
lifetime: user
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1862955
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1862955#c2
data_sensitivity:
- technical
notification_emails:
- [email protected]
expires: never

session_count:
type: counter
description: |
A running counter of the number of sessions for this client. A session is
the period of time in which a user interacts with the application. After
a period of inactivity (default being 30 minutes) a new session will be
created the next time the user interacts with the application. On each
new session, the session_count will be incremented.
This count will ONLY be reset on opt-out or whenever storage is deleted.
send_in_pings:
- glean_client_info
lifetime: user
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1862955
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1862955#c2
data_sensitivity:
- technical
notification_emails:
- [email protected]
expires: never

app_build:
type: string
lifetime: application
Expand Down
Loading

0 comments on commit 3027e1e

Please sign in to comment.