stream-chat 4.3.0 → 4.4.2

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/src/client.ts CHANGED
@@ -118,6 +118,7 @@ import {
118
118
  DeleteChannelsResponse,
119
119
  TaskResponse,
120
120
  } from './types';
121
+ import { InsightTypes, InsightMetrics } from './insights';
121
122
 
122
123
  function isString(x: unknown): x is string {
123
124
  return typeof x === 'string' || x instanceof String;
@@ -197,6 +198,7 @@ export class StreamChat<
197
198
  wsConnection: StableWSConnection<ChannelType, CommandType, UserType> | null;
198
199
  wsPromise: ConnectAPIResponse<ChannelType, CommandType, UserType> | null;
199
200
  consecutiveFailures: number;
201
+ insightMetrics: InsightMetrics;
200
202
 
201
203
  /**
202
204
  * Initialize a client
@@ -291,6 +293,7 @@ export class StreamChat<
291
293
  // generated from secret.
292
294
  this.tokenManager = new TokenManager(this.secret);
293
295
  this.consecutiveFailures = 0;
296
+ this.insightMetrics = new InsightMetrics();
294
297
 
295
298
  /**
296
299
  * logger function should accept 3 parameters:
@@ -1617,6 +1620,7 @@ export class StreamChat<
1617
1620
  }
1618
1621
 
1619
1622
  // The StableWSConnection handles all the reconnection logic.
1623
+
1620
1624
  this.wsConnection = new StableWSConnection<ChannelType, CommandType, UserType>({
1621
1625
  wsBaseURL: client.wsBaseURL,
1622
1626
  clientID: client.clientID,
@@ -1631,6 +1635,8 @@ export class StreamChat<
1631
1635
  eventCallback: this.dispatchEvent as (event: ConnectionChangeEvent) => void,
1632
1636
  logger: this.logger,
1633
1637
  device: this.options.device,
1638
+ postInsights: this.options.enableInsights ? this.postInsights : undefined,
1639
+ insightMetrics: this.insightMetrics,
1634
1640
  });
1635
1641
 
1636
1642
  let warmUpPromise;
@@ -3343,6 +3349,27 @@ export class StreamChat<
3343
3349
  );
3344
3350
  }
3345
3351
 
3352
+ postInsights = async (insightType: InsightTypes, insights: Record<string, unknown>) => {
3353
+ const maxAttempts = 3;
3354
+ for (let i = 0; i < maxAttempts; i++) {
3355
+ try {
3356
+ await this.axiosInstance.post(
3357
+ `https://insights.stream-io-api.com/insights/${insightType}`,
3358
+ insights,
3359
+ );
3360
+ } catch (e) {
3361
+ this.logger('warn', `failed to send insights event ${insightType}`, {
3362
+ tags: ['insights', 'connection'],
3363
+ error: e,
3364
+ insights,
3365
+ });
3366
+ await sleep((i + 1) * 3000);
3367
+ continue;
3368
+ }
3369
+ break;
3370
+ }
3371
+ };
3372
+
3346
3373
  /**
3347
3374
  * deleteUsers - Batch Delete Users
3348
3375
  *
package/src/connection.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  import WebSocket from 'isomorphic-ws';
2
- import { chatCodes, sleep, retryInterval } from './utils';
2
+ import { chatCodes, sleep, retryInterval, randomId } from './utils';
3
3
  import { TokenManager } from './token_manager';
4
+ import {
5
+ buildWsFatalInsight,
6
+ buildWsSuccessAfterFailureInsight,
7
+ InsightMetrics,
8
+ InsightTypes,
9
+ } from './insights';
4
10
  import {
5
11
  BaseDeviceFields,
6
12
  ConnectAPIResponse,
@@ -30,6 +36,7 @@ type Constructor<
30
36
  authType: 'anonymous' | 'jwt';
31
37
  clientID: string;
32
38
  eventCallback: (event: ConnectionChangeEvent) => void;
39
+ insightMetrics: InsightMetrics;
33
40
  logger: Logger | (() => void);
34
41
  messageCallback: (messageEvent: WebSocket.MessageEvent) => void;
35
42
  recoverCallback: (
@@ -41,6 +48,7 @@ type Constructor<
41
48
  userID: string;
42
49
  wsBaseURL: string;
43
50
  device?: BaseDeviceFields;
51
+ postInsights?: (eventType: InsightTypes, event: Record<string, unknown>) => void;
44
52
  };
45
53
 
46
54
  /**
@@ -78,7 +86,6 @@ export class StableWSConnection<
78
86
  userID: Constructor<ChannelType, CommandType, UserType>['userID'];
79
87
  wsBaseURL: Constructor<ChannelType, CommandType, UserType>['wsBaseURL'];
80
88
  device: Constructor<ChannelType, CommandType, UserType>['device'];
81
-
82
89
  connectionID?: string;
83
90
  connectionOpen?: ConnectAPIResponse<ChannelType, CommandType, UserType>;
84
91
  consecutiveFailures: number;
@@ -97,11 +104,14 @@ export class StableWSConnection<
97
104
  StatusCode?: string | number;
98
105
  },
99
106
  ) => void;
107
+ requestID: string | undefined;
108
+ connectionStartTimestamp: number | undefined;
100
109
  resolvePromise?: (value: WebSocket.MessageEvent) => void;
101
110
  totalFailures: number;
102
111
  ws?: WebSocket;
103
112
  wsID: number;
104
-
113
+ postInsights?: Constructor<ChannelType, CommandType, UserType>['postInsights'];
114
+ insightMetrics: InsightMetrics;
105
115
  constructor({
106
116
  apiKey,
107
117
  authType,
@@ -116,6 +126,8 @@ export class StableWSConnection<
116
126
  userID,
117
127
  wsBaseURL,
118
128
  device,
129
+ postInsights,
130
+ insightMetrics,
119
131
  }: Constructor<ChannelType, CommandType, UserType>) {
120
132
  this.wsBaseURL = wsBaseURL;
121
133
  this.clientID = clientID;
@@ -149,6 +161,8 @@ export class StableWSConnection<
149
161
  this.pingInterval = 25 * 1000;
150
162
  this.connectionCheckTimeout = this.pingInterval + 10 * 1000;
151
163
  this._listenForConnectionChanges();
164
+ this.postInsights = postInsights;
165
+ this.insightMetrics = insightMetrics;
152
166
  }
153
167
 
154
168
  /**
@@ -244,13 +258,19 @@ export class StableWSConnection<
244
258
  ]);
245
259
  }
246
260
 
247
- _buildUrl = () => {
261
+ /**
262
+ * Builds and returns the url for websocket.
263
+ * @param reqID Unique identifier generated on client side, to help tracking apis on backend.
264
+ * @returns url string
265
+ */
266
+ _buildUrl = (reqID?: string) => {
248
267
  const params = {
249
268
  user_id: this.user.id,
250
269
  user_details: this.user,
251
270
  user_token: this.tokenManager.getToken(),
252
271
  server_determines_connection_id: true,
253
272
  device: this.device,
273
+ request_id: reqID,
254
274
  };
255
275
  const qs = encodeURIComponent(JSON.stringify(params));
256
276
  const token = this.tokenManager.getToken();
@@ -352,11 +372,12 @@ export class StableWSConnection<
352
372
  async _connect() {
353
373
  if (this.isConnecting) return; // simply ignore _connect if it's currently trying to connect
354
374
  this.isConnecting = true;
355
-
375
+ this.requestID = randomId();
376
+ this.insightMetrics.connectionStartTimestamp = new Date().getTime();
356
377
  try {
357
378
  await this.tokenManager.tokenReady();
358
379
  this._setupConnectionPromise();
359
- const wsURL = this._buildUrl();
380
+ const wsURL = this._buildUrl(this.requestID);
360
381
  this.ws = new WebSocket(wsURL);
361
382
  this.ws.onopen = this.onopen.bind(this, this.wsID);
362
383
  this.ws.onclose = this.onclose.bind(this, this.wsID);
@@ -367,6 +388,13 @@ export class StableWSConnection<
367
388
 
368
389
  if (response) {
369
390
  this.connectionID = response.connection_id;
391
+ if (this.insightMetrics.wsConsecutiveFailures > 0) {
392
+ this.postInsights?.(
393
+ 'ws_success_after_failure',
394
+ buildWsSuccessAfterFailureInsight(this),
395
+ );
396
+ this.insightMetrics.wsConsecutiveFailures = 0;
397
+ }
370
398
  return response;
371
399
  }
372
400
  } catch (err) {
@@ -559,6 +587,12 @@ export class StableWSConnection<
559
587
  };
560
588
 
561
589
  onclose = (wsID: number, event: WebSocket.CloseEvent) => {
590
+ if (event.code !== chatCodes.WS_CLOSED_SUCCESS) {
591
+ this.insightMetrics.wsConsecutiveFailures++;
592
+ this.insightMetrics.wsTotalFailures++;
593
+ this.postInsights?.('ws_fatal', buildWsFatalInsight(this, event));
594
+ }
595
+
562
596
  this.logger('info', 'connection:onclose() - onclose callback - ' + event.code, {
563
597
  tags: ['connection'],
564
598
  event,
@@ -791,7 +825,6 @@ export class StableWSConnection<
791
825
  {
792
826
  type: 'health.check',
793
827
  client_id: this.clientID,
794
- user_id: this.userID,
795
828
  },
796
829
  ];
797
830
  // try to send on the connection
package/src/index.ts CHANGED
@@ -8,5 +8,6 @@ export * from './events';
8
8
  export * from './permissions';
9
9
  export * from './signing';
10
10
  export * from './token_manager';
11
+ export * from './insights';
11
12
  export * from './types';
12
13
  export { isOwnUser, chatCodes, logChatPromiseExecution } from './utils';
@@ -0,0 +1,72 @@
1
+ import { StableWSConnection } from './connection';
2
+ import WebSocket from 'isomorphic-ws';
3
+ import { LiteralStringForUnion, UnknownType } from './types';
4
+ import { randomId } from './utils';
5
+
6
+ export type InsightTypes = 'ws_fatal' | 'ws_success_after_failure';
7
+ export class InsightMetrics {
8
+ connectionStartTimestamp: number | null;
9
+ wsConsecutiveFailures: number;
10
+ wsTotalFailures: number;
11
+ instanceClientId: string;
12
+
13
+ constructor() {
14
+ this.connectionStartTimestamp = null;
15
+ this.wsTotalFailures = 0;
16
+ this.wsConsecutiveFailures = 0;
17
+ this.instanceClientId = randomId();
18
+ }
19
+ }
20
+
21
+ export function buildWsFatalInsight<
22
+ ChannelType extends UnknownType = UnknownType,
23
+ CommandType extends string = LiteralStringForUnion,
24
+ UserType extends UnknownType = UnknownType
25
+ >(
26
+ connection: StableWSConnection<ChannelType, CommandType, UserType>,
27
+ event: WebSocket.CloseEvent,
28
+ ) {
29
+ return {
30
+ err: {
31
+ wasClean: event.wasClean,
32
+ code: event.code,
33
+ reason: event.reason,
34
+ },
35
+ ...buildWsBaseInsight(connection),
36
+ };
37
+ }
38
+
39
+ function buildWsBaseInsight<
40
+ ChannelType extends UnknownType = UnknownType,
41
+ CommandType extends string = LiteralStringForUnion,
42
+ UserType extends UnknownType = UnknownType
43
+ >(connection: StableWSConnection<ChannelType, CommandType, UserType>) {
44
+ return {
45
+ ready_state: connection.ws?.readyState,
46
+ url: connection._buildUrl(connection.requestID),
47
+ api_key: connection.apiKey,
48
+ start_ts: connection.insightMetrics.connectionStartTimestamp,
49
+ end_ts: new Date().getTime(),
50
+ auth_type: connection.authType,
51
+ token: connection.tokenManager.token,
52
+ user_id: connection.userID,
53
+ user_details: connection.user,
54
+ device: connection.device,
55
+ client_id: connection.connectionID,
56
+ ws_details: connection.ws,
57
+ ws_consecutive_failures: connection.insightMetrics.wsConsecutiveFailures,
58
+ ws_total_failures: connection.insightMetrics.wsTotalFailures,
59
+ request_id: connection.requestID,
60
+ online: typeof navigator !== 'undefined' ? navigator?.onLine : null,
61
+ user_agent: typeof navigator !== 'undefined' ? navigator?.userAgent : null,
62
+ instance_client_id: connection.insightMetrics.instanceClientId,
63
+ };
64
+ }
65
+
66
+ export function buildWsSuccessAfterFailureInsight<
67
+ ChannelType extends UnknownType = UnknownType,
68
+ CommandType extends string = LiteralStringForUnion,
69
+ UserType extends UnknownType = UnknownType
70
+ >(connection: StableWSConnection<ChannelType, CommandType, UserType>) {
71
+ return buildWsBaseInsight(connection);
72
+ }
package/src/types.ts CHANGED
@@ -69,6 +69,7 @@ export type AppSettingsAPIResponse<
69
69
  mutes?: boolean;
70
70
  name?: string;
71
71
  push_notifications?: boolean;
72
+ quotes?: boolean;
72
73
  reactions?: boolean;
73
74
  read_events?: boolean;
74
75
  replies?: boolean;
@@ -859,6 +860,7 @@ export type CreateChannelOptions<CommandType extends string = LiteralStringForUn
859
860
  name?: string;
860
861
  permissions?: PermissionObject[];
861
862
  push_notifications?: boolean;
863
+ quotes?: boolean;
862
864
  reactions?: boolean;
863
865
  read_events?: boolean;
864
866
  replies?: boolean;
@@ -995,6 +997,7 @@ export type StreamChatOptions = AxiosRequestConfig & {
995
997
  baseURL?: string;
996
998
  browser?: boolean;
997
999
  device?: BaseDeviceFields;
1000
+ enableInsights?: boolean;
998
1001
  logger?: Logger;
999
1002
  /**
1000
1003
  * When network is recovered, we re-query the active channels on client. But in single query, you can recover
@@ -1631,6 +1634,7 @@ export type ChannelConfigFields = {
1631
1634
  mutes?: boolean;
1632
1635
  name?: string;
1633
1636
  push_notifications?: boolean;
1637
+ quotes?: boolean;
1634
1638
  reactions?: boolean;
1635
1639
  read_events?: boolean;
1636
1640
  replies?: boolean;
@@ -1699,7 +1703,6 @@ export type PushProvider = 'apn' | 'firebase' | 'huawei';
1699
1703
  export type CommandVariants<CommandType extends string = LiteralStringForUnion> =
1700
1704
  | 'all'
1701
1705
  | 'ban'
1702
- | 'flag'
1703
1706
  | 'fun_set'
1704
1707
  | 'giphy'
1705
1708
  | 'imgur'
package/src/utils.ts CHANGED
@@ -144,12 +144,57 @@ export function retryInterval(numberOfFailures: number) {
144
144
  return Math.floor(Math.random() * (max - min) + min);
145
145
  }
146
146
 
147
- /** adopted from https://github.com/ai/nanoid/blob/master/non-secure/index.js */
148
- const alphabet = 'ModuleSymbhasOwnPr0123456789ABCDEFGHNRVfgctiUvzKqYTJkLxpZXIjQW';
149
147
  export function randomId() {
150
- let id = '';
151
- for (let i = 0; i < 21; i++) {
152
- id += alphabet[(Math.random() * 64) | 0];
148
+ return generateUUIDv4();
149
+ }
150
+
151
+ function hex(bytes: Uint8Array): string {
152
+ let s = '';
153
+ for (let i = 0; i < bytes.length; i++) {
154
+ s += bytes[i].toString(16).padStart(2, '0');
153
155
  }
154
- return id;
156
+ return s;
157
+ }
158
+
159
+ // https://tools.ietf.org/html/rfc4122
160
+ export function generateUUIDv4() {
161
+ const bytes = getRandomBytes(16);
162
+ bytes[6] = (bytes[6] & 0x0f) | 0x40; // version
163
+ bytes[8] = (bytes[8] & 0xbf) | 0x80; // variant
164
+
165
+ return (
166
+ hex(bytes.subarray(0, 4)) +
167
+ '-' +
168
+ hex(bytes.subarray(4, 6)) +
169
+ '-' +
170
+ hex(bytes.subarray(6, 8)) +
171
+ '-' +
172
+ hex(bytes.subarray(8, 10)) +
173
+ '-' +
174
+ hex(bytes.subarray(10, 16))
175
+ );
176
+ }
177
+
178
+ function getRandomValuesWithMathRandom(bytes: Uint8Array): void {
179
+ const max = Math.pow(2, (8 * bytes.byteLength) / bytes.length);
180
+ for (let i = 0; i < bytes.length; i++) {
181
+ bytes[i] = Math.random() * max;
182
+ }
183
+ }
184
+ declare const msCrypto: Crypto;
185
+
186
+ const getRandomValues = (() => {
187
+ if (typeof crypto !== 'undefined') {
188
+ return crypto.getRandomValues.bind(crypto);
189
+ } else if (typeof msCrypto !== 'undefined') {
190
+ return msCrypto.getRandomValues.bind(msCrypto);
191
+ } else {
192
+ return getRandomValuesWithMathRandom;
193
+ }
194
+ })();
195
+
196
+ function getRandomBytes(length: number): Uint8Array {
197
+ const bytes = new Uint8Array(length);
198
+ getRandomValues(bytes);
199
+ return bytes;
155
200
  }