@reservamos/browser-analytics 1.0.9 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reservamos/browser-analytics",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/reservamos/reservamos-browser-analytics.git"
@@ -88,6 +88,6 @@
88
88
  "upgradeps": "^2.0.6",
89
89
  "vite": "^5.0.12",
90
90
  "vitest": "^1.2.1",
91
- "@reservamos/js-api-client": "6.0.0-alpha.2"
91
+ "@reservamos/js-api-client": "6.0.0-alpha.5"
92
92
  }
93
93
  }
package/src/index.ts CHANGED
@@ -10,7 +10,10 @@ import type { SearchProps } from '@/events/search';
10
10
  import type { SeatChangeProps } from '@/events/seatChange';
11
11
  import type { ViewResultsProps } from '@/events/viewResults';
12
12
  import type { CreateAnonymousProfileProps } from '@/profiles/createAnonymousProfile';
13
+ import type { CreateRecommendedSeatsSchemaProps } from '@/recommendations/createRecommendedSeats';
14
+ import type { GetRecommendedSeatsSchemaProps } from '@/recommendations/getRecommendedSeats';
13
15
  import type { EventData, EventMetadata } from '@/types/eventData';
16
+ import type { GetRecommendedTripsProps } from '@/recommendations/getRecommendedTrips';
14
17
  import trackCustomEvent from '@/events/customEvent';
15
18
  import identify from '@/events/identify';
16
19
  import trackInterestInHome from '@/events/interestInHome';
@@ -19,14 +22,18 @@ import trackPassengersCreated from '@/events/passengersCreated';
19
22
  import trackPaymentAttempt from '@/events/paymentAttempt';
20
23
  import trackPickedDeparture from '@/events/pickedDeparture';
21
24
  import trackPurchaseAttempt from '@/events/purchaseAttempt';
25
+ import trackPurchaseCanceled from '@/events/purchaseCanceled';
22
26
  import trackSearch from '@/events/search';
23
27
  import trackSeatChange from '@/events/seatChange';
24
28
  import trackViewResults from '@/events/viewResults';
25
29
  import init, { isTrackerReady } from '@/init';
26
30
  import createAnonymousProfile from '@/profiles/createAnonymousProfile';
31
+ import createRecommendedSeats from '@/recommendations/createRecommendedSeats';
32
+ import getRecommendedPlaces from '@/recommendations/getRecommendedPlaces';
33
+ import getRecommendedSeats from '@/recommendations/getRecommendedSeats';
34
+ import getRecommendedTrips from '@/recommendations/getRecommendedTrips';
27
35
  import fingerprintService from '@/services/fingerprint';
28
36
  import mixpanelService from '@/services/mixpanel';
29
- import trackPurchaseCanceled from './events/purchaseCanceled/trackPurchaseCanceled.js';
30
37
 
31
38
  const analytics = {
32
39
  init,
@@ -39,6 +46,12 @@ const analytics = {
39
46
  profiles: {
40
47
  createAnonymousProfile,
41
48
  },
49
+ recommendations: {
50
+ getRecommendedPlaces,
51
+ createRecommendedSeats,
52
+ getRecommendedSeats,
53
+ getRecommendedTrips,
54
+ },
42
55
  track: {
43
56
  search: trackSearch,
44
57
  seatChange: trackSeatChange,
@@ -69,6 +82,9 @@ export type {
69
82
  EventMetadata,
70
83
  EventData,
71
84
  PurchaseCanceledProps,
85
+ GetRecommendedSeatsSchemaProps,
86
+ CreateRecommendedSeatsSchemaProps,
87
+ GetRecommendedTripsProps,
72
88
  };
73
89
 
74
90
  export default analytics;
@@ -0,0 +1,40 @@
1
+ import type {
2
+ RecommendedSeatsPayload,
3
+ RecommendedSeatsResponse,
4
+ } from '@reservamos/js-api-client/core';
5
+ import coreApi from '@reservamos/js-api-client/core';
6
+ import fingerprintService from '@/services/fingerprint';
7
+ import mixpanelService from '@/services/mixpanel';
8
+ import validatorService from '@/services/validator';
9
+ import CreateRecommendedSeatsSchema from './createRecommendedSeatsSchema';
10
+
11
+ /**
12
+ * Creates recommended seats by combining the provided payload with user identification data
13
+ * from Mixpanel and Fingerprint services.
14
+ * @throws {Error} When no distinct ID is found
15
+ * @throws {Error} When payload validation fails
16
+ * @throws {Error} When the API request fails
17
+ */
18
+ async function createRecommendedSeats(
19
+ payload: RecommendedSeatsPayload,
20
+ ): Promise<RecommendedSeatsResponse> {
21
+ try {
22
+ validatorService.validateProps(payload, CreateRecommendedSeatsSchema);
23
+ const distinctId = mixpanelService.getMixpanelDistinctId();
24
+ const userFingerprintId = fingerprintService.getCachedFingerprint();
25
+ if (!distinctId) {
26
+ throw new Error('No distinct ID found');
27
+ }
28
+
29
+ return await coreApi.recommendations.createRecommendedSeats({
30
+ ...payload,
31
+ distinct_id: distinctId,
32
+ device_fingerprint: userFingerprintId || '',
33
+ });
34
+ } catch (error) {
35
+ console.error('Could not create recommended seats:', error);
36
+ throw new Error(error instanceof Error ? error.message : String(error));
37
+ }
38
+ }
39
+
40
+ export default createRecommendedSeats;
@@ -0,0 +1,25 @@
1
+ import { z } from 'zod';
2
+
3
+ const SeatSchema = z.object({
4
+ category: z.string(),
5
+ number: z.string().optional(),
6
+ occupied: z.boolean().optional(),
7
+ adjacent_seats: z.null().optional(),
8
+ });
9
+
10
+ const BusSchemeSchema = z.object({
11
+ bus: z.array(z.array(z.array(SeatSchema))),
12
+ });
13
+
14
+ const CreateRecommendedSeatsSchema = z.object({
15
+ bus_type: z.string(),
16
+ selected_seats: z.string(),
17
+ bus_scheme: BusSchemeSchema,
18
+ });
19
+
20
+ type CreateRecommendedSeatsSchemaProps = z.infer<
21
+ typeof CreateRecommendedSeatsSchema
22
+ >;
23
+
24
+ export type { CreateRecommendedSeatsSchemaProps };
25
+ export default CreateRecommendedSeatsSchema;
@@ -0,0 +1,5 @@
1
+ import type { CreateRecommendedSeatsSchemaProps } from './createRecommendedSeatsSchema';
2
+ import createRecommendedSeats from './createRecommendedSeats';
3
+
4
+ export type { CreateRecommendedSeatsSchemaProps };
5
+ export default createRecommendedSeats;
@@ -0,0 +1,63 @@
1
+ import type { RecommendedPlacesResponse } from '@reservamos/js-api-client/core';
2
+ import coreApi from '@reservamos/js-api-client/core';
3
+ import mixpanelService from '@/services/mixpanel';
4
+
5
+ /**
6
+ * Polls the recommended places API until a final state is reached
7
+ * @throws Will reject the promise if the polling status returns 'failed'
8
+ * @private
9
+ */
10
+ const pollingRecommendedPlaces = async (
11
+ response: RecommendedPlacesResponse,
12
+ ) => {
13
+ return new Promise((resolve, reject) => {
14
+ const { state, polling_id } = response;
15
+ if (state === 'finished') {
16
+ resolve(response);
17
+ return;
18
+ }
19
+ coreApi.recommendations.pollRecommendedPlaces(polling_id, {
20
+ start: true,
21
+ watch: 'state',
22
+ expect: 'finished',
23
+ onEachResponse: (profile) => {
24
+ if (profile.status === 'finished') {
25
+ resolve(profile);
26
+ return;
27
+ }
28
+ if (profile.status === 'failed') {
29
+ reject(profile);
30
+ return;
31
+ }
32
+ },
33
+ });
34
+ });
35
+ };
36
+
37
+ /**
38
+ * Fetches personalized place recommendations for a user
39
+ * This function retrieves recommended places based on the user's Mixpanel distinct ID.
40
+ * It initiates the recommendation process and polls until the results are ready.
41
+ * @returns {Promise<RecommendedPlacesResponse>} A promise that resolves with the recommended places
42
+ * @throws {Error} When no Mixpanel distinct ID is found
43
+ * @throws {Error} When no response is received from the recommendations API
44
+ */
45
+ async function getRecommendedPlaces(): Promise<RecommendedPlacesResponse> {
46
+ const distinctId = mixpanelService.getMixpanelDistinctId();
47
+ if (!distinctId) {
48
+ throw new Error('No distinct ID found');
49
+ }
50
+
51
+ const response = await coreApi.recommendations.createRecommendedPlaces({
52
+ distinct_id: distinctId,
53
+ });
54
+
55
+ if (!response) {
56
+ throw new Error('No response received');
57
+ }
58
+
59
+ const profile = await pollingRecommendedPlaces(response);
60
+ return profile as RecommendedPlacesResponse;
61
+ }
62
+
63
+ export default getRecommendedPlaces;
@@ -0,0 +1,3 @@
1
+ import getRecommendedPlaces from './getRecommendedPlaces';
2
+
3
+ export default getRecommendedPlaces;
@@ -0,0 +1,37 @@
1
+ import type {
2
+ GetRecommendedSeatsPayload,
3
+ GetRecommendedSeatsResponse,
4
+ } from '@reservamos/js-api-client/core';
5
+ import coreApi from '@reservamos/js-api-client/core';
6
+ import mixpanelService from '@/services/mixpanel';
7
+ import validatorService from '@/services/validator';
8
+ import GetRecommendedSeatsSchema from './getRecommendedSeatsSchema';
9
+
10
+ /**
11
+ * Create recommended seats by combining the provided payload with data from
12
+ * user identification from Mixpanel and Fingerprint services.
13
+ * @throws {Error} When the payload fails schema validation
14
+ * @throws {Error} When no Mixpanel distinct ID is found
15
+ * @throws {Error} When the API request fails
16
+ */
17
+ async function createRecommendedSeats(
18
+ payload: GetRecommendedSeatsPayload,
19
+ ): Promise<GetRecommendedSeatsResponse> {
20
+ try {
21
+ validatorService.validateProps(payload, GetRecommendedSeatsSchema);
22
+ const distinctId = mixpanelService.getMixpanelDistinctId();
23
+ if (!distinctId) {
24
+ throw new Error('No distinct ID found');
25
+ }
26
+
27
+ return await coreApi.recommendations.getRecommendedSeats({
28
+ ...payload,
29
+ distinct_id: distinctId,
30
+ });
31
+ } catch (error) {
32
+ console.error('Could not create recommended seats:', error);
33
+ throw new Error(error instanceof Error ? error.message : String(error));
34
+ }
35
+ }
36
+
37
+ export default createRecommendedSeats;
@@ -0,0 +1,24 @@
1
+ import { z } from 'zod';
2
+
3
+ const SeatSchema = z.object({
4
+ category: z.string(),
5
+ number: z.string().optional(),
6
+ occupied: z.boolean().optional(),
7
+ adjacent_seats: z.null().optional(),
8
+ });
9
+
10
+ const BusSchemeSchema = z.object({
11
+ bus: z.array(z.array(z.array(SeatSchema))),
12
+ });
13
+
14
+ const GetRecommendedSeatsSchema = z.object({
15
+ bus_scheme: BusSchemeSchema,
16
+ total_seats: z.number()
17
+ });
18
+
19
+ type GetRecommendedSeatsSchemaProps = z.infer<
20
+ typeof GetRecommendedSeatsSchema
21
+ >;
22
+
23
+ export type { GetRecommendedSeatsSchemaProps };
24
+ export default GetRecommendedSeatsSchema;
@@ -0,0 +1,5 @@
1
+ import type { GetRecommendedSeatsSchemaProps } from './getRecommendedSeatsSchema';
2
+ import getRecommendedSeats from './getRecommendedSeats';
3
+
4
+ export type { GetRecommendedSeatsSchemaProps };
5
+ export default getRecommendedSeats;
@@ -0,0 +1,36 @@
1
+ import type {
2
+ RecommendedTripsResponse,
3
+ GetRecommendedTripsPayload
4
+ } from '@reservamos/js-api-client/core';
5
+ import coreApi from '@reservamos/js-api-client/core';
6
+ import fingerprintService from '@/services/fingerprint';
7
+ import mixpanelService from '@/services/mixpanel';
8
+ import validatorService from '@/services/validator';
9
+ import GetRecommendedTripsSchema from './getRecommendedTripsSchema';
10
+
11
+ /**
12
+ * Fetches recommended trips based on the provided search ID.
13
+ * @throws Error if user identifier is not loaded correctly or if the api request fails
14
+ */
15
+ async function getRecommendedTrips({
16
+ searchId,
17
+ }: GetRecommendedTripsPayload): Promise<RecommendedTripsResponse> {
18
+ try {
19
+ validatorService.validateProps({ searchId }, GetRecommendedTripsSchema);
20
+ const identifier =
21
+ fingerprintService.getCachedFingerprint() ||
22
+ mixpanelService.getMixpanelDistinctId();
23
+ if (!identifier) throw new Error('No identifier id');
24
+
25
+ const response = await coreApi.recommendations.getRecommendedTrips({
26
+ searchId,
27
+ userIdentifier: String(identifier),
28
+ });
29
+ return response;
30
+ } catch (error) {
31
+ console.error('Could not get recommended trips:', error);
32
+ throw new Error(error instanceof Error ? error.message : String(error));
33
+ }
34
+ }
35
+
36
+ export default getRecommendedTrips;
@@ -0,0 +1,10 @@
1
+ import { z } from 'zod';
2
+
3
+ const GetRecommendedTripsSchema = z.object({
4
+ searchId: z.number().min(1, 'SearchId is required'),
5
+ });
6
+
7
+ type GetRecommendedTripsProps = z.infer<typeof GetRecommendedTripsSchema>;
8
+
9
+ export type { GetRecommendedTripsProps };
10
+ export default GetRecommendedTripsSchema;
@@ -0,0 +1,5 @@
1
+ import type { GetRecommendedTripsProps } from './getRecommendedTripsSchema';
2
+ import getRecommendedTrips from './getRecommendedTrips';
3
+
4
+ export type { GetRecommendedTripsProps };
5
+ export default getRecommendedTrips;
@@ -9,23 +9,55 @@ interface Coordinates {
9
9
 
10
10
  // Add state to store coordinates
11
11
  let currentCoordinates: Coordinates | null = null;
12
+
13
+ // Constants for local storage keys and time limits
14
+ const GEOLOCATION_PERMISSION_KEY = 'geolocationPermissionTimestamp';
15
+ const SEVEN_DAYS_IN_MS = 7 * 24 * 60 * 60 * 1000;
16
+
17
+ /**
18
+ * Checks if the geolocation permission timestamp is still valid.
19
+ * @param {string | null} lastPermissionTimestamp - The last permission timestamp from local storage.
20
+ * @returns {boolean} True if the timestamp is valid, false otherwise.
21
+ */
22
+ function isPermissionTimestampValid(
23
+ lastPermissionTimestamp: string | null,
24
+ ): boolean {
25
+ if (!lastPermissionTimestamp) return false;
26
+ const now = Date.now();
27
+ return now - parseInt(lastPermissionTimestamp, 10) < SEVEN_DAYS_IN_MS;
28
+ }
29
+
30
+ /**
31
+ * Updates the geolocation permission timestamp in local storage.
32
+ * @param {number} timestamp - The current timestamp to store.
33
+ */
34
+ function updatePermissionTimestamp(timestamp: number): void {
35
+ localStorage.setItem(GEOLOCATION_PERMISSION_KEY, timestamp.toString());
36
+ }
37
+
12
38
  /**
13
39
  * Initializes geolocation service and retrieves user coordinates
14
40
  * @returns {Promise<Coordinates | null>} Promise resolving to coordinates or null if geolocation fails/is denied
15
41
  */
16
42
  function init(): Promise<Coordinates | null> {
17
43
  return new Promise((resolve) => {
18
- if (currentCoordinates) {
44
+ const lastPermissionTimestamp = localStorage.getItem(
45
+ GEOLOCATION_PERMISSION_KEY,
46
+ );
47
+
48
+ if (isPermissionTimestampValid(lastPermissionTimestamp)) {
19
49
  resolve(currentCoordinates);
20
50
  return;
21
51
  }
22
52
 
23
53
  const GEOLOCATION_TRACK_EVENT = 'Geolocation Requested';
54
+ const now = Date.now();
24
55
 
25
56
  navigator.geolocation.getCurrentPosition(
26
57
  (geolocation) => {
27
58
  const { latitude: lat, longitude: long } = geolocation.coords;
28
59
  currentCoordinates = { lat, long };
60
+ updatePermissionTimestamp(now);
29
61
  if (isTrackerReady()) {
30
62
  trackCustomEvent(GEOLOCATION_TRACK_EVENT, {
31
63
  geolocationAccepted: true,
@@ -34,6 +66,7 @@ function init(): Promise<Coordinates | null> {
34
66
  resolve(currentCoordinates);
35
67
  },
36
68
  () => {
69
+ updatePermissionTimestamp(now);
37
70
  if (isTrackerReady()) {
38
71
  trackCustomEvent(GEOLOCATION_TRACK_EVENT, {
39
72
  geolocationAccepted: false,