Skip to content

Commit a66415b

Browse files
committed
add custom events operation, executor, and controller
flesh out the track event operation and create custom logic add tests for custom events add subscription type for custom events
1 parent e60a7da commit a66415b

30 files changed

Lines changed: 451 additions & 169 deletions

__test__/support/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export const DUMMY_GET_USER_REQUEST_WITH_PUSH_SUB = {
4444
session_count: 1,
4545
sdk: __VERSION__,
4646
device_model: 'MacIntel',
47-
device_os: '114',
47+
device_os: 114,
4848
rooted: false,
4949
test_type: 0,
5050
app_version: '',

__test__/support/helpers/requests.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,3 +325,16 @@ export const setUpdateUserError = ({
325325
status,
326326
retryAfter,
327327
});
328+
329+
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
330+
// custom events
331+
const getCustomEventsUri = () =>
332+
`**/apps/${APP_ID}/integrations/sdk/custom_events`;
333+
export const sendCustomEventFn = vi.fn();
334+
export const setSendCustomEventResponse = () =>
335+
getHandler({
336+
uri: getCustomEventsUri(),
337+
method: 'post',
338+
status: 200,
339+
callback: sendCustomEventFn,
340+
});

src/core/CoreModule.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { logMethodCall } from 'src/shared/utils/utils';
2+
import { CustomEventController } from './controllers/CustomEventController';
3+
import { CustomEventsOperationExecutor } from './executors/CustomEventOperationExecutor';
24
import { IdentityOperationExecutor } from './executors/IdentityOperationExecutor';
35
import { LoginUserOperationExecutor } from './executors/LoginUserOperationExecutor';
46
import { RefreshUserOperationExecutor } from './executors/RefreshUserOperationExecutor';
@@ -26,6 +28,7 @@ export default class CoreModule {
2628
public subscriptionModelStore: SubscriptionModelStore;
2729
public identityModelStore: IdentityModelStore;
2830
public propertiesModelStore: PropertiesModelStore;
31+
public customEventController: CustomEventController;
2932

3033
private initPromise: Promise<void>;
3134

@@ -56,6 +59,11 @@ export default class CoreModule {
5659
this.operationModelStore,
5760
this.newRecordsState,
5861
);
62+
this.customEventController = new CustomEventController(
63+
this.identityModelStore,
64+
this.operationRepo,
65+
);
66+
5967
this.listeners = this.initializeListeners();
6068
this.initPromise = this.operationRepo.start();
6169
}
@@ -114,13 +122,15 @@ export default class CoreModule {
114122
this.rebuildUserService,
115123
this.newRecordsState,
116124
);
125+
const customEventOpExecutor = new CustomEventsOperationExecutor();
117126

118127
return [
119128
identityOpExecutor,
120129
loginOpExecutor,
121130
refreshOpExecutor,
122131
subscriptionOpExecutor,
123132
updateSubOpExecutor,
133+
customEventOpExecutor,
124134
];
125135
}
126136
}

src/core/CoreModuleDirector.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { PropertiesModelStore } from './modelStores/PropertiesModelStore';
1414
import { SubscriptionModelStore } from './modelStores/SubscriptionModelStore';
1515
import { NewRecordsState } from './operationRepo/NewRecordsState';
1616
import { type OperationRepo } from './operationRepo/OperationRepo';
17+
import { type ICustomEventController } from './types/customEvents';
1718
import { ModelChangeTags } from './types/models';
1819
import {
1920
SubscriptionChannel,
@@ -45,6 +46,10 @@ export class CoreModuleDirector {
4546
return this.core.subscriptionModelStore;
4647
}
4748

49+
get customEventController(): ICustomEventController {
50+
return this.core.customEventController;
51+
}
52+
4853
public generatePushSubscriptionModel(
4954
rawPushSubscription: RawPushSubscription,
5055
): SubscriptionModel {

src/core/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ export const OPERATION_NAME = {
1515
UPDATE_SUBSCRIPTION: 'update-subscription',
1616
DELETE_SUBSCRIPTION: 'delete-subscription',
1717
TRANSFER_SUBSCRIPTION: 'transfer-subscription',
18+
19+
// Custom Events Operations
20+
CUSTOM_EVENT: 'custom-event',
1821
} as const;
1922

2023
export const IdentityConstants = {
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import MainHelper from '../../shared/helpers/MainHelper';
2+
import { IdentityModelStore } from '../modelStores/IdentityModelStore';
3+
import { TrackEventOperation } from '../operations/TrackEventOperation';
4+
import { ICustomEvent, ICustomEventController } from '../types/customEvents';
5+
import { IOperationRepo } from '../types/operation';
6+
7+
/**
8+
* Implements custom event tracking functionality.
9+
* This class handles sending custom events by creating TrackEventOperation instances
10+
* and enqueueing them for execution via the operation repository.
11+
*/
12+
export class CustomEventController implements ICustomEventController {
13+
constructor(
14+
private readonly _identityModelStore: IdentityModelStore,
15+
private readonly _opRepo: IOperationRepo,
16+
) {}
17+
18+
sendCustomEvent(event: ICustomEvent): void {
19+
const appId = MainHelper.getAppId();
20+
const identityModel = this._identityModelStore.model;
21+
22+
const op = new TrackEventOperation({
23+
appId,
24+
onesignalId: identityModel.onesignalId,
25+
externalId: identityModel.externalId,
26+
timestamp: new Date().toISOString(),
27+
event,
28+
});
29+
30+
this._opRepo.enqueue(op);
31+
}
32+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import Environment from 'src/shared/helpers/Environment';
2+
import {
3+
getResponseStatusType,
4+
ResponseStatusType,
5+
} from 'src/shared/helpers/NetworkUtils';
6+
import Log from 'src/shared/libraries/Log';
7+
import { VERSION } from 'src/shared/utils/EnvVariables';
8+
import { OPERATION_NAME } from '../constants';
9+
import { ExecutionResponse } from '../operations/ExecutionResponse';
10+
import { Operation } from '../operations/Operation';
11+
import { TrackEventOperation } from '../operations/TrackEventOperation';
12+
import { RequestService } from '../requestService/RequestService';
13+
import { ICustomEventMetadata } from '../types/customEvents';
14+
import { ExecutionResult, IOperationExecutor } from '../types/operation';
15+
16+
// Implements logic similar to Android SDK's CustomEventOperationExecutor
17+
// Reference: https://github.com/OneSignal/OneSignal-Android-SDK/blob/main/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/CustomEventOperationExecutor.kt
18+
export class CustomEventsOperationExecutor implements IOperationExecutor {
19+
constructor() {}
20+
21+
get operations(): string[] {
22+
return [OPERATION_NAME.CUSTOM_EVENT];
23+
}
24+
25+
private get eventMetadata(): ICustomEventMetadata {
26+
return {
27+
sdk: VERSION,
28+
device_model: Environment.getDeviceModel(),
29+
device_os: Environment.getDeviceOS(),
30+
type: Environment.getSubscriptionType(),
31+
};
32+
}
33+
34+
async execute(operations: Operation[]): Promise<ExecutionResponse> {
35+
Log.debug(
36+
`CustomEventsOperationExecutor(operations: ${JSON.stringify(
37+
operations,
38+
)})`,
39+
);
40+
41+
// TODO: each trackEvent is sent individually right now; may need to batch in the future
42+
const operation = operations[0];
43+
44+
if (!(operation instanceof TrackEventOperation)) {
45+
throw new Error(
46+
`Unrecognized operation! Expected TrackEventOperation, got: ${operation.constructor.name}`,
47+
);
48+
}
49+
50+
const response = await RequestService.sendCustomEvent(
51+
{ appId: operation.appId },
52+
{
53+
name: operation.event.name,
54+
onesignal_id: operation.onesignalId,
55+
external_id: operation.externalId,
56+
timestamp: operation.timestamp,
57+
payload: {
58+
...(operation.event.properties ?? {}),
59+
os_sdk: this.eventMetadata,
60+
},
61+
},
62+
);
63+
64+
const { ok, status } = response;
65+
const responseType = getResponseStatusType(status);
66+
67+
if (ok) return new ExecutionResponse(ExecutionResult.SUCCESS);
68+
69+
switch (responseType) {
70+
case ResponseStatusType.RETRYABLE:
71+
return new ExecutionResponse(ExecutionResult.FAIL_RETRY);
72+
default:
73+
return new ExecutionResponse(ExecutionResult.FAIL_NORETRY);
74+
}
75+
}
76+
}

src/core/executors/RefreshUserOperationExecutor.test.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,6 @@ describe('RefreshUserOperationExecutor', () => {
161161
enabled: false,
162162
token: 'test@example.com',
163163
type: SubscriptionType.Email,
164-
device_os: '',
165-
device_model: '',
166-
sdk: '',
167164
});
168165
});
169166

src/core/executors/RefreshUserOperationExecutor.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,9 @@ export class RefreshUserOperationExecutor implements IOperationExecutor {
106106
model.type = sub.type;
107107
model.enabled =
108108
model.notification_types !== NotificationType.UserOptedOut;
109-
model.sdk = sub.sdk ?? '';
110-
model.device_os = sub.device_os ?? '';
111-
model.device_model = sub.device_model ?? '';
109+
model.sdk = sub.sdk;
110+
model.device_os = sub.device_os;
111+
model.device_model = sub.device_model;
112112
model.onesignalId = op.onesignalId;
113113

114114
// We only add a non-push subscriptions. For push, the device is the source of truth

src/core/listeners/types.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type { Model } from '../models/Model';
2-
import type { IEventNotifier } from '../types/events';
3-
import type { ISingletonModelStoreChangeHandler } from '../types/models';
2+
import type {
3+
IEventNotifier,
4+
ISingletonModelStoreChangeHandler,
5+
} from '../types/models';
46

57
export interface ISingletonModelStore<TModel extends Model>
68
extends IEventNotifier<ISingletonModelStoreChangeHandler<TModel>> {

0 commit comments

Comments
 (0)