Skip to content

Commit 250055d

Browse files
authored
Merge pull request #624 from OneSignal/feat-session-outcomes
Feature branch: new session logic + outcomes
2 parents d205a4b + 324e3fa commit 250055d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+2702
-503
lines changed

express_webpack/index.html

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,21 @@
1919
OneSignal.push(function() {
2020
OneSignal.init({appId});
2121
});
22+
23+
function sendOutcome() {
24+
OneSignal.push(function() {
25+
const outcomeName = document.querySelector('#outcome_name').value;
26+
OneSignal.sendOutcome(outcomeName);
27+
});
28+
}
29+
30+
function sendOutcomeWithWeight() {
31+
OneSignal.push(function() {
32+
const outcomeName = document.querySelector('#outcome_name').value;
33+
const outcomeWeight = parseFloat(document.querySelector('#outcome_weight').value);
34+
OneSignal.sendOutcome(outcomeName, outcomeWeight);
35+
});
36+
}
2237
</script>
2338
<head>
2439
<meta charset="utf-8">
@@ -28,5 +43,12 @@
2843
<body>
2944
<h1>OneSignal WebSDK Sandbox</h1>
3045
<p class="description">WebSDK Sandbox Environment</p>
46+
<input id="outcome_name" value="iryna-022020" />
47+
<button onclick="javascript:sendOutcome();">Send outcome</button>
48+
<br />
49+
<input id="outcome_weight" value=""/>
50+
<button onclick="javascript:sendOutcomeWithWeight();">Send outcome with weight</button>
51+
<br/>
52+
<div class='onesignal-customlink-container'></div>
3153
</body>
3254
</html>

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
"deepmerge": "^4.2.2",
6969
"dom-storage": "^2.0.2",
7070
"extract-text-webpack-plugin": "^4.0.0-beta.0",
71-
"fake-indexeddb": "github:OneSignal/fakeIndexedDB#onesignal",
71+
"fake-indexeddb": "^2.1.1",
7272
"imports-loader": "^0.7.1",
7373
"md5-file": "^3.2.2",
7474
"nock": "^9.1.6",
@@ -120,12 +120,12 @@
120120
},
121121
{
122122
"path": "./build/bundles/OneSignalPageSDKES6.js",
123-
"maxSize": "55 kB",
123+
"maxSize": "58 kB",
124124
"compression": "gzip"
125125
},
126126
{
127127
"path": "./build/bundles/OneSignalSDKWorker.js",
128-
"maxSize": "30 kB",
128+
"maxSize": "35 kB",
129129
"compression": "gzip"
130130
},
131131
{

src/OneSignal.ts

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import LegacyManager from './managers/LegacyManager';
1717
import SdkEnvironment from './managers/SdkEnvironment';
1818
import { AppConfig, AppUserConfig, AppUserConfigNotifyButton } from './models/AppConfig';
1919
import Context from './models/Context';
20-
import { Notification } from './models/Notification';
20+
import { Notification } from "./models/Notification";
2121
import { NotificationActionButton } from './models/NotificationActionButton';
2222
import { NotificationPermission } from './models/NotificationPermission';
2323
import { WindowEnvironmentKind } from './models/WindowEnvironmentKind';
@@ -53,6 +53,9 @@ import { ProcessOneSignalPushCalls } from "./utils/ProcessOneSignalPushCalls";
5353
import { AutoPromptOptions } from "./managers/PromptsManager";
5454
import { EnvironmentInfoHelper } from './context/browser/helpers/EnvironmentInfoHelper';
5555
import { EnvironmentInfo } from './context/browser/models/EnvironmentInfo';
56+
import { SessionManager } from './managers/sessionManager/page/SessionManager';
57+
import OutcomesHelper from "./helpers/shared/OutcomesHelper";
58+
import { OutcomeAttributionType } from "./models/Outcomes";
5659

5760
export default class OneSignal {
5861
/**
@@ -204,6 +207,11 @@ export default class OneSignal {
204207
const appConfig = await new ConfigManager().getAppConfig(options);
205208
Log.debug(`OneSignal: Final web app config: %c${JSON.stringify(appConfig, null, 4)}`, getConsoleStyle('code'));
206209

210+
// TODO: environmentInfo is explicitly dependent on existence of OneSignal.config. Needs refactor.
211+
// Workaround to temp assign config so that it can be used in context.
212+
OneSignal.config = appConfig;
213+
OneSignal.environmentInfo = EnvironmentInfoHelper.getEnvironmentInfo();
214+
207215
OneSignal.context = new Context(appConfig);
208216
OneSignal.config = OneSignal.context.appConfig;
209217
}
@@ -219,8 +227,6 @@ export default class OneSignal {
219227
InitHelper.errorIfInitAlreadyCalled();
220228
await OneSignal.initializeConfig(options);
221229

222-
OneSignal.environmentInfo = EnvironmentInfoHelper.getEnvironmentInfo();
223-
224230
if (!OneSignal.config) {
225231
throw new Error("OneSignal config not initialized!");
226232
}
@@ -272,6 +278,16 @@ export default class OneSignal {
272278
*/
273279
if (!OneSignal.config || !OneSignal.config.subdomain)
274280
throw new SdkInitError(SdkInitErrorKind.MissingSubdomain);
281+
282+
/**
283+
* We'll need to set up page activity tracking events on the main page but we can do so
284+
* only after the main initialization in the iframe is successful and a new session
285+
* is initiated.
286+
*/
287+
OneSignal.emitter.on(
288+
OneSignal.EVENTS.SESSION_STARTED, SessionManager.setupSessionEventListenersForHttp
289+
);
290+
275291
/**
276292
* The iFrame may never load (e.g. OneSignal might be down), in which
277293
* case the rest of the SDK's initialization will be blocked. This is a
@@ -453,7 +469,7 @@ export default class OneSignal {
453469
}
454470
// After the user subscribers, he will have a device ID, so get it again
455471
var { deviceId: newDeviceId } = await Database.getSubscription();
456-
await OneSignalApi.updatePlayer(appId, newDeviceId, {
472+
await OneSignalApi.updatePlayer(appId, newDeviceId!, {
457473
tags: tags
458474
});
459475
executeCallback(callback, tags);
@@ -781,6 +797,53 @@ export default class OneSignal {
781797
return this.emitter.once(event, listener);
782798
}
783799

800+
public static async sendOutcome(outcomeName: string, outcomeWeight?: number | undefined): Promise<void> {
801+
const outcomesConfig = OneSignal.config!.userConfig.outcomes;
802+
if (!outcomesConfig) {
803+
Log.debug("Outcomes feature not supported by main application yet.");
804+
return;
805+
}
806+
if (!outcomeName) {
807+
Log.error("Outcome name is required");
808+
return;
809+
}
810+
if (typeof outcomeWeight !== "undefined" && typeof outcomeWeight !== "number") {
811+
Log.error("Outcome weight can only be a number if present.");
812+
return;
813+
}
814+
// TODO: check built-in outcome names? not allow sending?
815+
816+
await awaitOneSignalInitAndSupported();
817+
818+
const isSubscribed = await OneSignal.privateIsPushNotificationsEnabled();
819+
if (!isSubscribed) {
820+
Log.warn("Reporting outcomes is supported only for subscribed users.");
821+
return;
822+
}
823+
824+
// TODO: add error handling
825+
const outcomeAttribution = await OutcomesHelper.getAttribution(outcomesConfig);
826+
switch (outcomeAttribution.type) {
827+
case OutcomeAttributionType.Direct:
828+
await OneSignal.context.updateManager.sendOutcomeDirect(
829+
OneSignal.config!.appId, outcomeAttribution.notificationIds, outcomeName, outcomeWeight
830+
);
831+
return;
832+
case OutcomeAttributionType.Indirect:
833+
await OneSignal.context.updateManager.sendOutcomeInfluenced(
834+
OneSignal.config!.appId, outcomeAttribution.notificationIds, outcomeName, outcomeWeight
835+
);
836+
return;
837+
case OutcomeAttributionType.Unattributed:
838+
await OneSignal.context.updateManager.sendOutcomeUnattributed(
839+
OneSignal.config!.appId, outcomeName, outcomeWeight);
840+
return;
841+
default:
842+
Log.warn("You are on a free plan. Please upgrade to use this functionality.");
843+
return;
844+
}
845+
}
846+
784847
static __doNotShowWelcomeNotification: boolean;
785848
static VERSION = Environment.version();
786849
static _VERSION = Environment.version();
@@ -825,6 +888,7 @@ export default class OneSignal {
825888
static proxyFrameHost: ProxyFrameHost;
826889
static proxyFrame: ProxyFrame;
827890
static emitter: Emitter = new Emitter();
891+
static cache: any = {};
828892

829893
/**
830894
* The additional path to the worker file.
@@ -874,6 +938,7 @@ export default class OneSignal {
874938
CONNECTED: 'connect',
875939
REMOTE_NOTIFICATION_PERMISSION: 'postmam.remoteNotificationPermission',
876940
REMOTE_DATABASE_GET: 'postmam.remoteDatabaseGet',
941+
REMOTE_DATABASE_GET_ALL: 'postmam.remoteDatabaseGetAll',
877942
REMOTE_DATABASE_PUT: 'postmam.remoteDatabasePut',
878943
REMOTE_DATABASE_REMOVE: 'postmam.remoteDatabaseRemove',
879944
REMOTE_OPERATION_COMPLETE: 'postman.operationComplete',
@@ -904,6 +969,10 @@ export default class OneSignal {
904969
SUBSCRIPTION_EXPIRATION_STATE: 'postmam.subscriptionExpirationState',
905970
PROCESS_EXPIRING_SUBSCRIPTIONS: 'postmam.processExpiringSubscriptions',
906971
GET_SUBSCRIPTION_STATE: 'postmam.getSubscriptionState',
972+
SESSION_UPSERT: 'postmam.sessionUpsert',
973+
SESSION_DEACTIVATE: 'postmam.sessionDeactivate',
974+
ARE_YOU_VISIBLE_REQUEST: 'postmam.areYouVisibleRequest',
975+
ARE_YOU_VISIBLE_RESPONSE: 'postmam.areYouVisibleResponse',
907976
};
908977

909978
static EVENTS = {
@@ -973,6 +1042,7 @@ export default class OneSignal {
9731042
TEST_INIT_OPTION_DISABLED: 'testInitOptionDisabled',
9741043
TEST_WOULD_DISPLAY: 'testWouldDisplay',
9751044
POPUP_WINDOW_TIMEOUT: 'popupWindowTimeout',
1045+
SESSION_STARTED: "os.sessionStarted",
9761046
};
9771047
}
9781048

src/OneSignalApiBase.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export class OneSignalApiBase {
3737
}
3838

3939
let callHeaders: any = new Headers();
40+
callHeaders.append("Origin", SdkEnvironment.getOrigin());
4041
callHeaders.append('SDK-Version', `onesignal/web/${Environment.version()}`);
4142
callHeaders.append('Content-Type', 'application/json;charset=UTF-8');
4243
if (headers) {
@@ -54,7 +55,7 @@ export class OneSignalApiBase {
5455
contents.body = JSON.stringify(data);
5556

5657
let status: number;
57-
return fetch(SdkEnvironment.getOneSignalApiUrl().toString() + '/' + action, contents)
58+
return fetch(SdkEnvironment.getOneSignalApiUrl(undefined, action).toString() + '/' + action, contents)
5859
.then(response => {
5960
status = response.status;
6061
return response.json();

src/OneSignalApiSW.ts

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { ServerAppConfig } from "./models/AppConfig";
22
import { OneSignalApiBase } from "./OneSignalApiBase";
33
import { SubscriptionStateKind } from "./models/SubscriptionStateKind";
4+
import { FlattenedDeviceRecord } from "./models/DeviceRecord";
45
import Log from "./libraries/Log";
56
import { Utils } from "./context/shared/utils/Utils";
7+
import { OutcomeAttribution, OutcomeAttributionType } from "./models/Outcomes";
68

79
export class OneSignalApiSW {
810
static async downloadServerAppConfig(appId: string): Promise<ServerAppConfig> {
@@ -36,10 +38,56 @@ export class OneSignalApiSW {
3638
});
3739
}
3840

39-
static updatePlayer(appId: string, playerId: string, options?: Object) {
40-
Utils.enforceAppId(appId);
41-
Utils.enforcePlayerId(playerId);
42-
return OneSignalApiBase.put(`players/${playerId}`, {app_id: appId, ...options});
41+
static async updatePlayer(appId: string, playerId: string, options?: Object): Promise<void> {
42+
const funcToExecute = async () => {
43+
await OneSignalApiBase.put(`players/${playerId}`, {app_id: appId, ...options});
44+
}
45+
return await Utils.enforceAppIdAndPlayerId(appId, playerId, funcToExecute);
46+
}
47+
48+
public static async updateUserSession(
49+
userId: string,
50+
serializedDeviceRecord: FlattenedDeviceRecord,
51+
): Promise<string> {
52+
const funcToExecute = async () => {
53+
const response = await OneSignalApiBase.post(
54+
`players/${userId}/on_session`, serializedDeviceRecord);
55+
if (response.id) {
56+
// A new user ID can be returned
57+
return response.id;
58+
} else {
59+
return userId;
60+
}
61+
};
62+
return await Utils.enforceAppIdAndPlayerId(serializedDeviceRecord.app_id, userId, funcToExecute);
63+
};
64+
65+
public static async sendSessionDuration(
66+
appId: string, deviceId: string, sessionDuration: number, deviceType: number, attribution: OutcomeAttribution
67+
): Promise<void> {
68+
const funcToExecute = async () => {
69+
const payload: any = {
70+
app_id: appId,
71+
type: 1,
72+
state: "ping",
73+
active_time: sessionDuration,
74+
device_type: deviceType,
75+
};
76+
switch (attribution.type) {
77+
case OutcomeAttributionType.Direct:
78+
payload.direct = true;
79+
payload.notification_ids = attribution.notificationIds;
80+
break;
81+
case OutcomeAttributionType.Indirect:
82+
payload.direct = false;
83+
payload.notification_ids = attribution.notificationIds;
84+
break;
85+
default:
86+
break;
87+
}
88+
await OneSignalApiBase.post(`players/${deviceId}/on_focus`, payload);
89+
}
90+
Utils.enforceAppIdAndPlayerId(appId, deviceId, funcToExecute);
4391
}
4492
}
4593

src/OneSignalApiShared.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import { DeviceRecord } from './models/DeviceRecord';
33
import { OneSignalApiErrorKind, OneSignalApiError } from './errors/OneSignalApiError';
44
import { EmailProfile } from './models/EmailProfile';
55
import { EmailDeviceRecord } from './models/EmailDeviceRecord';
6+
import { OutcomeRequestData } from "./models/OutcomeRequestData";
67
import OneSignalApiBase from "./OneSignalApiBase";
78
import Utils from "./context/shared/utils/Utils";
9+
import Log from "./libraries/Log";
810

911
export default class OneSignalApiShared {
1012
static getPlayer(appId: string, playerId: string) {
@@ -123,4 +125,12 @@ export default class OneSignalApiShared {
123125
} else throw e;
124126
}
125127
}
128+
129+
static async sendOutcome(data: OutcomeRequestData): Promise<void> {
130+
try {
131+
await OneSignalApiBase.post("outcomes/measure", data);
132+
} catch(e) {
133+
Log.error("sendOutcome", e);
134+
}
135+
}
126136
}

src/config/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const SERVER_CONFIG_DEFAULTS_SESSION = {
2+
reportingThreshold: 30,
3+
enableOnSessionForUnsubcribed: false,
4+
enableOnFocus: true,
5+
}

0 commit comments

Comments
 (0)