@mongosh/logging 3.2.0 → 3.4.0

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.
@@ -0,0 +1,26 @@
1
+ import { expect } from 'chai';
2
+ import { toSnakeCase } from './helpers';
3
+
4
+ describe('logging helpers', function () {
5
+ describe('toSnakeCase', function () {
6
+ const useCases = [
7
+ { input: 'MongoDB REPL', output: 'mongo_db_repl' },
8
+ {
9
+ input: 'Node.js REPL Instantiation',
10
+ output: 'node_js_repl_instantiation',
11
+ },
12
+ { input: 'A', output: 'a' },
13
+ {
14
+ input: 'OneLongThingInPascalCase',
15
+ output: 'one_long_thing_in_pascal_case',
16
+ },
17
+ { input: 'Removes .Dots in Node.js', output: 'removes_dots_in_node_js' },
18
+ ];
19
+
20
+ for (const { input, output } of useCases) {
21
+ it(`should convert ${input} to ${output}`, function () {
22
+ expect(toSnakeCase(input)).to.equal(output);
23
+ });
24
+ }
25
+ });
26
+ });
package/src/helpers.ts ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * A helper class for keeping track of how often specific events occurred.
3
+ */
4
+ export class MultiSet<T extends Record<string, unknown>> {
5
+ _entries: Map<string, number> = new Map();
6
+
7
+ add(entry: T): void {
8
+ const key = JSON.stringify(Object.entries(entry).sort());
9
+ this._entries.set(key, (this._entries.get(key) ?? 0) + 1);
10
+ }
11
+
12
+ clear(): void {
13
+ this._entries.clear();
14
+ }
15
+
16
+ *[Symbol.iterator](): Iterator<[T, number]> {
17
+ for (const [key, count] of this._entries) {
18
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
19
+ yield [Object.fromEntries(JSON.parse(key)) as T, count];
20
+ }
21
+ }
22
+ }
23
+
24
+ /**
25
+ * It transforms a random string into snake case. Snake case is completely
26
+ * lowercase and uses '_' to separate words. For example:
27
+ *
28
+ * This function defines a "word" as a sequence of characters until the next `.` or capital letter.
29
+ *
30
+ * 'Random String' => 'random_string'
31
+ *
32
+ * It will also remove any non alphanumeric characters to ensure the string
33
+ * is compatible with Segment. For example:
34
+ *
35
+ * 'Node.js REPL Instantiation' => 'node_js_repl_instantiation'
36
+ *
37
+ * @param str Any non snake-case formatted string
38
+ * @returns The snake-case formatted string
39
+ */
40
+ export function toSnakeCase(str: string): string {
41
+ const matches = str.match(
42
+ /[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g
43
+ );
44
+ if (!matches) {
45
+ return str;
46
+ }
47
+
48
+ return matches.map((x) => x.toLowerCase()).join('_');
49
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,3 @@
1
- export { setupLoggerAndTelemetry } from './setup-logger-and-telemetry';
2
1
  export {
3
2
  MongoshAnalytics,
4
3
  ToggleableAnalytics,
@@ -6,3 +5,5 @@ export {
6
5
  NoopAnalytics,
7
6
  ThrottledAnalytics,
8
7
  } from './analytics-helpers';
8
+ export { MongoshLoggingAndTelemetry } from './types';
9
+ export { setupLoggingAndTelemetry } from './logging-and-telemetry';
@@ -1,35 +1,14 @@
1
1
  /* eslint-disable mocha/max-top-level-suites */
2
2
  import { expect } from 'chai';
3
3
  import { MongoLogWriter } from 'mongodb-log-writer';
4
- import { setupLoggerAndTelemetry } from './';
5
4
  import { EventEmitter } from 'events';
6
5
  import { MongoshInvalidInputError } from '@mongosh/errors';
7
6
  import type { MongoshBus } from '@mongosh/types';
8
- import { toSnakeCase } from './setup-logger-and-telemetry';
9
-
10
- describe('toSnakeCase', function () {
11
- const useCases = [
12
- { input: 'MongoDB REPL', output: 'mongo_db_repl' },
13
- {
14
- input: 'Node.js REPL Instantiation',
15
- output: 'node_js_repl_instantiation',
16
- },
17
- { input: 'A', output: 'a' },
18
- {
19
- input: 'OneLongThingInPascalCase',
20
- output: 'one_long_thing_in_pascal_case',
21
- },
22
- { input: 'Removes .Dots in Node.js', output: 'removes_dots_in_node_js' },
23
- ];
24
-
25
- for (const { input, output } of useCases) {
26
- it(`should convert ${input} to ${output}`, function () {
27
- expect(toSnakeCase(input)).to.equal(output);
28
- });
29
- }
30
- });
7
+ import type { Writable } from 'stream';
8
+ import type { MongoshLoggingAndTelemetry } from '.';
9
+ import { setupLoggingAndTelemetry } from '.';
31
10
 
32
- describe('setupLoggerAndTelemetry', function () {
11
+ describe('MongoshLoggingAndTelemetry', function () {
33
12
  let logOutput: any[];
34
13
  let analyticsOutput: ['identify' | 'track' | 'log', any][];
35
14
  let bus: MongoshBus;
@@ -37,15 +16,8 @@ describe('setupLoggerAndTelemetry', function () {
37
16
  const userId = '53defe995fa47e6c13102d9d';
38
17
  const logId = '5fb3c20ee1507e894e5340f3';
39
18
 
40
- const logger = new MongoLogWriter(logId, `/tmp/${logId}_log`, {
41
- write(chunk: string, cb: () => void) {
42
- logOutput.push(JSON.parse(chunk));
43
- cb();
44
- },
45
- end(cb: () => void) {
46
- cb();
47
- },
48
- } as any);
19
+ let logger: MongoLogWriter;
20
+
49
21
  const analytics = {
50
22
  identify(info: any) {
51
23
  analyticsOutput.push(['identify', info]);
@@ -58,26 +30,61 @@ describe('setupLoggerAndTelemetry', function () {
58
30
  },
59
31
  };
60
32
 
33
+ let loggingAndTelemetry: MongoshLoggingAndTelemetry;
34
+
61
35
  beforeEach(function () {
62
36
  logOutput = [];
63
37
  analyticsOutput = [];
64
38
  bus = new EventEmitter();
65
- });
66
39
 
67
- it('tracks new local connection events', function () {
68
- setupLoggerAndTelemetry(
40
+ loggingAndTelemetry = setupLoggingAndTelemetry({
69
41
  bus,
70
- logger,
71
42
  analytics,
72
- {
43
+ userTraits: {
73
44
  platform: process.platform,
74
45
  arch: process.arch,
75
46
  },
76
- '1.0.0'
47
+ mongoshVersion: '1.0.0',
48
+ });
49
+
50
+ logger = new MongoLogWriter(logId, `/tmp/${logId}_log`, {
51
+ write(chunk: string, cb: () => void) {
52
+ logOutput.push(JSON.parse(chunk));
53
+ cb();
54
+ },
55
+ end(cb: () => void) {
56
+ cb();
57
+ },
58
+ } as Writable);
59
+ });
60
+
61
+ afterEach(function () {
62
+ loggingAndTelemetry.detachLogger();
63
+ logger.destroy();
64
+ });
65
+
66
+ it('throws when running attachLogger twice without detaching', function () {
67
+ loggingAndTelemetry.attachLogger(logger);
68
+ expect(() => loggingAndTelemetry.attachLogger(logger)).throws(
69
+ 'Previously set logger has not been detached. Run detachLogger() before setting.'
77
70
  );
71
+ });
72
+
73
+ it('does not throw when attaching and detaching loggers', function () {
74
+ loggingAndTelemetry.attachLogger(logger);
75
+ loggingAndTelemetry.detachLogger();
76
+ expect(() => loggingAndTelemetry.attachLogger(logger)).does.not.throw();
77
+ });
78
+
79
+ it('tracks new local connection events', function () {
80
+ loggingAndTelemetry.attachLogger(logger);
81
+
78
82
  expect(logOutput).to.have.lengthOf(0);
79
83
  expect(analyticsOutput).to.be.empty;
80
84
 
85
+ bus.emit('mongosh:new-user', { userId, anonymousId: userId });
86
+ bus.emit('mongosh:log-initialized');
87
+
81
88
  bus.emit('mongosh:connect', {
82
89
  uri: 'mongodb://localhost/',
83
90
  is_localhost: true,
@@ -94,14 +101,25 @@ describe('setupLoggerAndTelemetry', function () {
94
101
  expect(logOutput[0].attr.node_version).to.equal('v12.19.0');
95
102
 
96
103
  expect(analyticsOutput).to.deep.equal([
104
+ [
105
+ 'identify',
106
+ {
107
+ anonymousId: userId,
108
+ traits: {
109
+ arch: process.arch,
110
+ platform: process.platform,
111
+ session_id: logId,
112
+ },
113
+ },
114
+ ],
97
115
  [
98
116
  'track',
99
117
  {
100
- anonymousId: undefined,
118
+ anonymousId: userId,
101
119
  event: 'New Connection',
102
120
  properties: {
103
121
  mongosh_version: '1.0.0',
104
- session_id: '5fb3c20ee1507e894e5340f3',
122
+ session_id: logId,
105
123
  is_localhost: true,
106
124
  is_atlas: false,
107
125
  atlas_hostname: null,
@@ -113,19 +131,14 @@ describe('setupLoggerAndTelemetry', function () {
113
131
  });
114
132
 
115
133
  it('tracks new atlas connection events', function () {
116
- setupLoggerAndTelemetry(
117
- bus,
118
- logger,
119
- analytics,
120
- {
121
- platform: process.platform,
122
- arch: process.arch,
123
- },
124
- '1.0.0'
125
- );
134
+ loggingAndTelemetry.attachLogger(logger);
135
+
126
136
  expect(logOutput).to.have.lengthOf(0);
127
137
  expect(analyticsOutput).to.be.empty;
128
138
 
139
+ bus.emit('mongosh:new-user', { userId, anonymousId: userId });
140
+ bus.emit('mongosh:log-initialized');
141
+
129
142
  bus.emit('mongosh:connect', {
130
143
  uri: 'mongodb://test-data-sets-a011bb.mongodb.net/',
131
144
  is_localhost: false,
@@ -146,14 +159,25 @@ describe('setupLoggerAndTelemetry', function () {
146
159
  expect(logOutput[0].attr.node_version).to.equal('v12.19.0');
147
160
 
148
161
  expect(analyticsOutput).to.deep.equal([
162
+ [
163
+ 'identify',
164
+ {
165
+ anonymousId: userId,
166
+ traits: {
167
+ arch: process.arch,
168
+ platform: process.platform,
169
+ session_id: logId,
170
+ },
171
+ },
172
+ ],
149
173
  [
150
174
  'track',
151
175
  {
152
- anonymousId: undefined,
176
+ anonymousId: userId,
153
177
  event: 'New Connection',
154
178
  properties: {
155
179
  mongosh_version: '1.0.0',
156
- session_id: '5fb3c20ee1507e894e5340f3',
180
+ session_id: logId,
157
181
  is_localhost: false,
158
182
  is_atlas: true,
159
183
  atlas_hostname: 'test-data-sets-00-02-a011bb.mongodb.net',
@@ -164,21 +188,100 @@ describe('setupLoggerAndTelemetry', function () {
164
188
  ]);
165
189
  });
166
190
 
191
+ it('detaching logger leads to no logging but persists analytics', function () {
192
+ loggingAndTelemetry.attachLogger(logger);
193
+
194
+ expect(logOutput).to.have.lengthOf(0);
195
+ expect(analyticsOutput).to.have.lengthOf(0);
196
+
197
+ loggingAndTelemetry.detachLogger();
198
+
199
+ // This event has both analytics and logging
200
+ bus.emit('mongosh:use', { db: '' });
201
+
202
+ expect(logOutput).to.have.lengthOf(0);
203
+ expect(analyticsOutput).to.have.lengthOf(1);
204
+ });
205
+
206
+ it('detaching logger applies to devtools-connect events', function () {
207
+ loggingAndTelemetry.attachLogger(logger);
208
+
209
+ bus.emit('devtools-connect:connect-fail-early');
210
+ bus.emit('devtools-connect:connect-fail-early');
211
+
212
+ expect(logOutput).to.have.lengthOf(2);
213
+ // No analytics event attached to this
214
+ expect(analyticsOutput).to.have.lengthOf(0);
215
+
216
+ loggingAndTelemetry.detachLogger();
217
+ bus.emit('devtools-connect:connect-fail-early');
218
+
219
+ expect(logOutput).to.have.lengthOf(2);
220
+ expect(analyticsOutput).to.have.lengthOf(0);
221
+
222
+ loggingAndTelemetry.attachLogger(logger);
223
+
224
+ bus.emit('devtools-connect:connect-fail-early');
225
+ bus.emit('devtools-connect:connect-fail-early');
226
+ expect(logOutput).to.have.lengthOf(4);
227
+ });
228
+
229
+ it('detaching logger mid-way leads to no logging but persists analytics', function () {
230
+ loggingAndTelemetry.attachLogger(logger);
231
+
232
+ expect(logOutput).to.have.lengthOf(0);
233
+ expect(analyticsOutput).to.have.lengthOf(0);
234
+
235
+ // This event has both analytics and logging
236
+ bus.emit('mongosh:use', { db: '' });
237
+
238
+ expect(logOutput).to.have.lengthOf(1);
239
+ expect(analyticsOutput).to.have.lengthOf(1);
240
+
241
+ loggingAndTelemetry.detachLogger();
242
+
243
+ bus.emit('mongosh:use', { db: '' });
244
+
245
+ expect(logOutput).to.have.lengthOf(1);
246
+ expect(analyticsOutput).to.have.lengthOf(2);
247
+ });
248
+
249
+ it('detaching logger is recoverable', function () {
250
+ loggingAndTelemetry.attachLogger(logger);
251
+
252
+ expect(logOutput).to.have.lengthOf(0);
253
+ expect(analyticsOutput).to.have.lengthOf(0);
254
+
255
+ // This event has both analytics and logging
256
+ bus.emit('mongosh:use', { db: '' });
257
+
258
+ expect(logOutput).to.have.lengthOf(1);
259
+ expect(analyticsOutput).to.have.lengthOf(1);
260
+
261
+ loggingAndTelemetry.detachLogger();
262
+
263
+ bus.emit('mongosh:use', { db: '' });
264
+
265
+ expect(logOutput).to.have.lengthOf(1);
266
+ expect(analyticsOutput).to.have.lengthOf(2);
267
+
268
+ loggingAndTelemetry.attachLogger(logger);
269
+
270
+ bus.emit('mongosh:use', { db: '' });
271
+
272
+ expect(logOutput).to.have.lengthOf(2);
273
+ expect(analyticsOutput).to.have.lengthOf(3);
274
+ });
275
+
167
276
  it('tracks a sequence of events', function () {
168
- setupLoggerAndTelemetry(
169
- bus,
170
- logger,
171
- analytics,
172
- {
173
- platform: process.platform,
174
- arch: process.arch,
175
- },
176
- '1.0.0'
177
- );
277
+ loggingAndTelemetry.attachLogger(logger);
278
+
178
279
  expect(logOutput).to.have.lengthOf(0);
179
280
  expect(analyticsOutput).to.be.empty;
180
281
 
181
282
  bus.emit('mongosh:new-user', { userId, anonymousId: userId });
283
+ bus.emit('mongosh:log-initialized');
284
+
182
285
  bus.emit('mongosh:update-user', { userId, anonymousId: userId });
183
286
  bus.emit('mongosh:start-session', {
184
287
  isInteractive: true,
@@ -476,7 +579,7 @@ describe('setupLoggerAndTelemetry', function () {
476
579
  traits: {
477
580
  platform: process.platform,
478
581
  arch: process.arch,
479
- session_id: '5fb3c20ee1507e894e5340f3',
582
+ session_id: logId,
480
583
  },
481
584
  },
482
585
  ],
@@ -487,7 +590,7 @@ describe('setupLoggerAndTelemetry', function () {
487
590
  traits: {
488
591
  platform: process.platform,
489
592
  arch: process.arch,
490
- session_id: '5fb3c20ee1507e894e5340f3',
593
+ session_id: logId,
491
594
  },
492
595
  },
493
596
  ],
@@ -502,7 +605,7 @@ describe('setupLoggerAndTelemetry', function () {
502
605
  boxed_node_bindings: 50,
503
606
  node_repl: 100,
504
607
  mongosh_version: '1.0.0',
505
- session_id: '5fb3c20ee1507e894e5340f3',
608
+ session_id: logId,
506
609
  },
507
610
  },
508
611
  ],
@@ -513,7 +616,7 @@ describe('setupLoggerAndTelemetry', function () {
513
616
  event: 'Error',
514
617
  properties: {
515
618
  mongosh_version: '1.0.0',
516
- session_id: '5fb3c20ee1507e894e5340f3',
619
+ session_id: logId,
517
620
  name: 'MongoshInvalidInputError',
518
621
  code: 'CLIREPL-1005',
519
622
  scope: 'CLIREPL',
@@ -528,7 +631,7 @@ describe('setupLoggerAndTelemetry', function () {
528
631
  event: 'Error',
529
632
  properties: {
530
633
  mongosh_version: '1.0.0',
531
- session_id: '5fb3c20ee1507e894e5340f3',
634
+ session_id: logId,
532
635
  name: 'MongoshInvalidInputError',
533
636
  code: 'CLIREPL-1005',
534
637
  scope: 'CLIREPL',
@@ -543,7 +646,7 @@ describe('setupLoggerAndTelemetry', function () {
543
646
  event: 'Use',
544
647
  properties: {
545
648
  mongosh_version: '1.0.0',
546
- session_id: '5fb3c20ee1507e894e5340f3',
649
+ session_id: logId,
547
650
  },
548
651
  },
549
652
  ],
@@ -554,7 +657,7 @@ describe('setupLoggerAndTelemetry', function () {
554
657
  event: 'Show',
555
658
  properties: {
556
659
  mongosh_version: '1.0.0',
557
- session_id: '5fb3c20ee1507e894e5340f3',
660
+ session_id: logId,
558
661
  method: 'dbs',
559
662
  },
560
663
  },
@@ -565,7 +668,7 @@ describe('setupLoggerAndTelemetry', function () {
565
668
  event: 'Script Loaded CLI',
566
669
  properties: {
567
670
  mongosh_version: '1.0.0',
568
- session_id: '5fb3c20ee1507e894e5340f3',
671
+ session_id: logId,
569
672
  nested: true,
570
673
  shell: true,
571
674
  },
@@ -578,7 +681,7 @@ describe('setupLoggerAndTelemetry', function () {
578
681
  event: 'Script Loaded',
579
682
  properties: {
580
683
  mongosh_version: '1.0.0',
581
- session_id: '5fb3c20ee1507e894e5340f3',
684
+ session_id: logId,
582
685
  nested: false,
583
686
  },
584
687
  anonymousId: '53defe995fa47e6c13102d9d',
@@ -590,7 +693,7 @@ describe('setupLoggerAndTelemetry', function () {
590
693
  event: 'Mongoshrc Loaded',
591
694
  properties: {
592
695
  mongosh_version: '1.0.0',
593
- session_id: '5fb3c20ee1507e894e5340f3',
696
+ session_id: logId,
594
697
  },
595
698
  anonymousId: '53defe995fa47e6c13102d9d',
596
699
  },
@@ -601,7 +704,7 @@ describe('setupLoggerAndTelemetry', function () {
601
704
  event: 'Mongorc Warning',
602
705
  properties: {
603
706
  mongosh_version: '1.0.0',
604
- session_id: '5fb3c20ee1507e894e5340f3',
707
+ session_id: logId,
605
708
  },
606
709
  anonymousId: '53defe995fa47e6c13102d9d',
607
710
  },
@@ -612,7 +715,7 @@ describe('setupLoggerAndTelemetry', function () {
612
715
  event: 'Script Evaluated',
613
716
  properties: {
614
717
  mongosh_version: '1.0.0',
615
- session_id: '5fb3c20ee1507e894e5340f3',
718
+ session_id: logId,
616
719
  shell: true,
617
720
  },
618
721
  anonymousId: '53defe995fa47e6c13102d9d',
@@ -625,7 +728,7 @@ describe('setupLoggerAndTelemetry', function () {
625
728
  event: 'Snippet Install',
626
729
  properties: {
627
730
  mongosh_version: '1.0.0',
628
- session_id: '5fb3c20ee1507e894e5340f3',
731
+ session_id: logId,
629
732
  },
630
733
  },
631
734
  ],
@@ -633,11 +736,14 @@ describe('setupLoggerAndTelemetry', function () {
633
736
  });
634
737
 
635
738
  it('buffers deprecated API calls', function () {
636
- setupLoggerAndTelemetry(bus, logger, analytics, {}, '1.0.0');
739
+ loggingAndTelemetry.attachLogger(logger);
740
+
637
741
  expect(logOutput).to.have.lengthOf(0);
638
742
  expect(analyticsOutput).to.be.empty;
639
743
 
640
744
  bus.emit('mongosh:new-user', { userId, anonymousId: userId });
745
+ bus.emit('mongosh:log-initialized');
746
+
641
747
  bus.emit('mongosh:evaluate-started');
642
748
 
643
749
  logOutput = [];
@@ -716,7 +822,7 @@ describe('setupLoggerAndTelemetry', function () {
716
822
  event: 'Deprecated Method',
717
823
  properties: {
718
824
  mongosh_version: '1.0.0',
719
- session_id: '5fb3c20ee1507e894e5340f3',
825
+ session_id: logId,
720
826
  class: 'Database',
721
827
  method: 'cloneDatabase',
722
828
  },
@@ -729,7 +835,7 @@ describe('setupLoggerAndTelemetry', function () {
729
835
  event: 'Deprecated Method',
730
836
  properties: {
731
837
  mongosh_version: '1.0.0',
732
- session_id: '5fb3c20ee1507e894e5340f3',
838
+ session_id: logId,
733
839
  class: 'Database',
734
840
  method: 'copyDatabase',
735
841
  },
@@ -742,7 +848,7 @@ describe('setupLoggerAndTelemetry', function () {
742
848
  event: 'Deprecated Method',
743
849
  properties: {
744
850
  mongosh_version: '1.0.0',
745
- session_id: '5fb3c20ee1507e894e5340f3',
851
+ session_id: logId,
746
852
  class: 'Database',
747
853
  method: 'mangleDatabase',
748
854
  },
@@ -755,7 +861,7 @@ describe('setupLoggerAndTelemetry', function () {
755
861
  event: 'API Call',
756
862
  properties: {
757
863
  mongosh_version: '1.0.0',
758
- session_id: '5fb3c20ee1507e894e5340f3',
864
+ session_id: logId,
759
865
  class: 'Database',
760
866
  method: 'cloneDatabase',
761
867
  count: 3,
@@ -769,7 +875,7 @@ describe('setupLoggerAndTelemetry', function () {
769
875
  event: 'API Call',
770
876
  properties: {
771
877
  mongosh_version: '1.0.0',
772
- session_id: '5fb3c20ee1507e894e5340f3',
878
+ session_id: logId,
773
879
  class: 'Database',
774
880
  method: 'copyDatabase',
775
881
  count: 1,
@@ -805,7 +911,8 @@ describe('setupLoggerAndTelemetry', function () {
805
911
  });
806
912
 
807
913
  it('does not track database calls outside of evaluate-{started,finished}', function () {
808
- setupLoggerAndTelemetry(bus, logger, analytics, {}, '1.0.0');
914
+ loggingAndTelemetry.attachLogger(logger);
915
+
809
916
  expect(logOutput).to.have.lengthOf(0);
810
917
  expect(analyticsOutput).to.be.empty;
811
918
 
@@ -828,19 +935,11 @@ describe('setupLoggerAndTelemetry', function () {
828
935
  });
829
936
 
830
937
  it('tracks custom logging events', function () {
831
- setupLoggerAndTelemetry(
832
- bus,
833
- logger,
834
- analytics,
835
- {
836
- platform: process.platform,
837
- arch: process.arch,
838
- },
839
- '1.0.0'
840
- );
841
938
  expect(logOutput).to.have.lengthOf(0);
842
939
  expect(analyticsOutput).to.be.empty;
843
940
 
941
+ loggingAndTelemetry.attachLogger(logger);
942
+
844
943
  bus.emit('mongosh:connect', {
845
944
  uri: 'mongodb://localhost/',
846
945
  is_localhost: true,