Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## XX.XX.XX
- Added the method `set_location` that sets the user's country code, city, GPS coordinates and IP address after init:
- When the session has already begun, or sessions are not tracked, the location is sent right away, otherwise with the session start
- Every later request carries it, also those that cluster workers hand to the main process

## 26.8.0
- ! Minor breaking change ! SDK behavior settings are now enabled by default. Changes made in SDK Manager on your server affect the SDK's behavior directly and take precedence over the matching init options:
- The SDK fetches the settings at start, every 4 hours by default and after a device ID change, also before consent is given, and stores them for the next start
Expand Down
59 changes: 59 additions & 0 deletions lib/countly.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ Countly.Bulk = Bulk;
var maxStackTraceLinesPerThread = 30;
var maxStackTraceLineLength = 200;
var deviceIdType = null;
var gpsCoordinates = null;
var heartBeatTimer = null;
var uncaughtExceptionHandler = null;
var unhandledRejectionHandler = null;
Expand Down Expand Up @@ -414,6 +415,7 @@ Countly.Bulk = Bulk;
maxStackTraceLinesPerThread = 30;
maxStackTraceLineLength = 200;
deviceIdType = null;
gpsCoordinates = null;
if (heartBeatTimer) {
clearInterval(heartBeatTimer);
heartBeatTimer = null;
Expand Down Expand Up @@ -777,6 +779,46 @@ Countly.Bulk = Bulk;
}
};

/**
* Set the user's location after init. The values replace the ones given at init, and every request from then on carries them.
* When the session has already begun, or sessions are not tracked, they are also sent right away in a request of their own;
* otherwise they are sent with begin_session. An empty value clears that part of the location.
* Needs the "location" consent, and is ignored while location tracking is turned off in the SDK behavior settings.
* @param {string=} country_code - ISO code of the user's country, for example "TR". Set it together with city
* @param {string=} city - name of the user's city. Set it together with country_code
* @param {string=} gps_coordinates - latitude and longitude separated by a comma, for example "56.42345,123.45325"
* @param {string=} ip_address - IP address of the user, for example "192.168.88.33"
* @example
* Countly.set_location("TR", "Izmir", "38.4237,27.1428", null);
*/
Countly.set_location = function(country_code, city, gps_coordinates, ip_address) {
cc.log(cc.logLevelEnums.INFO, `set_location, Setting the location. Country code: [${country_code}], city: [${city}], GPS coordinates: [${gps_coordinates}], IP address: [${ip_address}].`);
if (!Countly.check_consent("location")) {
cc.log(cc.logLevelEnums.WARNING, "set_location, No consent given for location. The location was not set.");
return;
}
if (!setting("lt")) {
cc.log(cc.logLevelEnums.INFO, "set_location, Location tracking is turned off in the SDK behavior settings. The location was not set.");
return;
}
var location = {
country_code: country_code || null,
city: city || null,
gps_coordinates: gps_coordinates || null,
ip_address: ip_address || null,
};
if ((location.country_code === null) !== (location.city === null)) {
cc.log(cc.logLevelEnums.WARNING, "set_location, Country code and city should be set together, the server may ignore one without the other.");
}
setLocationValues(location);
if (!ownsQueue()) {
process.send({ cly: { location: location } });
}
if (sessionStarted || !Countly.check_consent("sessions") || !setting("st")) {
toRequestQueue({});
}
};

/**
* Check and return the current device id type
* @returns {number} a number that indicates the device id type
Expand Down Expand Up @@ -1754,6 +1796,9 @@ Countly.Bulk = Bulk;
if (Countly.ip_address !== null) {
request.ip_address = Countly.ip_address;
}
if (gpsCoordinates) {
request.location = gpsCoordinates;
}
}
else {
request.location = "";
Expand All @@ -1766,6 +1811,17 @@ Countly.Bulk = Bulk;
request.tz = -date.getTimezoneOffset();
}

/**
* Store the location values that prepareRequest adds to every request
* @param {Object} location - country_code, city, gps_coordinates and ip_address, each a string or null
*/
function setLocationValues(location) {
Countly.country_code = location.country_code;
Countly.city = location.city;
Countly.ip_address = location.ip_address;
gpsCoordinates = location.gps_coordinates;
}

/**
* Add request to request queue
* @param {Object} request - object with request parameters
Expand Down Expand Up @@ -2757,6 +2813,9 @@ Countly.Bulk = Bulk;
else if (msg.cly.change_id) {
Countly.change_id(msg.cly.change_id, msg.cly.merge);
}
else if (msg.cly.location) {
setLocationValues(msg.cly.location);
}
else if (msg.cly.event) {
Countly.add_event(msg.cly.event);
}
Expand Down
5 changes: 4 additions & 1 deletion test/helpers/scripts/cluster_app.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const config = {
};

if (cluster.isMaster) {
if (mode === "primary_sdk" || mode === "worker_switch") {
if (mode === "primary_sdk" || mode === "worker_switch" || mode === "worker_location") {
config.device_id = "cluster-primary";
Countly.init(config);
}
Expand Down Expand Up @@ -40,6 +40,9 @@ else {
}
Countly.init(config);
Countly.begin_session();
if (mode === "worker_location") {
Countly.set_location("TR", "Izmir", "38.4237,27.1428", null);
}
Countly.add_event({ key: "worker_event" });
console.log(`worker device id: ${Countly.get_device_id()}`);
setInterval(() => { }, 1000);
Expand Down
15 changes: 15 additions & 0 deletions test/tests_cluster.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,19 @@ describe("Cluster tests", () => {
assert.equal(receivedEvents(result.requests).filter((e) => e.key === "worker_event").length, 0, "the worker's custom event was recorded although custom events are turned off");
assert.equal(result.requests.filter((r) => r.params.begin_session).length, 0, "the worker's session was recorded although sessions are turned off");
});

it("9- a location set in a worker is sent with the data the main process sends after it", async() => {
const result = await runCluster("worker_location");
const begin = result.requests.find((r) => r.params.begin_session === "1");
assert.ok(begin, "the worker's session did not reach the main process");
assert.equal(begin.params.city, undefined);
const located = result.requests.filter((r) => r.params.city === "Izmir");
assert.ok(located.length >= 2, "the location request and the event request do not both carry the location");
assert.ok(receivedEvents(located).some((e) => e.key === "worker_event"), "the worker's event was sent without the location");
located.forEach((r) => {
assert.equal(r.params.device_id, "cluster-primary");
assert.equal(r.params.country_code, "TR");
assert.equal(r.params.location, "38.4237,27.1428");
});
});
});
110 changes: 110 additions & 0 deletions test/tests_location.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
var assert = require("assert");
var Countly = require("../lib/countly");
var hp = require("./helpers/helper_functions");

/**
* Initializes the SDK with a fixed device ID
* @param {Object=} extra - init options to add
*/
function initMain(extra) {
Countly.init({
app_key: "YOUR_APP_KEY",
url: "https://try.count.ly",
device_id: "location-user",
max_events: -1,
...extra,
});
}

/**
* Returns the location parameters that a queued request carries
* @param {Object} request - request read from the request queue
* @returns {Object} the request's country_code, city, location and ip_address, without the ones it does not carry
*/
function locationOf(request) {
var result = {};
["country_code", "city", "location", "ip_address"].forEach((key) => {
if (key in request) {
result[key] = request[key];
}
});
return result;
}

describe("Location tests", () => {
beforeEach(async() => {
await hp.clearStorage();
});
it("1- after begin_session, sends the location right away and with every later request", (done) => {
initMain();
Countly.begin_session();
Countly.set_location("TR", "Izmir", "38.4237,27.1428", "10.0.0.1");
Countly.end_session(10);
setTimeout(() => {
var rq = hp.readRequestQueue();
assert.equal(rq.length, 3);
assert.equal(rq[0].begin_session, 1);
assert.deepStrictEqual(locationOf(rq[0]), {});
for (var i = 1; i < 3; i++) {
hp.requestBaseParamValidator(rq[i]);
assert.deepStrictEqual(locationOf(rq[i]), { country_code: "TR", city: "Izmir", location: "38.4237,27.1428", ip_address: "10.0.0.1" });
}
assert.equal(rq[1].begin_session, undefined);
assert.equal(rq[2].end_session, 1);
done();
}, hp.sWait);
});
it("2- before begin_session, sends the location with begin_session only", (done) => {
initMain();
Countly.set_location("TR", "Izmir", "38.4237,27.1428", null);
Countly.begin_session();
setTimeout(() => {
var rq = hp.readRequestQueue();
assert.equal(rq.length, 1);
assert.equal(rq[0].begin_session, 1);
assert.deepStrictEqual(locationOf(rq[0]), { country_code: "TR", city: "Izmir", location: "38.4237,27.1428" });
done();
}, hp.sWait);
});
it("3- replaces the location given at init, and empty values clear it", (done) => {
initMain({ country_code: "DE", city: "Berlin", ip_address: "10.0.0.2" });
Countly.begin_session();
Countly.set_location("TR", "Izmir");
Countly.set_location(null, "", undefined, null);
setTimeout(() => {
var rq = hp.readRequestQueue();
assert.equal(rq.length, 3);
assert.deepStrictEqual(locationOf(rq[0]), { country_code: "DE", city: "Berlin", ip_address: "10.0.0.2" });
assert.deepStrictEqual(locationOf(rq[1]), { country_code: "TR", city: "Izmir" });
assert.deepStrictEqual(locationOf(rq[2]), {});
done();
}, hp.sWait);
});
it("4- without the sessions consent, sends the location right away", (done) => {
initMain({ require_consent: true });
Countly.add_consent("location");
Countly.set_location("TR", "Izmir");
setTimeout(() => {
var located = hp.readRequestQueue().filter((r) => r.city === "Izmir");
assert.equal(located.length, 1);
assert.deepStrictEqual(locationOf(located[0]), { country_code: "TR", city: "Izmir" });
done();
}, hp.sWait);
});
it("5- without the location consent, the location is not set", (done) => {
initMain({ require_consent: true });
Countly.add_consent("sessions");
Countly.begin_session();
Countly.set_location("TR", "Izmir", "38.4237,27.1428", "10.0.0.1");
setTimeout(() => {
assert.equal(Countly.country_code, null);
assert.equal(Countly.city, null);
var rq = hp.readRequestQueue();
assert.ok(rq.length > 0);
rq.forEach((r) => {
assert.deepStrictEqual(locationOf(r), { location: "" });
});
done();
}, hp.sWait);
});
});
3 changes: 2 additions & 1 deletion test/tests_storage_safety.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ describe("Storage safety tests", () => {
const cwd = makeTempDir("default-location");
const run = await runScript("storage_location.js", { DEVICE_ID: "default-user" }, 20000, cwd);
const output = lastLine(run.stdout);
assert.equal(output.storagePath, path.join(cwd, ".countly-storage"));
// the script sees its working folder with symlinks resolved, /private/var instead of /var on macOS
assert.equal(output.storagePath, path.join(fs.realpathSync(cwd), ".countly-storage"));
assert.equal(readStoredFile(path.join(cwd, ".countly-storage", "__cly_id.json"), "cly_id"), "default-user");
assert.equal(fs.existsSync(path.join(oldDefaultDir, "__cly_id.json")), false);
});
Expand Down
Loading