diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v1.js b/packages/bolt-connection/src/bolt/bolt-protocol-v1.js index 18b643ded..7f4469bf1 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v1.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v1.js @@ -27,6 +27,7 @@ import { Chunker } from '../channel' import { structure, v1 } from '../packstream' import RequestMessage, { SIGNATURES } from './request-message' import { + CompletedObserver, LoginObserver, LogoffObserver, ResetObserver, @@ -454,6 +455,24 @@ export default class BoltProtocol { return observer } + /** + * Send a TELEMETRY through the underlying connection. + * + * @param {object} param0 Message params + * @param {number} param0.api The API called + * @param {object} param1 Configuration and callbacks + * @param {function()} param1.onCompleted Called when completed + * @param {function()} param1.onError Called when error + * @return {StreamObserver} the stream observer that monitors the corresponding server response. + */ + telemetry ({ api }, { onError, onCompleted } = {}) { + const observer = new CompletedObserver() + if (onCompleted) { + onCompleted() + } + return observer + } + _createPacker (chunker) { return new v1.Packer(chunker) } diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v5x4.js b/packages/bolt-connection/src/bolt/bolt-protocol-v5x4.js new file mode 100644 index 000000000..f9763f81a --- /dev/null +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v5x4.js @@ -0,0 +1,61 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import BoltProtocolV5x3 from './bolt-protocol-v5x3' + +import transformersFactories from './bolt-protocol-v5x4.transformer' +import RequestMessage from './request-message' +import { TelemetryObserver } from './stream-observers' +import Transformer from './transformer' + +import { internal } from 'neo4j-driver-core' + +const { + constants: { BOLT_PROTOCOL_V5_4 } +} = internal + +export default class BoltProtocol extends BoltProtocolV5x3 { + get version () { + return BOLT_PROTOCOL_V5_4 + } + + get transformer () { + if (this._transformer === undefined) { + this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config, this._log))) + } + return this._transformer + } + + /** + * Send a TELEMETRY through the underlying connection. + * + * @param {object} param0 Message params + * @param {number} param0.api The API called + * @param {object} param1 Configuration and callbacks callbacks + * @param {function()} param1.onCompleted Called when completed + * @param {function()} param1.onError Called when error + * @return {StreamObserver} the stream observer that monitors the corresponding server response. + */ + telemetry ({ api }, { onError, onCompleted } = {}) { + const observer = new TelemetryObserver({ onCompleted, onError }) + + this.write(RequestMessage.telemetry({ api }), observer, false) + + return observer + } +} diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v5x4.transformer.js b/packages/bolt-connection/src/bolt/bolt-protocol-v5x4.transformer.js new file mode 100644 index 000000000..ef582907c --- /dev/null +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v5x4.transformer.js @@ -0,0 +1,24 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import v5x3 from './bolt-protocol-v5x3.transformer' + +export default { + ...v5x3 +} diff --git a/packages/bolt-connection/src/bolt/create.js b/packages/bolt-connection/src/bolt/create.js index 92906fda9..9638b654c 100644 --- a/packages/bolt-connection/src/bolt/create.js +++ b/packages/bolt-connection/src/bolt/create.js @@ -30,6 +30,7 @@ import BoltProtocolV5x0 from './bolt-protocol-v5x0' import BoltProtocolV5x1 from './bolt-protocol-v5x1' import BoltProtocolV5x2 from './bolt-protocol-v5x2' import BoltProtocolV5x3 from './bolt-protocol-v5x3' +import BoltProtocolV5x4 from './bolt-protocol-v5x4' // eslint-disable-next-line no-unused-vars import { Chunker, Dechunker } from '../channel' import ResponseHandler from './response-handler' @@ -222,6 +223,14 @@ function createProtocol ( log, onProtocolError, serversideRouting) + case 5.4: + return new BoltProtocolV5x4(server, + chunker, + packingConfig, + createResponseHandler, + log, + onProtocolError, + serversideRouting) default: throw newError('Unknown Bolt protocol version: ' + version) } diff --git a/packages/bolt-connection/src/bolt/handshake.js b/packages/bolt-connection/src/bolt/handshake.js index b1222ff40..b7a50e8f3 100644 --- a/packages/bolt-connection/src/bolt/handshake.js +++ b/packages/bolt-connection/src/bolt/handshake.js @@ -78,7 +78,7 @@ function parseNegotiatedResponse (buffer, log) { */ function newHandshakeBuffer () { return createHandshakeMessage([ - [version(5, 3), version(5, 0)], + [version(5, 4), version(5, 0)], [version(4, 4), version(4, 2)], version(4, 1), version(3, 0) diff --git a/packages/bolt-connection/src/bolt/request-message.js b/packages/bolt-connection/src/bolt/request-message.js index 6ee13651d..d0ea30e4a 100644 --- a/packages/bolt-connection/src/bolt/request-message.js +++ b/packages/bolt-connection/src/bolt/request-message.js @@ -38,6 +38,9 @@ const GOODBYE = 0x02 // 0000 0010 // GOODBYE const BEGIN = 0x11 // 0001 0001 // BEGIN const COMMIT = 0x12 // 0001 0010 // COMMIT const ROLLBACK = 0x13 // 0001 0011 // ROLLBACK + +const TELEMETRY = 0x54 // 0101 0100 // TELEMETRY + const ROUTE = 0x66 // 0110 0110 // ROUTE const LOGON = 0x6A // LOGON @@ -61,6 +64,7 @@ const SIGNATURES = Object.freeze({ BEGIN, COMMIT, ROLLBACK, + TELEMETRY, ROUTE, LOGON, LOGOFF, @@ -367,6 +371,15 @@ export default class RequestMessage { ) } + static telemetry ({ api }) { + const parsedApi = int(api) + return new RequestMessage( + TELEMETRY, + [parsedApi], + () => `TELEMETRY ${parsedApi.toString()}` + ) + } + /** * Generate the ROUTE message, this message is used to fetch the routing table from the server * diff --git a/packages/bolt-connection/src/bolt/stream-observers.js b/packages/bolt-connection/src/bolt/stream-observers.js index 04a243e4b..b210ef836 100644 --- a/packages/bolt-connection/src/bolt/stream-observers.js +++ b/packages/bolt-connection/src/bolt/stream-observers.js @@ -526,6 +526,38 @@ class ResetObserver extends StreamObserver { } } +class TelemetryObserver extends ResultStreamObserver { + /** + * + * @param {Object} param - + * @param {function(err: Error)} param.onError + * @param {function(metadata)} param.onCompleted + */ + constructor ({ onError, onCompleted } = {}) { + super() + this._onError = onError + this._onCompleted = onCompleted + } + + onNext (record) { + this.onError( + newError('Received RECORD when sending telemetry ' + json.stringify(record), PROTOCOL_ERROR) + ) + } + + onError (error) { + if (this._onError) { + this._onError(error) + } + } + + onCompleted (metadata) { + if (this._onCompleted) { + this._onCompleted(metadata) + } + } +} + class FailedObserver extends ResultStreamObserver { constructor ({ error, onError }) { super({ beforeError: onError }) @@ -708,5 +740,6 @@ export { FailedObserver, CompletedObserver, RouteObserver, - ProcedureRouteObserver + ProcedureRouteObserver, + TelemetryObserver } diff --git a/packages/bolt-connection/src/connection/connection-channel.js b/packages/bolt-connection/src/connection/connection-channel.js index 749cf23e6..29b650d35 100644 --- a/packages/bolt-connection/src/connection/connection-channel.js +++ b/packages/bolt-connection/src/connection/connection-channel.js @@ -87,7 +87,8 @@ export function createChannelConnection ( serversideRouting, chunker, config.notificationFilter, - createProtocol + createProtocol, + config.telemetryDisabled ) // forward all pending bytes to the dechunker @@ -121,7 +122,8 @@ export default class ChannelConnection extends Connection { serversideRouting = null, chunker, // to be removed, notificationFilter, - protocolSupplier + protocolSupplier, + telemetryDisabled ) { super(errorHandler) this._authToken = null @@ -137,6 +139,8 @@ export default class ChannelConnection extends Connection { this._log = createConnectionLogger(this, log) this._serversideRouting = serversideRouting this._notificationFilter = notificationFilter + this._telemetryDisabledDriverConfig = telemetryDisabled === true + this._telemetryDisabledConnection = true // connection from the database, returned in response for HELLO message and might not be available this._dbConnectionId = null @@ -157,13 +161,31 @@ export default class ChannelConnection extends Connection { } beginTransaction (config) { + this._sendTelemetryIfEnabled(config) return this._protocol.beginTransaction(config) } run (query, parameters, config) { + this._sendTelemetryIfEnabled(config) return this._protocol.run(query, parameters, config) } + _sendTelemetryIfEnabled (config) { + if (this._telemetryDisabledConnection || + this._telemetryDisabledDriverConfig || + config == null || + config.apiTelemetryConfig == null) { + return + } + + this._protocol.telemetry({ + api: config.apiTelemetryConfig.api + }, { + onCompleted: config.apiTelemetryConfig.onTelemetrySuccess, + onError: config.beforeError + }) + } + commitTransaction (config) { return this._protocol.commitTransaction(config) } @@ -290,6 +312,11 @@ export default class ChannelConnection extends Connection { ) } } + + const telemetryEnabledHint = metadata.hints['telemetry.enabled'] + if (telemetryEnabledHint === true) { + this._telemetryDisabledConnection = false + } } } resolve(self) diff --git a/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v1.test.js.snap b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v1.test.js.snap index 77712f134..5f5c946e2 100644 --- a/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v1.test.js.snap +++ b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v1.test.js.snap @@ -1,12 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`#unit BoltProtocolV1 .packable() should pack not pack graph types (Node) 1`] = `"It is not allowed to pass nodes in query parameters, given: (c:a {a:"b"})"`; +exports[`#unit BoltProtocolV1 .packable() should resultant function not pack graph types (Node) 1`] = `"It is not allowed to pass nodes in query parameters, given: (c:a {a:"b"})"`; -exports[`#unit BoltProtocolV1 .packable() should pack not pack graph types (Path) 1`] = `"It is not allowed to pass paths in query parameters, given: [object Object]"`; +exports[`#unit BoltProtocolV1 .packable() should resultant function not pack graph types (Path) 1`] = `"It is not allowed to pass paths in query parameters, given: [object Object]"`; -exports[`#unit BoltProtocolV1 .packable() should pack not pack graph types (Relationship) 1`] = `"It is not allowed to pass relationships in query parameters, given: (e)-[:a {b:"c"}]->(f)"`; +exports[`#unit BoltProtocolV1 .packable() should resultant function not pack graph types (Relationship) 1`] = `"It is not allowed to pass relationships in query parameters, given: (e)-[:a {b:"c"}]->(f)"`; -exports[`#unit BoltProtocolV1 .packable() should pack not pack graph types (UnboundRelationship) 1`] = `"It is not allowed to pass unbound relationships in query parameters, given: -[:a {b:"c"}]->"`; +exports[`#unit BoltProtocolV1 .packable() should resultant function not pack graph types (UnboundRelationship) 1`] = `"It is not allowed to pass unbound relationships in query parameters, given: -[:a {b:"c"}]->"`; exports[`#unit BoltProtocolV1 .packable() should pack types introduced afterwards as Map (Date) 1`] = ` { diff --git a/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v2.test.js.snap b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v2.test.js.snap index bbbf9ec01..d8932a782 100644 --- a/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v2.test.js.snap +++ b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v2.test.js.snap @@ -1,12 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`#unit BoltProtocolV2 .packable() should pack not pack graph types (Node) 1`] = `"It is not allowed to pass nodes in query parameters, given: (c:a {a:"b"})"`; +exports[`#unit BoltProtocolV2 .packable() should resultant function not pack graph types (Node) 1`] = `"It is not allowed to pass nodes in query parameters, given: (c:a {a:"b"})"`; -exports[`#unit BoltProtocolV2 .packable() should pack not pack graph types (Path) 1`] = `"It is not allowed to pass paths in query parameters, given: [object Object]"`; +exports[`#unit BoltProtocolV2 .packable() should resultant function not pack graph types (Path) 1`] = `"It is not allowed to pass paths in query parameters, given: [object Object]"`; -exports[`#unit BoltProtocolV2 .packable() should pack not pack graph types (Relationship) 1`] = `"It is not allowed to pass relationships in query parameters, given: (e)-[:a {b:"c"}]->(f)"`; +exports[`#unit BoltProtocolV2 .packable() should resultant function not pack graph types (Relationship) 1`] = `"It is not allowed to pass relationships in query parameters, given: (e)-[:a {b:"c"}]->(f)"`; -exports[`#unit BoltProtocolV2 .packable() should pack not pack graph types (UnboundRelationship) 1`] = `"It is not allowed to pass unbound relationships in query parameters, given: -[:a {b:"c"}]->"`; +exports[`#unit BoltProtocolV2 .packable() should resultant function not pack graph types (UnboundRelationship) 1`] = `"It is not allowed to pass unbound relationships in query parameters, given: -[:a {b:"c"}]->"`; exports[`#unit BoltProtocolV2 .unpack() should not unpack with wrong size (Date with less fields) 1`] = `"Wrong struct size for Date, expected 1 but was 0"`; diff --git a/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v3.test.js.snap b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v3.test.js.snap index d394ce87f..973736e88 100644 --- a/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v3.test.js.snap +++ b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v3.test.js.snap @@ -1,12 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`#unit BoltProtocolV3 .packable() should pack not pack graph types (Node) 1`] = `"It is not allowed to pass nodes in query parameters, given: (c:a {a:"b"})"`; +exports[`#unit BoltProtocolV3 .packable() should resultant function not pack graph types (Node) 1`] = `"It is not allowed to pass nodes in query parameters, given: (c:a {a:"b"})"`; -exports[`#unit BoltProtocolV3 .packable() should pack not pack graph types (Path) 1`] = `"It is not allowed to pass paths in query parameters, given: [object Object]"`; +exports[`#unit BoltProtocolV3 .packable() should resultant function not pack graph types (Path) 1`] = `"It is not allowed to pass paths in query parameters, given: [object Object]"`; -exports[`#unit BoltProtocolV3 .packable() should pack not pack graph types (Relationship) 1`] = `"It is not allowed to pass relationships in query parameters, given: (e)-[:a {b:"c"}]->(f)"`; +exports[`#unit BoltProtocolV3 .packable() should resultant function not pack graph types (Relationship) 1`] = `"It is not allowed to pass relationships in query parameters, given: (e)-[:a {b:"c"}]->(f)"`; -exports[`#unit BoltProtocolV3 .packable() should pack not pack graph types (UnboundRelationship) 1`] = `"It is not allowed to pass unbound relationships in query parameters, given: -[:a {b:"c"}]->"`; +exports[`#unit BoltProtocolV3 .packable() should resultant function not pack graph types (UnboundRelationship) 1`] = `"It is not allowed to pass unbound relationships in query parameters, given: -[:a {b:"c"}]->"`; exports[`#unit BoltProtocolV3 .unpack() should not unpack with wrong size (Date with less fields) 1`] = `"Wrong struct size for Date, expected 1 but was 0"`; diff --git a/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v4x0.test.js.snap b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v4x0.test.js.snap index 9ef4335b2..421602c0a 100644 --- a/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v4x0.test.js.snap +++ b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v4x0.test.js.snap @@ -1,12 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`#unit BoltProtocolV4x0 .packable() should pack not pack graph types (Node) 1`] = `"It is not allowed to pass nodes in query parameters, given: (c:a {a:"b"})"`; +exports[`#unit BoltProtocolV4x0 .packable() should resultant function not pack graph types (Node) 1`] = `"It is not allowed to pass nodes in query parameters, given: (c:a {a:"b"})"`; -exports[`#unit BoltProtocolV4x0 .packable() should pack not pack graph types (Path) 1`] = `"It is not allowed to pass paths in query parameters, given: [object Object]"`; +exports[`#unit BoltProtocolV4x0 .packable() should resultant function not pack graph types (Path) 1`] = `"It is not allowed to pass paths in query parameters, given: [object Object]"`; -exports[`#unit BoltProtocolV4x0 .packable() should pack not pack graph types (Relationship) 1`] = `"It is not allowed to pass relationships in query parameters, given: (e)-[:a {b:"c"}]->(f)"`; +exports[`#unit BoltProtocolV4x0 .packable() should resultant function not pack graph types (Relationship) 1`] = `"It is not allowed to pass relationships in query parameters, given: (e)-[:a {b:"c"}]->(f)"`; -exports[`#unit BoltProtocolV4x0 .packable() should pack not pack graph types (UnboundRelationship) 1`] = `"It is not allowed to pass unbound relationships in query parameters, given: -[:a {b:"c"}]->"`; +exports[`#unit BoltProtocolV4x0 .packable() should resultant function not pack graph types (UnboundRelationship) 1`] = `"It is not allowed to pass unbound relationships in query parameters, given: -[:a {b:"c"}]->"`; exports[`#unit BoltProtocolV4x0 .unpack() should not unpack with wrong size (Date with less fields) 1`] = `"Wrong struct size for Date, expected 1 but was 0"`; diff --git a/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v4x1.test.js.snap b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v4x1.test.js.snap index 81ae58ea4..65e5d3c8d 100644 --- a/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v4x1.test.js.snap +++ b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v4x1.test.js.snap @@ -1,12 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`#unit BoltProtocolV4x1 .packable() should pack not pack graph types (Node) 1`] = `"It is not allowed to pass nodes in query parameters, given: (c:a {a:"b"})"`; +exports[`#unit BoltProtocolV4x1 .packable() should resultant function not pack graph types (Node) 1`] = `"It is not allowed to pass nodes in query parameters, given: (c:a {a:"b"})"`; -exports[`#unit BoltProtocolV4x1 .packable() should pack not pack graph types (Path) 1`] = `"It is not allowed to pass paths in query parameters, given: [object Object]"`; +exports[`#unit BoltProtocolV4x1 .packable() should resultant function not pack graph types (Path) 1`] = `"It is not allowed to pass paths in query parameters, given: [object Object]"`; -exports[`#unit BoltProtocolV4x1 .packable() should pack not pack graph types (Relationship) 1`] = `"It is not allowed to pass relationships in query parameters, given: (e)-[:a {b:"c"}]->(f)"`; +exports[`#unit BoltProtocolV4x1 .packable() should resultant function not pack graph types (Relationship) 1`] = `"It is not allowed to pass relationships in query parameters, given: (e)-[:a {b:"c"}]->(f)"`; -exports[`#unit BoltProtocolV4x1 .packable() should pack not pack graph types (UnboundRelationship) 1`] = `"It is not allowed to pass unbound relationships in query parameters, given: -[:a {b:"c"}]->"`; +exports[`#unit BoltProtocolV4x1 .packable() should resultant function not pack graph types (UnboundRelationship) 1`] = `"It is not allowed to pass unbound relationships in query parameters, given: -[:a {b:"c"}]->"`; exports[`#unit BoltProtocolV4x1 .unpack() should not unpack with wrong size (Date with less fields) 1`] = `"Wrong struct size for Date, expected 1 but was 0"`; diff --git a/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v4x2.test.js.snap b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v4x2.test.js.snap index 31e3e10e0..1dbde2b9b 100644 --- a/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v4x2.test.js.snap +++ b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v4x2.test.js.snap @@ -1,12 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`#unit BoltProtocolV4x2 .packable() should pack not pack graph types (Node) 1`] = `"It is not allowed to pass nodes in query parameters, given: (c:a {a:"b"})"`; +exports[`#unit BoltProtocolV4x2 .packable() should resultant function not pack graph types (Node) 1`] = `"It is not allowed to pass nodes in query parameters, given: (c:a {a:"b"})"`; -exports[`#unit BoltProtocolV4x2 .packable() should pack not pack graph types (Path) 1`] = `"It is not allowed to pass paths in query parameters, given: [object Object]"`; +exports[`#unit BoltProtocolV4x2 .packable() should resultant function not pack graph types (Path) 1`] = `"It is not allowed to pass paths in query parameters, given: [object Object]"`; -exports[`#unit BoltProtocolV4x2 .packable() should pack not pack graph types (Relationship) 1`] = `"It is not allowed to pass relationships in query parameters, given: (e)-[:a {b:"c"}]->(f)"`; +exports[`#unit BoltProtocolV4x2 .packable() should resultant function not pack graph types (Relationship) 1`] = `"It is not allowed to pass relationships in query parameters, given: (e)-[:a {b:"c"}]->(f)"`; -exports[`#unit BoltProtocolV4x2 .packable() should pack not pack graph types (UnboundRelationship) 1`] = `"It is not allowed to pass unbound relationships in query parameters, given: -[:a {b:"c"}]->"`; +exports[`#unit BoltProtocolV4x2 .packable() should resultant function not pack graph types (UnboundRelationship) 1`] = `"It is not allowed to pass unbound relationships in query parameters, given: -[:a {b:"c"}]->"`; exports[`#unit BoltProtocolV4x2 .unpack() should not unpack with wrong size (Date with less fields) 1`] = `"Wrong struct size for Date, expected 1 but was 0"`; diff --git a/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v4x3.test.js.snap b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v4x3.test.js.snap index 7ab6474ed..09c64c4a4 100644 --- a/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v4x3.test.js.snap +++ b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v4x3.test.js.snap @@ -1,12 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`#unit BoltProtocolV4x3 .packable() should pack not pack graph types (Node) 1`] = `"It is not allowed to pass nodes in query parameters, given: (c:a {a:"b"})"`; +exports[`#unit BoltProtocolV4x3 .packable() should resultant function not pack graph types (Node) 1`] = `"It is not allowed to pass nodes in query parameters, given: (c:a {a:"b"})"`; -exports[`#unit BoltProtocolV4x3 .packable() should pack not pack graph types (Path) 1`] = `"It is not allowed to pass paths in query parameters, given: [object Object]"`; +exports[`#unit BoltProtocolV4x3 .packable() should resultant function not pack graph types (Path) 1`] = `"It is not allowed to pass paths in query parameters, given: [object Object]"`; -exports[`#unit BoltProtocolV4x3 .packable() should pack not pack graph types (Relationship) 1`] = `"It is not allowed to pass relationships in query parameters, given: (e)-[:a {b:"c"}]->(f)"`; +exports[`#unit BoltProtocolV4x3 .packable() should resultant function not pack graph types (Relationship) 1`] = `"It is not allowed to pass relationships in query parameters, given: (e)-[:a {b:"c"}]->(f)"`; -exports[`#unit BoltProtocolV4x3 .packable() should pack not pack graph types (UnboundRelationship) 1`] = `"It is not allowed to pass unbound relationships in query parameters, given: -[:a {b:"c"}]->"`; +exports[`#unit BoltProtocolV4x3 .packable() should resultant function not pack graph types (UnboundRelationship) 1`] = `"It is not allowed to pass unbound relationships in query parameters, given: -[:a {b:"c"}]->"`; exports[`#unit BoltProtocolV4x3 .unpack() should not unpack with wrong size (Date with less fields) 1`] = `"Wrong struct size for Date, expected 1 but was 0"`; diff --git a/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v4x4.test.js.snap b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v4x4.test.js.snap index 02f9e5ca4..a3151072f 100644 --- a/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v4x4.test.js.snap +++ b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v4x4.test.js.snap @@ -1,12 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`#unit BoltProtocolV4x4 .packable() should pack not pack graph types (Node) 1`] = `"It is not allowed to pass nodes in query parameters, given: (c:a {a:"b"})"`; +exports[`#unit BoltProtocolV4x4 .packable() should resultant function not pack graph types (Node) 1`] = `"It is not allowed to pass nodes in query parameters, given: (c:a {a:"b"})"`; -exports[`#unit BoltProtocolV4x4 .packable() should pack not pack graph types (Path) 1`] = `"It is not allowed to pass paths in query parameters, given: [object Object]"`; +exports[`#unit BoltProtocolV4x4 .packable() should resultant function not pack graph types (Path) 1`] = `"It is not allowed to pass paths in query parameters, given: [object Object]"`; -exports[`#unit BoltProtocolV4x4 .packable() should pack not pack graph types (Relationship) 1`] = `"It is not allowed to pass relationships in query parameters, given: (e)-[:a {b:"c"}]->(f)"`; +exports[`#unit BoltProtocolV4x4 .packable() should resultant function not pack graph types (Relationship) 1`] = `"It is not allowed to pass relationships in query parameters, given: (e)-[:a {b:"c"}]->(f)"`; -exports[`#unit BoltProtocolV4x4 .packable() should pack not pack graph types (UnboundRelationship) 1`] = `"It is not allowed to pass unbound relationships in query parameters, given: -[:a {b:"c"}]->"`; +exports[`#unit BoltProtocolV4x4 .packable() should resultant function not pack graph types (UnboundRelationship) 1`] = `"It is not allowed to pass unbound relationships in query parameters, given: -[:a {b:"c"}]->"`; exports[`#unit BoltProtocolV4x4 .unpack() should not unpack with wrong size (Date with less fields) 1`] = `"Wrong struct size for Date, expected 1 but was 0"`; diff --git a/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v5x0.test.js.snap b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v5x0.test.js.snap index ff7b7c350..afe8244ac 100644 --- a/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v5x0.test.js.snap +++ b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v5x0.test.js.snap @@ -1,12 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`#unit BoltProtocolV5x0 .packable() should pack not pack graph types (Node) 1`] = `"It is not allowed to pass nodes in query parameters, given: (c:a {a:"b"})"`; +exports[`#unit BoltProtocolV5x0 .packable() should resultant function not pack graph types (Node) 1`] = `"It is not allowed to pass nodes in query parameters, given: (c:a {a:"b"})"`; -exports[`#unit BoltProtocolV5x0 .packable() should pack not pack graph types (Path) 1`] = `"It is not allowed to pass paths in query parameters, given: [object Object]"`; +exports[`#unit BoltProtocolV5x0 .packable() should resultant function not pack graph types (Path) 1`] = `"It is not allowed to pass paths in query parameters, given: [object Object]"`; -exports[`#unit BoltProtocolV5x0 .packable() should pack not pack graph types (Relationship) 1`] = `"It is not allowed to pass relationships in query parameters, given: (e)-[:a {b:"c"}]->(f)"`; +exports[`#unit BoltProtocolV5x0 .packable() should resultant function not pack graph types (Relationship) 1`] = `"It is not allowed to pass relationships in query parameters, given: (e)-[:a {b:"c"}]->(f)"`; -exports[`#unit BoltProtocolV5x0 .packable() should pack not pack graph types (UnboundRelationship) 1`] = `"It is not allowed to pass unbound relationships in query parameters, given: -[:a {b:"c"}]->"`; +exports[`#unit BoltProtocolV5x0 .packable() should resultant function not pack graph types (UnboundRelationship) 1`] = `"It is not allowed to pass unbound relationships in query parameters, given: -[:a {b:"c"}]->"`; exports[`#unit BoltProtocolV5x0 .unpack() should not unpack with wrong size (Date with less fields) 1`] = `"Wrong struct size for Date, expected 1 but was 0"`; diff --git a/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v5x1.test.js.snap b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v5x1.test.js.snap index e7b86a717..440409894 100644 --- a/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v5x1.test.js.snap +++ b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v5x1.test.js.snap @@ -1,12 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`#unit BoltProtocolV5x1 .packable() should pack not pack graph types (Node) 1`] = `"It is not allowed to pass nodes in query parameters, given: (c:a {a:"b"})"`; +exports[`#unit BoltProtocolV5x1 .packable() should resultant function not pack graph types (Node) 1`] = `"It is not allowed to pass nodes in query parameters, given: (c:a {a:"b"})"`; -exports[`#unit BoltProtocolV5x1 .packable() should pack not pack graph types (Path) 1`] = `"It is not allowed to pass paths in query parameters, given: [object Object]"`; +exports[`#unit BoltProtocolV5x1 .packable() should resultant function not pack graph types (Path) 1`] = `"It is not allowed to pass paths in query parameters, given: [object Object]"`; -exports[`#unit BoltProtocolV5x1 .packable() should pack not pack graph types (Relationship) 1`] = `"It is not allowed to pass relationships in query parameters, given: (e)-[:a {b:"c"}]->(f)"`; +exports[`#unit BoltProtocolV5x1 .packable() should resultant function not pack graph types (Relationship) 1`] = `"It is not allowed to pass relationships in query parameters, given: (e)-[:a {b:"c"}]->(f)"`; -exports[`#unit BoltProtocolV5x1 .packable() should pack not pack graph types (UnboundRelationship) 1`] = `"It is not allowed to pass unbound relationships in query parameters, given: -[:a {b:"c"}]->"`; +exports[`#unit BoltProtocolV5x1 .packable() should resultant function not pack graph types (UnboundRelationship) 1`] = `"It is not allowed to pass unbound relationships in query parameters, given: -[:a {b:"c"}]->"`; exports[`#unit BoltProtocolV5x1 .unpack() should not unpack with wrong size (Date with less fields) 1`] = `"Wrong struct size for Date, expected 1 but was 0"`; diff --git a/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v5x2.test.js.snap b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v5x2.test.js.snap index 4e5af4930..435d688c9 100644 --- a/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v5x2.test.js.snap +++ b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v5x2.test.js.snap @@ -1,12 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`#unit BoltProtocolV5x2 .packable() should pack not pack graph types (Node) 1`] = `"It is not allowed to pass nodes in query parameters, given: (c:a {a:"b"})"`; +exports[`#unit BoltProtocolV5x2 .packable() should resultant function not pack graph types (Node) 1`] = `"It is not allowed to pass nodes in query parameters, given: (c:a {a:"b"})"`; -exports[`#unit BoltProtocolV5x2 .packable() should pack not pack graph types (Path) 1`] = `"It is not allowed to pass paths in query parameters, given: [object Object]"`; +exports[`#unit BoltProtocolV5x2 .packable() should resultant function not pack graph types (Path) 1`] = `"It is not allowed to pass paths in query parameters, given: [object Object]"`; -exports[`#unit BoltProtocolV5x2 .packable() should pack not pack graph types (Relationship) 1`] = `"It is not allowed to pass relationships in query parameters, given: (e)-[:a {b:"c"}]->(f)"`; +exports[`#unit BoltProtocolV5x2 .packable() should resultant function not pack graph types (Relationship) 1`] = `"It is not allowed to pass relationships in query parameters, given: (e)-[:a {b:"c"}]->(f)"`; -exports[`#unit BoltProtocolV5x2 .packable() should pack not pack graph types (UnboundRelationship) 1`] = `"It is not allowed to pass unbound relationships in query parameters, given: -[:a {b:"c"}]->"`; +exports[`#unit BoltProtocolV5x2 .packable() should resultant function not pack graph types (UnboundRelationship) 1`] = `"It is not allowed to pass unbound relationships in query parameters, given: -[:a {b:"c"}]->"`; exports[`#unit BoltProtocolV5x2 .unpack() should not unpack with wrong size (Date with less fields) 1`] = `"Wrong struct size for Date, expected 1 but was 0"`; diff --git a/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v5x3.test.js.snap b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v5x3.test.js.snap index 6cc0d1276..6c272460f 100644 --- a/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v5x3.test.js.snap +++ b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v5x3.test.js.snap @@ -1,12 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`#unit BoltProtocolV5x3 .packable() should pack not pack graph types (Node) 1`] = `"It is not allowed to pass nodes in query parameters, given: (c:a {a:"b"})"`; +exports[`#unit BoltProtocolV5x3 .packable() should resultant function not pack graph types (Node) 1`] = `"It is not allowed to pass nodes in query parameters, given: (c:a {a:"b"})"`; -exports[`#unit BoltProtocolV5x3 .packable() should pack not pack graph types (Path) 1`] = `"It is not allowed to pass paths in query parameters, given: [object Object]"`; +exports[`#unit BoltProtocolV5x3 .packable() should resultant function not pack graph types (Path) 1`] = `"It is not allowed to pass paths in query parameters, given: [object Object]"`; -exports[`#unit BoltProtocolV5x3 .packable() should pack not pack graph types (Relationship) 1`] = `"It is not allowed to pass relationships in query parameters, given: (e)-[:a {b:"c"}]->(f)"`; +exports[`#unit BoltProtocolV5x3 .packable() should resultant function not pack graph types (Relationship) 1`] = `"It is not allowed to pass relationships in query parameters, given: (e)-[:a {b:"c"}]->(f)"`; -exports[`#unit BoltProtocolV5x3 .packable() should pack not pack graph types (UnboundRelationship) 1`] = `"It is not allowed to pass unbound relationships in query parameters, given: -[:a {b:"c"}]->"`; +exports[`#unit BoltProtocolV5x3 .packable() should resultant function not pack graph types (UnboundRelationship) 1`] = `"It is not allowed to pass unbound relationships in query parameters, given: -[:a {b:"c"}]->"`; exports[`#unit BoltProtocolV5x3 .unpack() should not unpack with wrong size (Date with less fields) 1`] = `"Wrong struct size for Date, expected 1 but was 0"`; diff --git a/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v5x4.test.js.snap b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v5x4.test.js.snap new file mode 100644 index 000000000..cba7e7385 --- /dev/null +++ b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v5x4.test.js.snap @@ -0,0 +1,61 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`#unit BoltProtocolV5x4 .packable() should resultant function not pack graph types (Node) 1`] = `"It is not allowed to pass nodes in query parameters, given: (c:a {a:"b"})"`; + +exports[`#unit BoltProtocolV5x4 .packable() should resultant function not pack graph types (Path) 1`] = `"It is not allowed to pass paths in query parameters, given: [object Object]"`; + +exports[`#unit BoltProtocolV5x4 .packable() should resultant function not pack graph types (Relationship) 1`] = `"It is not allowed to pass relationships in query parameters, given: (e)-[:a {b:"c"}]->(f)"`; + +exports[`#unit BoltProtocolV5x4 .packable() should resultant function not pack graph types (UnboundRelationship) 1`] = `"It is not allowed to pass unbound relationships in query parameters, given: -[:a {b:"c"}]->"`; + +exports[`#unit BoltProtocolV5x4 .unpack() should not unpack with wrong size (Date with less fields) 1`] = `"Wrong struct size for Date, expected 1 but was 0"`; + +exports[`#unit BoltProtocolV5x4 .unpack() should not unpack with wrong size (Date with more fields) 1`] = `"Wrong struct size for Date, expected 1 but was 2"`; + +exports[`#unit BoltProtocolV5x4 .unpack() should not unpack with wrong size (DateTimeWithZoneId with less fields) 1`] = `"Wrong struct size for DateTimeWithZoneId, expected 3 but was 2"`; + +exports[`#unit BoltProtocolV5x4 .unpack() should not unpack with wrong size (DateTimeWithZoneId with more fields) 1`] = `"Wrong struct size for DateTimeWithZoneId, expected 3 but was 4"`; + +exports[`#unit BoltProtocolV5x4 .unpack() should not unpack with wrong size (DateTimeWithZoneOffset with less fields) 1`] = `"Wrong struct size for DateTimeWithZoneOffset, expected 3 but was 2"`; + +exports[`#unit BoltProtocolV5x4 .unpack() should not unpack with wrong size (DateTimeWithZoneOffset with more fields) 1`] = `"Wrong struct size for DateTimeWithZoneOffset, expected 3 but was 4"`; + +exports[`#unit BoltProtocolV5x4 .unpack() should not unpack with wrong size (Duration with less fields) 1`] = `"Wrong struct size for Duration, expected 4 but was 3"`; + +exports[`#unit BoltProtocolV5x4 .unpack() should not unpack with wrong size (Duration with more fields) 1`] = `"Wrong struct size for Duration, expected 4 but was 5"`; + +exports[`#unit BoltProtocolV5x4 .unpack() should not unpack with wrong size (LocalDateTime with less fields) 1`] = `"Wrong struct size for LocalDateTime, expected 2 but was 1"`; + +exports[`#unit BoltProtocolV5x4 .unpack() should not unpack with wrong size (LocalDateTime with more fields) 1`] = `"Wrong struct size for LocalDateTime, expected 2 but was 3"`; + +exports[`#unit BoltProtocolV5x4 .unpack() should not unpack with wrong size (LocalTime with less fields) 1`] = `"Wrong struct size for LocalTime, expected 1 but was 0"`; + +exports[`#unit BoltProtocolV5x4 .unpack() should not unpack with wrong size (LocalTime with more fields) 1`] = `"Wrong struct size for LocalTime, expected 1 but was 2"`; + +exports[`#unit BoltProtocolV5x4 .unpack() should not unpack with wrong size (Node with less fields) 1`] = `"Wrong struct size for Node, expected 4 but was 3"`; + +exports[`#unit BoltProtocolV5x4 .unpack() should not unpack with wrong size (Node with more fields) 1`] = `"Wrong struct size for Node, expected 4 but was 5"`; + +exports[`#unit BoltProtocolV5x4 .unpack() should not unpack with wrong size (Path with less fields) 1`] = `"Wrong struct size for Path, expected 3 but was 2"`; + +exports[`#unit BoltProtocolV5x4 .unpack() should not unpack with wrong size (Path with more fields) 1`] = `"Wrong struct size for Path, expected 3 but was 4"`; + +exports[`#unit BoltProtocolV5x4 .unpack() should not unpack with wrong size (Point with less fields) 1`] = `"Wrong struct size for Point2D, expected 3 but was 2"`; + +exports[`#unit BoltProtocolV5x4 .unpack() should not unpack with wrong size (Point with more fields) 1`] = `"Wrong struct size for Point2D, expected 3 but was 4"`; + +exports[`#unit BoltProtocolV5x4 .unpack() should not unpack with wrong size (Point3D with less fields) 1`] = `"Wrong struct size for Point3D, expected 4 but was 3"`; + +exports[`#unit BoltProtocolV5x4 .unpack() should not unpack with wrong size (Point3D with more fields) 1`] = `"Wrong struct size for Point3D, expected 4 but was 5"`; + +exports[`#unit BoltProtocolV5x4 .unpack() should not unpack with wrong size (Relationship with less fields) 1`] = `"Wrong struct size for Relationship, expected 8 but was 5"`; + +exports[`#unit BoltProtocolV5x4 .unpack() should not unpack with wrong size (Relationship with more fields) 1`] = `"Wrong struct size for Relationship, expected 8 but was 9"`; + +exports[`#unit BoltProtocolV5x4 .unpack() should not unpack with wrong size (Time with less fields) 1`] = `"Wrong struct size for Time, expected 2 but was 1"`; + +exports[`#unit BoltProtocolV5x4 .unpack() should not unpack with wrong size (Time with more fileds) 1`] = `"Wrong struct size for Time, expected 2 but was 3"`; + +exports[`#unit BoltProtocolV5x4 .unpack() should not unpack with wrong size (UnboundRelationship with less fields) 1`] = `"Wrong struct size for UnboundRelationship, expected 4 but was 3"`; + +exports[`#unit BoltProtocolV5x4 .unpack() should not unpack with wrong size (UnboundRelationship with more fields) 1`] = `"Wrong struct size for UnboundRelationship, expected 4 but was 5"`; diff --git a/packages/bolt-connection/test/bolt/behaviour/index.js b/packages/bolt-connection/test/bolt/behaviour/index.js index 6149bfb61..90a82b2c9 100644 --- a/packages/bolt-connection/test/bolt/behaviour/index.js +++ b/packages/bolt-connection/test/bolt/behaviour/index.js @@ -18,3 +18,4 @@ */ export * as notificationFilterBehaviour from './notification-filter' +export * as telemetryBehaviour from './telemetry' diff --git a/packages/bolt-connection/test/bolt/behaviour/telemetry.js b/packages/bolt-connection/test/bolt/behaviour/telemetry.js new file mode 100644 index 000000000..1e398c3f1 --- /dev/null +++ b/packages/bolt-connection/test/bolt/behaviour/telemetry.js @@ -0,0 +1,132 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { internal } from 'neo4j-driver-core' +import { CompletedObserver, TelemetryObserver } from '../../../src/bolt' +import utils from '../../test-utils' +import RequestMessage from '../../../src/bolt/request-message' + +const { + constants: { + TELEMETRY_APIS + } +} = internal + +/** + * Test setup for protocol versions which doesn't supports telemetry + * + * @param {function()} createProtocol + * @returns {void} + */ +export function protocolNotSupportsTelemetry (createProtocol) { + describe('.telemetry()', () => { + describe.each(telemetryApiFixture())('when called with { api= %s } and onCompleted defined', (api) => { + let onCompleted + let result + + beforeEach(() => { + onCompleted = jest.fn() + const protocol = createProtocol() + + result = protocol.telemetry({ api }, { onCompleted }) + }) + + it('should return a completed observer', () => { + expect(result).toBeInstanceOf(CompletedObserver) + }) + + it('should call onCompleted', () => { + expect(onCompleted).toHaveBeenCalledTimes(1) + }) + }) + }) +} + +/** + * + * @param {function()} createProtocol + * @returns {void} + */ +export function protocolSupportsTelemetry (createProtocol) { + describe('.telemetry()', () => { + let protocol + let recorder + + beforeEach(() => { + recorder = new utils.MessageRecordingConnection() + protocol = createProtocol(recorder) + utils.spyProtocolWrite(protocol) + }) + + describe.each(telemetryApiFixture())('when called with { api = %s }', (api) => { + describe.each([ + ['not defined', undefined], + ['empty', {}], + ['has onCompleted', { onCompleted: jest.fn() }], + ['has onError', { onError: jest.fn() }], + ['has onError and onCompleted', { onError: jest.fn(), onCompleted: jest.fn() }] + ])('and config %s', (_, config) => { + let result + + beforeEach(() => { + result = protocol.telemetry({ api }, config) + }) + + it('should write the message and correct observer and not flush it', () => { + protocol.verifyMessageCount(1) + + expect(protocol.messages[0]).toBeMessage( + RequestMessage.telemetry({ api }) + ) + + expect(protocol.observers.length).toBe(1) + + const telemetryObserver = protocol.observers[0] + expect(telemetryObserver).toBeInstanceOf(TelemetryObserver) + expect(telemetryObserver).toBe(result) + expect(protocol.flushes).toEqual([false]) + }) + + it('should notify onCompleted when completed is defined', () => { + const meta = { meta: 'data ' } + result.onCompleted(meta) + + if (config != null && config.onCompleted) { + expect(config.onCompleted).toHaveBeenCalledWith(meta) + } + }) + + it('should notify onError when error is defined', () => { + const error = new Error('something right is not wrong') + result.onError(error) + + if (config != null && config.onError) { + expect(config.onError).toHaveBeenCalledWith(error) + } + }) + }) + }) + }) +} + +export function telemetryApiFixture () { + return [ + ...Object.values(TELEMETRY_APIS) + ] +} diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v1.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v1.test.js index 51184cd65..e5619e798 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v1.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v1.test.js @@ -38,7 +38,7 @@ import utils from '../test-utils' import { LoginObserver } from '../../src/bolt/stream-observers' import { alloc } from '../../src/channel' import { structure } from '../../src/packstream' -import { notificationFilterBehaviour } from './behaviour' +import { notificationFilterBehaviour, telemetryBehaviour } from './behaviour' const WRITE = 'WRITE' @@ -407,7 +407,7 @@ describe('#unit BoltProtocolV1', () => { ['Relationship', new Relationship(1, 2, 3, 'a', { b: 'c' }, 'd', 'e', 'f')], ['UnboundRelationship', new UnboundRelationship(1, 'a', { b: 'c' }, '1')], ['Path', new Path(new Node(1, [], {}), new Node(2, [], {}), [])] - ])('should pack not pack graph types (%s)', (_, graphType) => { + ])('should resultant function not pack graph types (%s)', (_, graphType) => { const protocol = new BoltProtocolV1( new utils.MessageRecordingConnection(), null, @@ -666,6 +666,10 @@ describe('#unit BoltProtocolV1', () => { notificationFilterBehaviour.shouldNotSupportNotificationFilterOnRun(newProtocol) }) + describe('Bolt 5.4', () => { + telemetryBehaviour.protocolNotSupportsTelemetry(newProtocol) + }) + function newProtocol (recorder) { return new BoltProtocolV1(recorder, null, false, undefined, undefined, () => {}) } diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v2.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v2.test.js index 77d1a52b4..7363a3576 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v2.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v2.test.js @@ -38,7 +38,7 @@ import { import { alloc } from '../../src/channel' import { structure } from '../../src/packstream' -import { notificationFilterBehaviour } from './behaviour' +import { notificationFilterBehaviour, telemetryBehaviour } from './behaviour' describe('#unit BoltProtocolV2', () => { beforeEach(() => { @@ -161,7 +161,7 @@ describe('#unit BoltProtocolV2', () => { ['Relationship', new Relationship(1, 2, 3, 'a', { b: 'c' }, 'd', 'e', 'f')], ['UnboundRelationship', new UnboundRelationship(1, 'a', { b: 'c' }, '1')], ['Path', new Path(new Node(1, [], {}), new Node(2, [], {}), [])] - ])('should pack not pack graph types (%s)', (_, graphType) => { + ])('should resultant function not pack graph types (%s)', (_, graphType) => { const protocol = new BoltProtocolV2( new utils.MessageRecordingConnection(), null, @@ -533,6 +533,10 @@ describe('#unit BoltProtocolV2', () => { notificationFilterBehaviour.shouldNotSupportNotificationFilterOnRun(newProtocol) }) + describe('Bolt 5.4', () => { + telemetryBehaviour.protocolNotSupportsTelemetry(newProtocol) + }) + function newProtocol (recorder) { return new BoltProtocolV2(recorder, null, false, undefined, undefined, () => {}) } diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v3.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v3.test.js index 102810ee1..fe58520f8 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v3.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v3.test.js @@ -43,7 +43,7 @@ import { import { alloc } from '../../src/channel' import { structure } from '../../src/packstream' -import { notificationFilterBehaviour } from './behaviour' +import { notificationFilterBehaviour, telemetryBehaviour } from './behaviour' const { bookmarks: { Bookmarks }, @@ -365,7 +365,7 @@ describe('#unit BoltProtocolV3', () => { ['Relationship', new Relationship(1, 2, 3, 'a', { b: 'c' }, 'd', 'e', 'f')], ['UnboundRelationship', new UnboundRelationship(1, 'a', { b: 'c' }, '1')], ['Path', new Path(new Node(1, [], {}), new Node(2, [], {}), [])] - ])('should pack not pack graph types (%s)', (_, graphType) => { + ])('should resultant function not pack graph types (%s)', (_, graphType) => { const protocol = new BoltProtocolV3( new utils.MessageRecordingConnection(), null, @@ -737,6 +737,10 @@ describe('#unit BoltProtocolV3', () => { notificationFilterBehaviour.shouldNotSupportNotificationFilterOnRun(newProtocol) }) + describe('Bolt 5.4', () => { + telemetryBehaviour.protocolNotSupportsTelemetry(newProtocol) + }) + function newProtocol (recorder) { return new BoltProtocolV3(recorder, null, false, undefined, undefined, () => {}) } diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v4x0.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v4x0.test.js index 0266dde62..69a020933 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v4x0.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v4x0.test.js @@ -43,7 +43,7 @@ import { import { alloc } from '../../src/channel' import { structure } from '../../src/packstream' -import { notificationFilterBehaviour } from './behaviour' +import { notificationFilterBehaviour, telemetryBehaviour } from './behaviour' const WRITE = 'WRITE' @@ -283,7 +283,7 @@ describe('#unit BoltProtocolV4x0', () => { ['Relationship', new Relationship(1, 2, 3, 'a', { b: 'c' }, 'd', 'e', 'f')], ['UnboundRelationship', new UnboundRelationship(1, 'a', { b: 'c' }, '1')], ['Path', new Path(new Node(1, [], {}), new Node(2, [], {}), [])] - ])('should pack not pack graph types (%s)', (_, graphType) => { + ])('should resultant function not pack graph types (%s)', (_, graphType) => { const protocol = new BoltProtocolV4x0( new utils.MessageRecordingConnection(), null, @@ -629,6 +629,10 @@ describe('#unit BoltProtocolV4x0', () => { notificationFilterBehaviour.shouldNotSupportNotificationFilterOnRun(newProtocol) }) + describe('Bolt 5.4', () => { + telemetryBehaviour.protocolNotSupportsTelemetry(newProtocol) + }) + function newProtocol (recorder) { return new BoltProtocolV4x0(recorder, null, false, undefined, undefined, () => {}) } diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v4x1.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v4x1.test.js index 1da1c4093..f955d8857 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v4x1.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v4x1.test.js @@ -38,7 +38,7 @@ import { import { alloc } from '../../src/channel' import { structure } from '../../src/packstream' -import { notificationFilterBehaviour } from './behaviour' +import { notificationFilterBehaviour, telemetryBehaviour } from './behaviour' const { txConfig: { TxConfig }, @@ -162,7 +162,7 @@ describe('#unit BoltProtocolV4x1', () => { ['Relationship', new Relationship(1, 2, 3, 'a', { b: 'c' }, 'd', 'e', 'f')], ['UnboundRelationship', new UnboundRelationship(1, 'a', { b: 'c' }, '1')], ['Path', new Path(new Node(1, [], {}), new Node(2, [], {}), [])] - ])('should pack not pack graph types (%s)', (_, graphType) => { + ])('should resultant function not pack graph types (%s)', (_, graphType) => { const protocol = new BoltProtocolV4x1( new utils.MessageRecordingConnection(), null, @@ -534,6 +534,10 @@ describe('#unit BoltProtocolV4x1', () => { notificationFilterBehaviour.shouldNotSupportNotificationFilterOnRun(newProtocol) }) + describe('Bolt 5.4', () => { + telemetryBehaviour.protocolNotSupportsTelemetry(newProtocol) + }) + function newProtocol (recorder) { return new BoltProtocolV4x1(recorder, null, false, undefined, undefined, () => {}) } diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v4x2.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v4x2.test.js index 5caa1bc92..94a623f19 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v4x2.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v4x2.test.js @@ -38,7 +38,7 @@ import { import { alloc } from '../../src/channel' import { structure } from '../../src/packstream' -import { notificationFilterBehaviour } from './behaviour' +import { notificationFilterBehaviour, telemetryBehaviour } from './behaviour' const { txConfig: { TxConfig }, @@ -161,7 +161,7 @@ describe('#unit BoltProtocolV4x2', () => { ['Relationship', new Relationship(1, 2, 3, 'a', { b: 'c' }, 'd', 'e', 'f')], ['UnboundRelationship', new UnboundRelationship(1, 'a', { b: 'c' }, '1')], ['Path', new Path(new Node(1, [], {}), new Node(2, [], {}), [])] - ])('should pack not pack graph types (%s)', (_, graphType) => { + ])('should resultant function not pack graph types (%s)', (_, graphType) => { const protocol = new BoltProtocolV4x2( new utils.MessageRecordingConnection(), null, @@ -533,6 +533,10 @@ describe('#unit BoltProtocolV4x2', () => { notificationFilterBehaviour.shouldNotSupportNotificationFilterOnRun(newProtocol) }) + describe('Bolt 5.4', () => { + telemetryBehaviour.protocolNotSupportsTelemetry(newProtocol) + }) + function newProtocol (recorder) { return new BoltProtocolV4x2(recorder, null, false, undefined, undefined, () => {}) } diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v4x3.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v4x3.test.js index 53e0f9626..6a2bfd2bb 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v4x3.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v4x3.test.js @@ -40,7 +40,7 @@ import { import { alloc } from '../../src/channel' import { structure } from '../../src/packstream' import fc from 'fast-check' -import { notificationFilterBehaviour } from './behaviour' +import { notificationFilterBehaviour, telemetryBehaviour } from './behaviour' const WRITE = 'WRITE' @@ -371,7 +371,7 @@ describe('#unit BoltProtocolV4x3', () => { ['Relationship', new Relationship(1, 2, 3, 'a', { b: 'c' }, 'd', 'e', 'f')], ['UnboundRelationship', new UnboundRelationship(1, 'a', { b: 'c' }, '1')], ['Path', new Path(new Node(1, [], {}), new Node(2, [], {}), [])] - ])('should pack not pack graph types (%s)', (_, graphType) => { + ])('should resultant function not pack graph types (%s)', (_, graphType) => { const protocol = new BoltProtocolV4x3( new utils.MessageRecordingConnection(), null, @@ -1161,6 +1161,10 @@ describe('#unit BoltProtocolV4x3', () => { notificationFilterBehaviour.shouldNotSupportNotificationFilterOnRun(newProtocol) }) + describe('Bolt 5.4', () => { + telemetryBehaviour.protocolNotSupportsTelemetry(newProtocol) + }) + function newProtocol (recorder) { return new BoltProtocolV4x3(recorder, null, false, undefined, undefined, () => {}) } diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v4x4.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v4x4.test.js index 001ed288f..0f5a7aba8 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v4x4.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v4x4.test.js @@ -40,7 +40,7 @@ import { import { alloc } from '../../src/channel' import { structure } from '../../src/packstream' import fc from 'fast-check' -import { notificationFilterBehaviour } from './behaviour' +import { notificationFilterBehaviour, telemetryBehaviour } from './behaviour' const WRITE = 'WRITE' @@ -404,7 +404,7 @@ describe('#unit BoltProtocolV4x4', () => { ['Relationship', new Relationship(1, 2, 3, 'a', { b: 'c' }, 'd', 'e', 'f')], ['UnboundRelationship', new UnboundRelationship(1, 'a', { b: 'c' }, '1')], ['Path', new Path(new Node(1, [], {}), new Node(2, [], {}), [])] - ])('should pack not pack graph types (%s)', (_, graphType) => { + ])('should resultant function not pack graph types (%s)', (_, graphType) => { const protocol = new BoltProtocolV4x4( new utils.MessageRecordingConnection(), null, @@ -1201,6 +1201,10 @@ describe('#unit BoltProtocolV4x4', () => { notificationFilterBehaviour.shouldNotSupportNotificationFilterOnRun(newProtocol) }) + describe('Bolt 5.4', () => { + telemetryBehaviour.protocolNotSupportsTelemetry(newProtocol) + }) + function newProtocol (recorder) { return new BoltProtocolV4x4(recorder, null, false, undefined, undefined, () => {}) } diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v5x0.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v5x0.test.js index 6c4923e7e..600be7b7f 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v5x0.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v5x0.test.js @@ -40,7 +40,7 @@ import { } from 'neo4j-driver-core' import { alloc } from '../../src/channel' -import { notificationFilterBehaviour } from './behaviour' +import { notificationFilterBehaviour, telemetryBehaviour } from './behaviour' const WRITE = 'WRITE' @@ -416,7 +416,7 @@ describe('#unit BoltProtocolV5x0', () => { ['Relationship', new Relationship(1, 2, 3, 'a', { b: 'c' }, 'd', 'e', 'f')], ['UnboundRelationship', new UnboundRelationship(1, 'a', { b: 'c' }, '1')], ['Path', new Path(new Node(1, [], {}), new Node(2, [], {}), [])] - ])('should pack not pack graph types (%s)', (_, graphType) => { + ])('should resultant function not pack graph types (%s)', (_, graphType) => { const protocol = new BoltProtocolV5x0( new utils.MessageRecordingConnection(), null, @@ -1100,6 +1100,10 @@ describe('#unit BoltProtocolV5x0', () => { notificationFilterBehaviour.shouldNotSupportNotificationFilterOnRun(newProtocol) }) + describe('Bolt 5.4', () => { + telemetryBehaviour.protocolNotSupportsTelemetry(newProtocol) + }) + function newProtocol (recorder) { return new BoltProtocolV5x0(recorder, null, false, undefined, undefined, () => {}) } diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v5x1.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v5x1.test.js index fcb438641..3ad634748 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v5x1.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v5x1.test.js @@ -40,7 +40,7 @@ import { } from 'neo4j-driver-core' import { alloc } from '../../src/channel' -import { notificationFilterBehaviour } from './behaviour' +import { notificationFilterBehaviour, telemetryBehaviour } from './behaviour' const WRITE = 'WRITE' @@ -493,7 +493,7 @@ describe('#unit BoltProtocolV5x1', () => { ['Relationship', new Relationship(1, 2, 3, 'a', { b: 'c' }, 'd', 'e', 'f')], ['UnboundRelationship', new UnboundRelationship(1, 'a', { b: 'c' }, '1')], ['Path', new Path(new Node(1, [], {}), new Node(2, [], {}), [])] - ])('should pack not pack graph types (%s)', (_, graphType) => { + ])('should resultant function not pack graph types (%s)', (_, graphType) => { const protocol = new BoltProtocolV5x1( new utils.MessageRecordingConnection(), null, @@ -1142,6 +1142,10 @@ describe('#unit BoltProtocolV5x1', () => { notificationFilterBehaviour.shouldNotSupportNotificationFilterOnRun(newProtocol) }) + describe('Bolt 5.4', () => { + telemetryBehaviour.protocolNotSupportsTelemetry(newProtocol) + }) + function newProtocol (recorder) { return new BoltProtocolV5x1(recorder, null, false, undefined, undefined, () => {}) } diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v5x2.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v5x2.test.js index bf78f10e0..269c8b891 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v5x2.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v5x2.test.js @@ -40,7 +40,7 @@ import { } from 'neo4j-driver-core' import { alloc } from '../../src/channel' -import { notificationFilterBehaviour } from './behaviour' +import { notificationFilterBehaviour, telemetryBehaviour } from './behaviour' const WRITE = 'WRITE' @@ -499,7 +499,7 @@ describe('#unit BoltProtocolV5x2', () => { ['Relationship', new Relationship(1, 2, 3, 'a', { b: 'c' }, 'd', 'e', 'f')], ['UnboundRelationship', new UnboundRelationship(1, 'a', { b: 'c' }, '1')], ['Path', new Path(new Node(1, [], {}), new Node(2, [], {}), [])] - ])('should pack not pack graph types (%s)', (_, graphType) => { + ])('should resultant function not pack graph types (%s)', (_, graphType) => { const protocol = new BoltProtocolV5x2( new utils.MessageRecordingConnection(), null, @@ -1140,6 +1140,10 @@ describe('#unit BoltProtocolV5x2', () => { const unpacked = protocol.unpack(buffer) expect(unpacked).toEqual(struct) }) + + describe('Bolt 5.4', () => { + telemetryBehaviour.protocolNotSupportsTelemetry(newProtocol) + }) }) function newProtocol (recorder) { diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v5x3.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v5x3.test.js index 1c16a971c..e0c6fed66 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v5x3.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v5x3.test.js @@ -40,7 +40,7 @@ import { } from 'neo4j-driver-core' import { alloc } from '../../src/channel' -import { notificationFilterBehaviour } from './behaviour' +import { notificationFilterBehaviour, telemetryBehaviour } from './behaviour' const WRITE = 'WRITE' @@ -508,7 +508,7 @@ describe('#unit BoltProtocolV5x3', () => { ['Relationship', new Relationship(1, 2, 3, 'a', { b: 'c' }, 'd', 'e', 'f')], ['UnboundRelationship', new UnboundRelationship(1, 'a', { b: 'c' }, '1')], ['Path', new Path(new Node(1, [], {}), new Node(2, [], {}), [])] - ])('should pack not pack graph types (%s)', (_, graphType) => { + ])('should resultant function not pack graph types (%s)', (_, graphType) => { const protocol = new BoltProtocolV5x3( new utils.MessageRecordingConnection(), null, @@ -1149,6 +1149,10 @@ describe('#unit BoltProtocolV5x3', () => { const unpacked = protocol.unpack(buffer) expect(unpacked).toEqual(struct) }) + + describe('Bolt 5.4', () => { + telemetryBehaviour.protocolNotSupportsTelemetry(newProtocol) + }) }) function newProtocol (recorder) { diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v5x4.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v5x4.test.js new file mode 100644 index 000000000..205220fdd --- /dev/null +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v5x4.test.js @@ -0,0 +1,1159 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import BoltProtocolV5x4 from '../../src/bolt/bolt-protocol-v5x4' +import RequestMessage from '../../src/bolt/request-message' +import { v2, structure } from '../../src/packstream' +import utils from '../test-utils' +import { LoginObserver, RouteObserver } from '../../src/bolt/stream-observers' +import fc from 'fast-check' +import { + Date, + DateTime, + Duration, + LocalDateTime, + LocalTime, + Path, + PathSegment, + Point, + Relationship, + Time, + UnboundRelationship, + Node, + internal +} from 'neo4j-driver-core' + +import { alloc } from '../../src/channel' +import { notificationFilterBehaviour, telemetryBehaviour } from './behaviour' + +const WRITE = 'WRITE' + +const { + txConfig: { TxConfig }, + bookmarks: { Bookmarks }, + logger: { Logger }, + temporalUtil +} = internal + +describe('#unit BoltProtocolV5x4', () => { + beforeEach(() => { + expect.extend(utils.matchers) + }) + + telemetryBehaviour.protocolSupportsTelemetry(newProtocol) + + it('should request routing information', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x4(recorder, null, false) + utils.spyProtocolWrite(protocol) + const routingContext = { someContextParam: 'value' } + const databaseName = 'name' + + const observer = protocol.requestRoutingInformation({ + routingContext, + databaseName + }) + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.routeV4x4(routingContext, [], { databaseName, impersonatedUser: null }) + ) + expect(protocol.observers).toEqual([observer]) + expect(observer).toEqual(expect.any(RouteObserver)) + expect(protocol.flushes).toEqual([true]) + }) + + it('should request routing information sending bookmarks', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x4(recorder, null, false) + utils.spyProtocolWrite(protocol) + const routingContext = { someContextParam: 'value' } + const listOfBookmarks = ['a', 'b', 'c'] + const bookmarks = new Bookmarks(listOfBookmarks) + const databaseName = 'name' + + const observer = protocol.requestRoutingInformation({ + routingContext, + databaseName, + sessionContext: { bookmarks } + }) + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.routeV4x4(routingContext, listOfBookmarks, { databaseName, impersonatedUser: null }) + ) + expect(protocol.observers).toEqual([observer]) + expect(observer).toEqual(expect.any(RouteObserver)) + expect(protocol.flushes).toEqual([true]) + }) + + it('should run a query', () => { + const database = 'testdb' + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx2' + ]) + const txConfig = new TxConfig({ + timeout: 5000, + metadata: { x: 1, y: 'something' } + }) + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x4(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const query = 'RETURN $x, $y' + const parameters = { x: 'x', y: 'y' } + + const observer = protocol.run(query, parameters, { + bookmarks, + txConfig, + database, + mode: WRITE + }) + + protocol.verifyMessageCount(2) + + expect(protocol.messages[0]).toBeMessage( + RequestMessage.runWithMetadata(query, parameters, { + bookmarks, + txConfig, + database, + mode: WRITE + }) + ) + expect(protocol.messages[1]).toBeMessage(RequestMessage.pull()) + expect(protocol.observers).toEqual([observer, observer]) + expect(protocol.flushes).toEqual([false, true]) + }) + + it('should run a with impersonated user', () => { + const database = 'testdb' + const impersonatedUser = 'the impostor' + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx2' + ]) + const txConfig = new TxConfig({ + timeout: 5000, + metadata: { x: 1, y: 'something' } + }) + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x4(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const query = 'RETURN $x, $y' + const parameters = { x: 'x', y: 'y' } + + const observer = protocol.run(query, parameters, { + bookmarks, + txConfig, + database, + mode: WRITE, + impersonatedUser + }) + + protocol.verifyMessageCount(2) + + expect(protocol.messages[0]).toBeMessage( + RequestMessage.runWithMetadata(query, parameters, { + bookmarks, + txConfig, + database, + mode: WRITE, + impersonatedUser + }) + ) + expect(protocol.messages[1]).toBeMessage(RequestMessage.pull()) + expect(protocol.observers).toEqual([observer, observer]) + expect(protocol.flushes).toEqual([false, true]) + }) + + it('should begin a transaction', () => { + const database = 'testdb' + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx2' + ]) + const txConfig = new TxConfig({ + timeout: 5000, + metadata: { x: 1, y: 'something' } + }) + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x4(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const observer = protocol.beginTransaction({ + bookmarks, + txConfig, + database, + mode: WRITE + }) + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.begin({ bookmarks, txConfig, database, mode: WRITE }) + ) + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([true]) + }) + + it('should begin a transaction with impersonated user', () => { + const database = 'testdb' + const impersonatedUser = 'the impostor' + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx2' + ]) + const txConfig = new TxConfig({ + timeout: 5000, + metadata: { x: 1, y: 'something' } + }) + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x4(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const observer = protocol.beginTransaction({ + bookmarks, + txConfig, + database, + mode: WRITE, + impersonatedUser + }) + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.begin({ bookmarks, txConfig, database, mode: WRITE, impersonatedUser }) + ) + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([true]) + }) + + it('should return correct bolt version number', () => { + const protocol = new BoltProtocolV5x4(null, null, false) + + expect(protocol.version).toBe(5.4) + }) + + it('should update metadata', () => { + const metadata = { t_first: 1, t_last: 2, db_hits: 3, some_other_key: 4 } + const protocol = new BoltProtocolV5x4(null, null, false) + + const transformedMetadata = protocol.transformMetadata(metadata) + + expect(transformedMetadata).toEqual({ + result_available_after: 1, + result_consumed_after: 2, + db_hits: 3, + some_other_key: 4 + }) + }) + + it('should initialize connection', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x4(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const clientName = 'js-driver/1.2.3' + const boltAgent = { + product: 'neo4j-javascript/5.6', + platform: 'netbsd 1.1.1; Some arch', + languageDetails: 'Node/16.0.1 (v8 1.7.0)' + } + const authToken = { username: 'neo4j', password: 'secret' } + + const observer = protocol.initialize({ userAgent: clientName, boltAgent, authToken }) + + protocol.verifyMessageCount(2) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.hello5x3(clientName, boltAgent) + ) + expect(protocol.messages[1]).toBeMessage( + RequestMessage.logon(authToken) + ) + + expect(protocol.observers.length).toBe(2) + + // hello observer + const helloObserver = protocol.observers[0] + expect(helloObserver).toBeInstanceOf(LoginObserver) + expect(helloObserver).not.toBe(observer) + + // login observer + const loginObserver = protocol.observers[1] + expect(loginObserver).toBeInstanceOf(LoginObserver) + expect(loginObserver).toBe(observer) + + expect(protocol.flushes).toEqual([false, true]) + }) + + it.each([ + 'javascript-driver/5.5.0', + '', + undefined, + null + ])('should always use the user agent set by the user', (userAgent) => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x4(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const boltAgent = { + product: 'neo4j-javascript/5.6', + platform: 'netbsd 1.1.1; Some arch', + languageDetails: 'Node/16.0.1 (v8 1.7.0)' + } + const authToken = { username: 'neo4j', password: 'secret' } + + const observer = protocol.initialize({ userAgent, boltAgent, authToken }) + + protocol.verifyMessageCount(2) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.hello5x3(userAgent, boltAgent) + ) + expect(protocol.messages[1]).toBeMessage( + RequestMessage.logon(authToken) + ) + + expect(protocol.observers.length).toBe(2) + + // hello observer + const helloObserver = protocol.observers[0] + expect(helloObserver).toBeInstanceOf(LoginObserver) + expect(helloObserver).not.toBe(observer) + + // login observer + const loginObserver = protocol.observers[1] + expect(loginObserver).toBeInstanceOf(LoginObserver) + expect(loginObserver).toBe(observer) + + expect(protocol.flushes).toEqual([false, true]) + }) + + it.each( + [true, false] + )('should logon to the server [flush=%s]', (flush) => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x4(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const authToken = { username: 'neo4j', password: 'secret' } + + const observer = protocol.logon({ authToken, flush }) + + protocol.verifyMessageCount(1) + + expect(protocol.messages[0]).toBeMessage( + RequestMessage.logon(authToken) + ) + + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([flush]) + }) + + it.each( + [true, false] + )('should logoff from the server [flush=%s]', (flush) => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x4(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const observer = protocol.logoff({ flush }) + + protocol.verifyMessageCount(1) + + expect(protocol.messages[0]).toBeMessage( + RequestMessage.logoff() + ) + + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([flush]) + }) + + it('should begin a transaction', () => { + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx2' + ]) + const txConfig = new TxConfig({ + timeout: 5000, + metadata: { x: 1, y: 'something' } + }) + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x4(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const observer = protocol.beginTransaction({ + bookmarks, + txConfig, + mode: WRITE + }) + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.begin({ bookmarks, txConfig, mode: WRITE }) + ) + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([true]) + }) + + it('should commit', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x4(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const observer = protocol.commitTransaction() + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage(RequestMessage.commit()) + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([true]) + }) + + it('should rollback', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x4(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const observer = protocol.rollbackTransaction() + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage(RequestMessage.rollback()) + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([true]) + }) + + it('should support logoff', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x4(recorder, null, false) + + expect(protocol.supportsReAuth).toBe(true) + }) + + describe('unpacker configuration', () => { + test.each([ + [false, false], + [false, true], + [true, false], + [true, true] + ])( + 'should create unpacker with disableLosslessIntegers=%p and useBigInt=%p', + (disableLosslessIntegers, useBigInt) => { + const protocol = new BoltProtocolV5x4(null, null, { + disableLosslessIntegers, + useBigInt + }) + expect(protocol._unpacker._disableLosslessIntegers).toBe( + disableLosslessIntegers + ) + expect(protocol._unpacker._useBigInt).toBe(useBigInt) + } + ) + }) + + describe('notificationFilter', () => { + notificationFilterBehaviour.shouldSupportNotificationFilterOnInitialize(newProtocol) + notificationFilterBehaviour.shouldSupportNotificationFilterOnBeginTransaction(newProtocol) + notificationFilterBehaviour.shouldSupportNotificationFilterOnRun(newProtocol) + }) + + describe('watermarks', () => { + it('.run() should configure watermarks', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = utils.spyProtocolWrite( + new BoltProtocolV5x4(recorder, null, false) + ) + + const query = 'RETURN $x, $y' + const parameters = { x: 'x', y: 'y' } + const observer = protocol.run(query, parameters, { + bookmarks: Bookmarks.empty(), + txConfig: TxConfig.empty(), + lowRecordWatermark: 100, + highRecordWatermark: 200 + }) + + expect(observer._lowRecordWatermark).toEqual(100) + expect(observer._highRecordWatermark).toEqual(200) + }) + }) + + describe('packstream', () => { + it('should configure v2 packer', () => { + const protocol = new BoltProtocolV5x4(null, null, false) + expect(protocol.packer()).toBeInstanceOf(v2.Packer) + }) + + it('should configure v2 unpacker', () => { + const protocol = new BoltProtocolV5x4(null, null, false) + expect(protocol.unpacker()).toBeInstanceOf(v2.Unpacker) + }) + }) + + describe('.packable()', () => { + it.each([ + ['Node', new Node(1, ['a'], { a: 'b' }, 'c')], + ['Relationship', new Relationship(1, 2, 3, 'a', { b: 'c' }, 'd', 'e', 'f')], + ['UnboundRelationship', new UnboundRelationship(1, 'a', { b: 'c' }, '1')], + ['Path', new Path(new Node(1, [], {}), new Node(2, [], {}), [])] + ])('should resultant function not pack graph types (%s)', (_, graphType) => { + const protocol = new BoltProtocolV5x4( + new utils.MessageRecordingConnection(), + null, + false + ) + + const packable = protocol.packable(graphType) + + expect(packable).toThrowErrorMatchingSnapshot() + }) + + it.each([ + ['Duration', new Duration(1, 1, 1, 1)], + ['LocalTime', new LocalTime(1, 1, 1, 1)], + ['Time', new Time(1, 1, 1, 1, 1)], + ['Date', new Date(1, 1, 1)], + ['LocalDateTime', new LocalDateTime(1, 1, 1, 1, 1, 1, 1)], + [ + 'DateTimeWithZoneOffset', + new DateTime(2022, 6, 14, 15, 21, 18, 183_000_000, 120 * 60) + ], + [ + 'DateTimeWithZoneOffset / 1978', + new DateTime(1978, 12, 16, 10, 5, 59, 128000987, -150 * 60) + ], + [ + 'DateTimeWithZoneId / Berlin 2:30 CET', + new DateTime(2022, 10, 30, 2, 30, 0, 183_000_000, 2 * 60 * 60, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Berlin 2:30 CEST', + new DateTime(2022, 10, 30, 2, 30, 0, 183_000_000, 1 * 60 * 60, 'Europe/Berlin') + ], + ['Point2D', new Point(1, 1, 1)], + ['Point3D', new Point(1, 1, 1, 1)] + ])('should pack spatial types and temporal types (%s)', (_, object) => { + const buffer = alloc(256) + const protocol = new BoltProtocolV5x4( + new utils.MessageRecordingConnection(), + buffer, + { + disableLosslessIntegers: true + } + ) + + const packable = protocol.packable(object) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + + expect(unpacked).toEqual(object) + }) + + it.each([ + [ + 'DateTimeWithZoneId / Australia', + new DateTime(2022, 6, 15, 15, 21, 18, 183_000_000, undefined, 'Australia/Eucla') + ], + [ + 'DateTimeWithZoneId', + new DateTime(2022, 6, 22, 15, 21, 18, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just before turn CEST', + new DateTime(2022, 3, 27, 1, 59, 59, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just 1 before turn CEST', + new DateTime(2022, 3, 27, 0, 59, 59, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just after turn CEST', + new DateTime(2022, 3, 27, 3, 0, 0, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just 1 after turn CEST', + new DateTime(2022, 3, 27, 4, 0, 0, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just before turn CET', + new DateTime(2022, 10, 30, 2, 59, 59, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just 1 before turn CET', + new DateTime(2022, 10, 30, 1, 59, 59, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just after turn CET', + new DateTime(2022, 10, 30, 3, 0, 0, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just 1 after turn CET', + new DateTime(2022, 10, 30, 4, 0, 0, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just before turn summer time', + new DateTime(2018, 11, 4, 11, 59, 59, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just 1 before turn summer time', + new DateTime(2018, 11, 4, 10, 59, 59, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just after turn summer time', + new DateTime(2018, 11, 5, 1, 0, 0, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just 1 after turn summer time', + new DateTime(2018, 11, 5, 2, 0, 0, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just before turn winter time', + new DateTime(2019, 2, 17, 11, 59, 59, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just 1 before turn winter time', + new DateTime(2019, 2, 17, 10, 59, 59, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just after turn winter time', + new DateTime(2019, 2, 18, 0, 0, 0, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just 1 after turn winter time', + new DateTime(2019, 2, 18, 1, 0, 0, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Istanbul', + new DateTime(1978, 12, 16, 12, 35, 59, 128000987, undefined, 'Europe/Istanbul') + ], + [ + 'DateTimeWithZoneId / Istanbul', + new DateTime(2020, 6, 15, 4, 30, 0, 183_000_000, undefined, 'Pacific/Honolulu') + ], + [ + 'DateWithWithZoneId / Berlin before common era', + new DateTime(-2020, 6, 15, 4, 30, 0, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateWithWithZoneId / Max Date', + new DateTime(99_999, 12, 31, 23, 59, 59, 999_999_999, undefined, 'Pacific/Kiritimati') + ], + [ + 'DateWithWithZoneId / Min Date', + new DateTime(-99_999, 12, 31, 23, 59, 59, 999_999_999, undefined, 'Pacific/Samoa') + ], + [ + 'DateWithWithZoneId / Ambiguous date between 00 and 99', + new DateTime(50, 12, 31, 23, 59, 59, 999_999_999, undefined, 'Pacific/Samoa') + ] + ])('should pack and unpack DateTimeWithZoneId and without offset (%s)', (_, object) => { + const buffer = alloc(256) + const loggerFunction = jest.fn() + const protocol = new BoltProtocolV5x4( + new utils.MessageRecordingConnection(), + buffer, + { + disableLosslessIntegers: true + }, + undefined, + new Logger('debug', loggerFunction) + ) + + const packable = protocol.packable(object) + + expect(packable).not.toThrow() + expect(loggerFunction) + .toBeCalledWith('warn', + 'DateTime objects without "timeZoneOffsetSeconds" property ' + + 'are prune to bugs related to ambiguous times. For instance, ' + + '2022-10-30T2:30:00[Europe/Berlin] could be GMT+1 or GMT+2.') + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + + expect(unpacked.timeZoneOffsetSeconds).toBeDefined() + + const unpackedDateTimeWithoutOffset = new DateTime( + unpacked.year, + unpacked.month, + unpacked.day, + unpacked.hour, + unpacked.minute, + unpacked.second, + unpacked.nanosecond, + undefined, + unpacked.timeZoneId + ) + + expect(unpackedDateTimeWithoutOffset).toEqual(object) + }) + + it('should pack and unpack DateTimeWithOffset', () => { + fc.assert( + fc.property( + fc.date({ + min: temporalUtil.newDate(utils.MIN_UTC_IN_MS + utils.ONE_DAY_IN_MS), + max: temporalUtil.newDate(utils.MAX_UTC_IN_MS - utils.ONE_DAY_IN_MS) + }), + fc.integer({ min: 0, max: 999_999 }), + utils.arbitraryTimeZoneId(), + (date, nanoseconds, timeZoneId) => { + const object = new DateTime( + date.getUTCFullYear(), + date.getUTCMonth() + 1, + date.getUTCDate(), + date.getUTCHours(), + date.getUTCMinutes(), + date.getUTCSeconds(), + date.getUTCMilliseconds() * 1_000_000 + nanoseconds, + undefined, + timeZoneId + ) + const buffer = alloc(256) + const loggerFunction = jest.fn() + const protocol = new BoltProtocolV5x4( + new utils.MessageRecordingConnection(), + buffer, + { + disableLosslessIntegers: true + }, + undefined, + new Logger('debug', loggerFunction) + ) + + const packable = protocol.packable(object) + + expect(packable).not.toThrow() + expect(loggerFunction) + .toBeCalledWith('warn', + 'DateTime objects without "timeZoneOffsetSeconds" property ' + + 'are prune to bugs related to ambiguous times. For instance, ' + + '2022-10-30T2:30:00[Europe/Berlin] could be GMT+1 or GMT+2.') + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + + expect(unpacked.timeZoneOffsetSeconds).toBeDefined() + + const unpackedDateTimeWithoutOffset = new DateTime( + unpacked.year, + unpacked.month, + unpacked.day, + unpacked.hour, + unpacked.minute, + unpacked.second, + unpacked.nanosecond, + undefined, + unpacked.timeZoneId + ) + + expect(unpackedDateTimeWithoutOffset).toEqual(object) + }) + ) + }) + + it('should pack and unpack DateTimeWithZoneIdAndNoOffset', () => { + fc.assert( + fc.property(fc.date(), date => { + const object = DateTime.fromStandardDate(date) + const buffer = alloc(256) + const loggerFunction = jest.fn() + const protocol = new BoltProtocolV5x4( + new utils.MessageRecordingConnection(), + buffer, + { + disableLosslessIntegers: true + }, + undefined, + new Logger('debug', loggerFunction) + ) + + const packable = protocol.packable(object) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + + expect(unpacked.timeZoneOffsetSeconds).toBeDefined() + + expect(unpacked).toEqual(object) + }) + ) + }) + }) + + describe('.unpack()', () => { + it.each([ + [ + 'Node', + new structure.Structure(0x4e, [1, ['a'], { c: 'd' }, 'elementId']), + new Node(1, ['a'], { c: 'd' }, 'elementId') + ], + [ + 'Relationship', + new structure.Structure(0x52, [1, 2, 3, '4', { 5: 6 }, 'elementId', 'node1', 'node2']), + new Relationship(1, 2, 3, '4', { 5: 6 }, 'elementId', 'node1', 'node2') + ], + [ + 'UnboundRelationship', + new structure.Structure(0x72, [1, '2', { 3: 4 }, 'elementId']), + new UnboundRelationship(1, '2', { 3: 4 }, 'elementId') + ], + [ + 'Path', + new structure.Structure( + 0x50, + [ + [ + new structure.Structure(0x4e, [1, ['2'], { 3: '4' }, 'node1']), + new structure.Structure(0x4e, [4, ['5'], { 6: 7 }, 'node2']), + new structure.Structure(0x4e, [2, ['3'], { 4: '5' }, 'node3']) + ], + [ + new structure.Structure(0x52, [3, 1, 4, 'reltype1', { 4: '5' }, 'rel1', 'node1', 'node2']), + new structure.Structure(0x52, [5, 4, 2, 'reltype2', { 6: 7 }, 'rel2', 'node2', 'node3']) + ], + [1, 1, 2, 2] + ] + ), + new Path( + new Node(1, ['2'], { 3: '4' }, 'node1'), + new Node(2, ['3'], { 4: '5' }, 'node3'), + [ + new PathSegment( + new Node(1, ['2'], { 3: '4' }, 'node1'), + new Relationship(3, 1, 4, 'reltype1', { 4: '5' }, 'rel1', 'node1', 'node2'), + new Node(4, ['5'], { 6: 7 }, 'node2') + ), + new PathSegment( + new Node(4, ['5'], { 6: 7 }, 'node2'), + new Relationship(5, 4, 2, 'reltype2', { 6: 7 }, 'rel2', 'node2', 'node3'), + new Node(2, ['3'], { 4: '5' }, 'node3') + ) + ] + ) + ] + ])('should unpack graph types (%s)', (_, struct, graphObject) => { + const buffer = alloc(256) + const protocol = new BoltProtocolV5x4( + new utils.MessageRecordingConnection(), + buffer, + false + ) + + const packable = protocol.packable(struct) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + expect(unpacked).toEqual(graphObject) + }) + + it.each([ + [ + 'Node with less fields', + new structure.Structure(0x4e, [1, ['a'], { c: 'd' }]) + ], + [ + 'Node with more fields', + new structure.Structure(0x4e, [1, ['a'], { c: 'd' }, '1', 'b']) + ], + [ + 'Relationship with less fields', + new structure.Structure(0x52, [1, 2, 3, '4', { 5: 6 }]) + ], + [ + 'Relationship with more fields', + new structure.Structure(0x52, [1, 2, 3, '4', { 5: 6 }, '1', '2', '3', '4']) + ], + [ + 'UnboundRelationship with less fields', + new structure.Structure(0x72, [1, '2', { 3: 4 }]) + ], + [ + 'UnboundRelationship with more fields', + new structure.Structure(0x72, [1, '2', { 3: 4 }, '1', '2']) + ], + [ + 'Path with less fields', + new structure.Structure( + 0x50, + [ + [ + new structure.Structure(0x4e, [1, ['2'], { 3: '4' }]), + new structure.Structure(0x4e, [4, ['5'], { 6: 7 }]), + new structure.Structure(0x4e, [2, ['3'], { 4: '5' }]) + ], + [ + new structure.Structure(0x52, [3, 1, 4, 'rel1', { 4: '5' }]), + new structure.Structure(0x52, [5, 4, 2, 'rel2', { 6: 7 }]) + ] + ] + ) + ], + [ + 'Path with more fields', + new structure.Structure( + 0x50, + [ + [ + new structure.Structure(0x4e, [1, ['2'], { 3: '4' }]), + new structure.Structure(0x4e, [4, ['5'], { 6: 7 }]), + new structure.Structure(0x4e, [2, ['3'], { 4: '5' }]) + ], + [ + new structure.Structure(0x52, [3, 1, 4, 'rel1', { 4: '5' }]), + new structure.Structure(0x52, [5, 4, 2, 'rel2', { 6: 7 }]) + ], + [1, 1, 2, 2], + 'a' + ] + ) + ], + [ + 'Point with less fields', + new structure.Structure(0x58, [1, 2]) + ], + [ + 'Point with more fields', + new structure.Structure(0x58, [1, 2, 3, 4]) + ], + [ + 'Point3D with less fields', + new structure.Structure(0x59, [1, 2, 3]) + ], + + [ + 'Point3D with more fields', + new structure.Structure(0x59, [1, 2, 3, 4, 6]) + ], + [ + 'Duration with less fields', + new structure.Structure(0x45, [1, 2, 3]) + ], + [ + 'Duration with more fields', + new structure.Structure(0x45, [1, 2, 3, 4, 5]) + ], + [ + 'LocalTime with less fields', + new structure.Structure(0x74, []) + ], + [ + 'LocalTime with more fields', + new structure.Structure(0x74, [1, 2]) + ], + [ + 'Time with less fields', + new structure.Structure(0x54, [1]) + ], + [ + 'Time with more fileds', + new structure.Structure(0x54, [1, 2, 3]) + ], + [ + 'Date with less fields', + new structure.Structure(0x44, []) + ], + [ + 'Date with more fields', + new structure.Structure(0x44, [1, 2]) + ], + [ + 'LocalDateTime with less fields', + new structure.Structure(0x64, [1]) + ], + [ + 'LocalDateTime with more fields', + new structure.Structure(0x64, [1, 2, 3]) + ], + [ + 'DateTimeWithZoneOffset with less fields', + new structure.Structure(0x49, [1, 2]) + ], + [ + 'DateTimeWithZoneOffset with more fields', + new structure.Structure(0x49, [1, 2, 3, 4]) + ], + [ + 'DateTimeWithZoneId with less fields', + new structure.Structure(0x69, [1, 2]) + ], + [ + 'DateTimeWithZoneId with more fields', + new structure.Structure(0x69, [1, 2, 'America/Sao Paulo', 'Brasil']) + ] + ])('should not unpack with wrong size (%s)', (_, struct) => { + const buffer = alloc(256) + const protocol = new BoltProtocolV5x4( + new utils.MessageRecordingConnection(), + buffer, + false + ) + + const packable = protocol.packable(struct) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + expect(() => unpacked instanceof structure.Structure).toThrowErrorMatchingSnapshot() + }) + + it.each([ + [ + 'Point', + new structure.Structure(0x58, [1, 2, 3]), + new Point(1, 2, 3) + ], + [ + 'Point3D', + new structure.Structure(0x59, [1, 2, 3, 4]), + new Point(1, 2, 3, 4) + ], + [ + 'Duration', + new structure.Structure(0x45, [1, 2, 3, 4]), + new Duration(1, 2, 3, 4) + ], + [ + 'LocalTime', + new structure.Structure(0x74, [1]), + new LocalTime(0, 0, 0, 1) + ], + [ + 'Time', + new structure.Structure(0x54, [1, 2]), + new Time(0, 0, 0, 1, 2) + ], + [ + 'Date', + new structure.Structure(0x44, [1]), + new Date(1970, 1, 2) + ], + [ + 'LocalDateTime', + new structure.Structure(0x64, [1, 2]), + new LocalDateTime(1970, 1, 1, 0, 0, 1, 2) + ], + [ + 'DateTimeWithZoneOffset', + new structure.Structure(0x49, [ + 1655212878, 183_000_000, 120 * 60 + ]), + new DateTime(2022, 6, 14, 15, 21, 18, 183_000_000, 120 * 60) + ], + [ + 'DateTimeWithZoneOffset / 1978', + new structure.Structure(0x49, [ + 282659759, 128000987, -150 * 60 + ]), + new DateTime(1978, 12, 16, 10, 5, 59, 128000987, -150 * 60) + ], + [ + 'DateTimeWithZoneId', + new structure.Structure(0x69, [ + 1655212878, 183_000_000, 'Europe/Berlin' + ]), + new DateTime(2022, 6, 14, 15, 21, 18, 183_000_000, 2 * 60 * 60, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Australia', + new structure.Structure(0x69, [ + 1655212878, 183_000_000, 'Australia/Eucla' + ]), + new DateTime(2022, 6, 14, 22, 6, 18, 183_000_000, 8 * 60 * 60 + 45 * 60, 'Australia/Eucla') + ], + [ + 'DateTimeWithZoneId / Honolulu', + new structure.Structure(0x69, [ + 1592231400, 183_000_000, 'Pacific/Honolulu' + ]), + new DateTime(2020, 6, 15, 4, 30, 0, 183_000_000, -10 * 60 * 60, 'Pacific/Honolulu') + ], + [ + 'DateTimeWithZoneId / Midnight', + new structure.Structure(0x69, [ + 1685397950, 183_000_000, 'Europe/Berlin' + ]), + new DateTime(2023, 5, 30, 0, 5, 50, 183_000_000, 2 * 60 * 60, 'Europe/Berlin') + ] + ])('should unpack spatial types and temporal types (%s)', (_, struct, object) => { + const buffer = alloc(256) + const protocol = new BoltProtocolV5x4( + new utils.MessageRecordingConnection(), + buffer, + { + disableLosslessIntegers: true + } + ) + + const packable = protocol.packable(struct) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + expect(unpacked).toEqual(object) + }) + + it.each([ + [ + 'DateTimeWithZoneOffset/0x46', + new structure.Structure(0x46, [1, 2, 3]) + ], + [ + 'DateTimeWithZoneId/0x66', + new structure.Structure(0x66, [1, 2, 'America/Sao_Paulo']) + ] + ])('should unpack deprecated temporal types as unknown structs (%s)', (_, struct) => { + const buffer = alloc(256) + const protocol = new BoltProtocolV5x4( + new utils.MessageRecordingConnection(), + buffer, + { + disableLosslessIntegers: true + } + ) + + const packable = protocol.packable(struct) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + expect(unpacked).toEqual(struct) + }) + }) + + function newProtocol (recorder) { + return new BoltProtocolV5x4(recorder, null, false, undefined, undefined, () => {}) + } +}) diff --git a/packages/bolt-connection/test/bolt/index.test.js b/packages/bolt-connection/test/bolt/index.test.js index b3359a117..935163196 100644 --- a/packages/bolt-connection/test/bolt/index.test.js +++ b/packages/bolt-connection/test/bolt/index.test.js @@ -34,6 +34,7 @@ import BoltProtocolV5x0 from '../../src/bolt/bolt-protocol-v5x0' import BoltProtocolV5x1 from '../../src/bolt/bolt-protocol-v5x1' import BoltProtocolV5x2 from '../../src/bolt/bolt-protocol-v5x2' import BoltProtocolV5x3 from '../../src/bolt/bolt-protocol-v5x3' +import BoltProtocolV5x4 from '../../src/bolt/bolt-protocol-v5x4' const { logger: { Logger } @@ -47,13 +48,13 @@ describe('#unit Bolt', () => { const writtenBuffer = channel.written[0] const boltMagicPreamble = '60 60 b0 17' - const protocolVersion5x3to5x0 = '00 03 03 05' + const protocolVersion5x4to5x0 = '00 04 04 05' const protocolVersion4x4to4x2 = '00 02 04 04' const protocolVersion4x1 = '00 00 01 04' const protocolVersion3 = '00 00 00 03' expect(writtenBuffer.toHex()).toEqual( - `${boltMagicPreamble} ${protocolVersion5x3to5x0} ${protocolVersion4x4to4x2} ${protocolVersion4x1} ${protocolVersion3}` + `${boltMagicPreamble} ${protocolVersion5x4to5x0} ${protocolVersion4x4to4x2} ${protocolVersion4x1} ${protocolVersion3}` ) }) @@ -390,7 +391,8 @@ describe('#unit Bolt', () => { v(5.0, BoltProtocolV5x0), v(5.1, BoltProtocolV5x1), v(5.2, BoltProtocolV5x2), - v(5.3, BoltProtocolV5x3) + v(5.3, BoltProtocolV5x3), + v(5.4, BoltProtocolV5x4) ] availableProtocols.forEach(lambda) diff --git a/packages/bolt-connection/test/bolt/request-message.test.js b/packages/bolt-connection/test/bolt/request-message.test.js index 53d14c511..cf9d3aee1 100644 --- a/packages/bolt-connection/test/bolt/request-message.test.js +++ b/packages/bolt-connection/test/bolt/request-message.test.js @@ -19,7 +19,7 @@ import RequestMessage from '../../src/bolt/request-message' import { internal, int, json } from 'neo4j-driver-core' -import { notificationFilterBehaviour } from './behaviour' +import { notificationFilterBehaviour, telemetryBehaviour } from './behaviour' const { bookmarks: { Bookmarks }, @@ -533,7 +533,7 @@ describe('#unit RequestMessage', () => { }) }) - describe('Bolt5.3', () => { + describe('BoltV5.3', () => { it('should create HELLO with NodeJS Bolt Agent', () => { const userAgent = 'my-driver/1.0.2' const boltAgent = { @@ -636,6 +636,18 @@ describe('#unit RequestMessage', () => { }) }) + describe('BoltV5.4', () => { + it.each(telemetryBehaviour.telemetryApiFixture())('should create TELEMETRY with api=%s', (api) => { + const message = RequestMessage.telemetry({ api }) + + expect(message.signature).toEqual(0x54) + expect(message.fields).toEqual([int(api)]) + expect(message.toString()).toEqual( + `TELEMETRY ${int(api).toString()}` + ) + }) + }) + function notificationFilterFixtures () { return notificationFilterBehaviour.notificationFilterFixture() .map(notificationFilter => { diff --git a/packages/bolt-connection/test/connection/connection-channel.test.js b/packages/bolt-connection/test/connection/connection-channel.test.js index 841488d3c..2549642dd 100644 --- a/packages/bolt-connection/test/connection/connection-channel.test.js +++ b/packages/bolt-connection/test/connection/connection-channel.test.js @@ -20,7 +20,7 @@ import ChannelConnection from '../../src/connection/connection-channel' import { int, internal, newError } from 'neo4j-driver-core' import { notificationFilterBehaviour } from '../bolt/behaviour' -import { ResultStreamObserver } from '../../src/bolt' +import { CompletedObserver, ResultStreamObserver } from '../../src/bolt' const { serverAddress: { ServerAddress }, @@ -744,6 +744,153 @@ describe('ChannelConnection', () => { expect(result).toBe(observer) expect(protocol.beginTransaction).toBeCalledWith(config) }) + + it.each([ + [undefined, { hints: { 'telemetry.enabled': true } }], + [false, { hints: { 'telemetry.enabled': true } }] + ])('should send telemetry when telemetryDisabled=%s and metadata=%o and telemetry configured', async (telemetryDisabled, metadata) => { + const observer = new ResultStreamObserver() + + const protocol = { + telemetry: jest.fn(() => new CompletedObserver()), + beginTransaction: jest.fn(() => observer), + initialize: jest.fn(observer => observer.onComplete(metadata)) + } + const protocolSupplier = () => protocol + const connection = spyOnConnectionChannel({ protocolSupplier, telemetryDisabled }) + + await connection.connect('userAgent', 'boltAgent', {}) + + const config = { + bookmarks: Bookmarks.empty(), + txConfig: TxConfig.empty(), + database: 'neo4j', + mode: 'READ', + impersonatedUser: 'other cat', + notificationFilter: { + minimumSeverityLevel: 'WARNING' + }, + apiTelemetryConfig: { + api: 2, + onTelemetrySuccess: jest.fn() + }, + beforeError: () => console.log('my error'), + afterComplete: (metadata) => console.log('metadata', metadata) + } + + const result = connection.beginTransaction(config) + + expect(result).toBe(observer) + expect(protocol.beginTransaction).toBeCalledWith(config) + + expect(protocol.telemetry).toBeCalled() + expect(protocol.telemetry).toHaveBeenCalledWith({ api: config.apiTelemetryConfig.api }, { + onCompleted: config.apiTelemetryConfig.onTelemetrySuccess, + onError: config.beforeError + }) + }) + + it.each([ + [true, { hints: { 'telemetry.enabled': true } }], + [undefined, { hints: { 'telemetry.enabled': false } }], + [false, { hints: { 'telemetry.enabled': false } }], + [true, { hints: { 'telemetry.enabled': false } }], + [undefined, { hints: { } }], + [false, { hints: { } }], + [true, { hints: { } }], + [undefined, { }], + [false, { }], + [true, { }], + [undefined, undefined], + [false, undefined], + [true, undefined] + ])('should not send telemetry when telemetryDisabled=%s and metadata=%o and telemetry configured', async (telemetryDisabled, metadata) => { + const observer = new ResultStreamObserver() + + const protocol = { + telemetry: jest.fn(() => new CompletedObserver()), + beginTransaction: jest.fn(() => observer), + initialize: jest.fn(observer => observer.onComplete(metadata)) + } + const protocolSupplier = () => protocol + const connection = spyOnConnectionChannel({ protocolSupplier, telemetryDisabled }) + + await connection.connect('userAgent', 'boltAgent', {}) + + const config = { + bookmarks: Bookmarks.empty(), + txConfig: TxConfig.empty(), + database: 'neo4j', + mode: 'READ', + impersonatedUser: 'other cat', + notificationFilter: { + minimumSeverityLevel: 'WARNING' + }, + apiTelemetryConfig: { + api: 2, + onTelemetrySuccess: jest.fn() + }, + beforeError: () => console.log('my error'), + afterComplete: (metadata) => console.log('metadata', metadata) + } + + const result = connection.beginTransaction(config) + + expect(result).toBe(observer) + expect(protocol.beginTransaction).toBeCalledWith(config) + + expect(protocol.telemetry).not.toBeCalled() + }) + + it.each([ + [undefined, { hints: { 'telemetry.enabled': true } }], + [false, { hints: { 'telemetry.enabled': true } }], + [true, { hints: { 'telemetry.enabled': true } }], + [undefined, { hints: { 'telemetry.enabled': false } }], + [false, { hints: { 'telemetry.enabled': false } }], + [true, { hints: { 'telemetry.enabled': false } }], + [undefined, { hints: { } }], + [false, { hints: { } }], + [true, { hints: { } }], + [undefined, { }], + [false, { }], + [true, { }], + [undefined, undefined], + [false, undefined], + [true, undefined] + ])('should not send telemetry when telemetryDisabled=%s and metadata=%o and telemetry is not configured', async (telemetryDisabled, metadata) => { + const observer = new ResultStreamObserver() + + const protocol = { + telemetry: jest.fn(() => new CompletedObserver()), + beginTransaction: jest.fn(() => observer), + initialize: jest.fn(observer => observer.onComplete(metadata)) + } + const protocolSupplier = () => protocol + const connection = spyOnConnectionChannel({ protocolSupplier, telemetryDisabled }) + + await connection.connect('userAgent', 'boltAgent', {}) + + const config = { + bookmarks: Bookmarks.empty(), + txConfig: TxConfig.empty(), + database: 'neo4j', + mode: 'READ', + impersonatedUser: 'other cat', + notificationFilter: { + minimumSeverityLevel: 'WARNING' + }, + beforeError: () => console.log('my error'), + afterComplete: (metadata) => console.log('metadata', metadata) + } + + const result = connection.beginTransaction(config) + + expect(result).toBe(observer) + expect(protocol.beginTransaction).toBeCalledWith(config) + + expect(protocol.telemetry).not.toBeCalled() + }) }) describe('.run()', () => { @@ -778,6 +925,162 @@ describe('ChannelConnection', () => { expect(result).toBe(observer) expect(protocol.run).toBeCalledWith(query, params, config) }) + + it.each([ + [undefined, { hints: { 'telemetry.enabled': true } }], + [false, { hints: { 'telemetry.enabled': true } }] + ])('should send telemetry when telemetryDisabled=%s and metadata=%o and telemetry configured', async (telemetryDisabled, metadata) => { + const observer = new ResultStreamObserver() + + const protocol = { + telemetry: jest.fn(() => new CompletedObserver()), + run: jest.fn(() => observer), + initialize: jest.fn(observer => observer.onComplete(metadata)) + } + const protocolSupplier = () => protocol + const connection = spyOnConnectionChannel({ protocolSupplier, telemetryDisabled }) + + await connection.connect('userAgent', 'boltAgent', {}) + + const config = { + bookmarks: Bookmarks.empty(), + txConfig: TxConfig.empty(), + database: 'neo4j', + mode: 'READ', + impersonatedUser: 'other cat', + notificationFilter: { + minimumSeverityLevel: 'WARNING' + }, + apiTelemetryConfig: { + api: 2, + onTelemetrySuccess: jest.fn() + }, + beforeError: () => console.log('my error'), + afterComplete: (metadata) => console.log('metadata', metadata) + } + + const query = 'RETURN $x' + const params = { x: 1 } + + const result = connection.run(query, params, config) + + expect(result).toBe(observer) + expect(protocol.run).toBeCalledWith(query, params, config) + + expect(protocol.telemetry).toBeCalled() + expect(protocol.telemetry).toHaveBeenCalledWith({ api: config.apiTelemetryConfig.api }, { + onCompleted: config.apiTelemetryConfig.onTelemetrySuccess, + onError: config.beforeError + }) + }) + + it.each([ + [true, { hints: { 'telemetry.enabled': true } }], + [undefined, { hints: { 'telemetry.enabled': false } }], + [false, { hints: { 'telemetry.enabled': false } }], + [true, { hints: { 'telemetry.enabled': false } }], + [undefined, { hints: { } }], + [false, { hints: { } }], + [true, { hints: { } }], + [undefined, { }], + [false, { }], + [true, { }], + [undefined, undefined], + [false, undefined], + [true, undefined] + ])('should not send telemetry when telemetryDisabled=%s and metadata=%o and telemetry configured', async (telemetryDisabled, metadata) => { + const observer = new ResultStreamObserver() + + const protocol = { + telemetry: jest.fn(() => new CompletedObserver()), + run: jest.fn(() => observer), + initialize: jest.fn(observer => observer.onComplete(metadata)) + } + const protocolSupplier = () => protocol + const connection = spyOnConnectionChannel({ protocolSupplier, telemetryDisabled }) + + await connection.connect('userAgent', 'boltAgent', {}) + + const config = { + bookmarks: Bookmarks.empty(), + txConfig: TxConfig.empty(), + database: 'neo4j', + mode: 'READ', + impersonatedUser: 'other cat', + notificationFilter: { + minimumSeverityLevel: 'WARNING' + }, + apiTelemetryConfig: { + api: 2, + onTelemetrySuccess: jest.fn() + }, + beforeError: () => console.log('my error'), + afterComplete: (metadata) => console.log('metadata', metadata) + } + + const query = 'RETURN $x' + const params = { x: 1 } + + const result = connection.run(query, params, config) + + expect(result).toBe(observer) + expect(protocol.run).toBeCalledWith(query, params, config) + + expect(protocol.telemetry).not.toBeCalled() + }) + + it.each([ + [undefined, { hints: { 'telemetry.enabled': true } }], + [false, { hints: { 'telemetry.enabled': true } }], + [true, { hints: { 'telemetry.enabled': true } }], + [undefined, { hints: { 'telemetry.enabled': false } }], + [false, { hints: { 'telemetry.enabled': false } }], + [true, { hints: { 'telemetry.enabled': false } }], + [undefined, { hints: { } }], + [false, { hints: { } }], + [true, { hints: { } }], + [undefined, { }], + [false, { }], + [true, { }], + [undefined, undefined], + [false, undefined], + [true, undefined] + ])('should not send telemetry when telemetryDisabled=%s and metadata=%o and telemetry is not configured', async (telemetryDisabled, metadata) => { + const observer = new ResultStreamObserver() + + const protocol = { + telemetry: jest.fn(() => new CompletedObserver()), + run: jest.fn(() => observer), + initialize: jest.fn(observer => observer.onComplete(metadata)) + } + const protocolSupplier = () => protocol + const connection = spyOnConnectionChannel({ protocolSupplier, telemetryDisabled }) + + await connection.connect('userAgent', 'boltAgent', {}) + + const config = { + bookmarks: Bookmarks.empty(), + txConfig: TxConfig.empty(), + database: 'neo4j', + mode: 'READ', + impersonatedUser: 'other cat', + notificationFilter: { + minimumSeverityLevel: 'WARNING' + }, + beforeError: () => console.log('my error'), + afterComplete: (metadata) => console.log('metadata', metadata) + } + + const query = 'RETURN $x' + const params = { x: 1 } + + const result = connection.run(query, params, config) + + expect(result).toBe(observer) + expect(protocol.run).toBeCalledWith(query, params, config) + + expect(protocol.telemetry).not.toBeCalled() + }) }) describe('.commitTransaction()', () => { @@ -903,7 +1206,8 @@ describe('ChannelConnection', () => { serversideRouting, chuncker, notificationFilter, - protocolSupplier + protocolSupplier, + telemetryDisabled }) { address = address || ServerAddress.fromUrl('bolt://localhost') logger = logger || new Logger('info', () => {}) @@ -916,7 +1220,8 @@ describe('ChannelConnection', () => { serversideRouting, chuncker, notificationFilter, - protocolSupplier + protocolSupplier, + telemetryDisabled ) } }) diff --git a/packages/core/src/connection.ts b/packages/core/src/connection.ts index 367d14945..7aa4589bf 100644 --- a/packages/core/src/connection.ts +++ b/packages/core/src/connection.ts @@ -15,21 +15,30 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - */ +*/ /* eslint-disable @typescript-eslint/promise-function-async */ import { Bookmarks } from './internal/bookmarks' -import { AccessMode } from './internal/constants' +import { AccessMode, TelemetryApis } from './internal/constants' import { ResultStreamObserver } from './internal/observers' import { TxConfig } from './internal/tx-config' import NotificationFilter from './notification-filter' +interface ApiTelemetryConfig { + api: Apis + onTelemetrySuccess?: () => void +} + +interface HasApiTelemetry { + apiTelemetryConfig?: ApiTelemetryConfig +} + interface HasBeforeErrorAndAfterComplete { beforeError?: (error: Error) => void afterComplete?: (metadata: unknown) => void } -interface BeginTransactionConfig extends HasBeforeErrorAndAfterComplete { +interface BeginTransactionConfig extends HasBeforeErrorAndAfterComplete, HasApiTelemetry { bookmarks: Bookmarks txConfig: TxConfig mode?: AccessMode @@ -104,5 +113,6 @@ export type { BeginTransactionConfig, CommitTransactionConfig, RollbackConnectionConfig, - RunQueryConfig + RunQueryConfig, + ApiTelemetryConfig } diff --git a/packages/core/src/internal/constants.ts b/packages/core/src/internal/constants.ts index f4cf14d9d..3c0f0c724 100644 --- a/packages/core/src/internal/constants.ts +++ b/packages/core/src/internal/constants.ts @@ -37,6 +37,16 @@ const BOLT_PROTOCOL_V5_0: number = 5.0 const BOLT_PROTOCOL_V5_1: number = 5.1 const BOLT_PROTOCOL_V5_2: number = 5.2 const BOLT_PROTOCOL_V5_3: number = 5.3 +const BOLT_PROTOCOL_V5_4: number = 5.4 + +const TELEMETRY_APIS = { + MANAGED_TRANSACTION: 0, + UNMANAGED_TRANSACTION: 1, + AUTO_COMMIT_TRANSACTION: 2, + EXECUTE_QUERY: 3 +} as const + +export type TelemetryApis = typeof TELEMETRY_APIS[keyof typeof TELEMETRY_APIS] export type AccessMode = typeof ACCESS_MODE_READ | typeof ACCESS_MODE_WRITE @@ -58,5 +68,7 @@ export { BOLT_PROTOCOL_V5_0, BOLT_PROTOCOL_V5_1, BOLT_PROTOCOL_V5_2, - BOLT_PROTOCOL_V5_3 + BOLT_PROTOCOL_V5_3, + BOLT_PROTOCOL_V5_4, + TELEMETRY_APIS } diff --git a/packages/core/src/internal/query-executor.ts b/packages/core/src/internal/query-executor.ts index 95ea3f3dd..dfee3fea1 100644 --- a/packages/core/src/internal/query-executor.ts +++ b/packages/core/src/internal/query-executor.ts @@ -22,6 +22,7 @@ import Session from '../session' import Result from '../result' import ManagedTransaction from '../transaction-managed' import { Query } from '../types' +import { TELEMETRY_APIS } from './constants' type SessionFactory = (config: { database?: string, bookmarkManager?: BookmarkManager, impersonatedUser?: string }) => Session @@ -48,7 +49,7 @@ export default class QueryExecutor { }) // @ts-expect-error The method is private for external users - session._setTxExecutorToPipelineBegin(true) + session._configureTransactionExecutor(true, TELEMETRY_APIS.EXECUTE_QUERY) try { const executeInTransaction: TransactionFunction = config.routing === 'READ' diff --git a/packages/core/src/internal/transaction-executor.ts b/packages/core/src/internal/transaction-executor.ts index e1a7979f4..28e6c082c 100644 --- a/packages/core/src/internal/transaction-executor.ts +++ b/packages/core/src/internal/transaction-executor.ts @@ -17,17 +17,17 @@ * limitations under the License. */ /* eslint-disable @typescript-eslint/promise-function-async */ - import { newError, isRetriableError } from '../error' -import Transaction from '../transaction' +import Transaction, { NonAutoCommitApiTelemetryConfig, NonAutoCommitTelemetryApis } from '../transaction' import TransactionPromise from '../transaction-promise' +import { TELEMETRY_APIS } from './constants' const DEFAULT_MAX_RETRY_TIME_MS = 30 * 1000 // 30 seconds const DEFAULT_INITIAL_RETRY_DELAY_MS = 1000 // 1 seconds const DEFAULT_RETRY_DELAY_MULTIPLIER = 2.0 const DEFAULT_RETRY_DELAY_JITTER_FACTOR = 0.2 -type TransactionCreator = () => TransactionPromise +type TransactionCreator = (apiTransactionConfig?: NonAutoCommitApiTelemetryConfig) => TransactionPromise type TransactionWork = (tx: Tx) => T | Promise type Resolve = (value: T | PromiseLike) => void type Reject = (value: any) => void @@ -48,6 +48,8 @@ function clearTimeoutWrapper (timeoutId: Timeout): void { return clearTimeout(timeoutId) } +interface ExecutionContext { apiTransactionConfig?: NonAutoCommitApiTelemetryConfig } + export class TransactionExecutor { private readonly _maxRetryTimeMs: number private readonly _initialRetryDelayMs: number @@ -56,6 +58,7 @@ export class TransactionExecutor { private _inFlightTimeoutIds: Timeout[] private readonly _setTimeout: SetTimeout private readonly _clearTimeout: ClearTimeout + public telemetryApi: NonAutoCommitTelemetryApis public pipelineBegin: boolean constructor ( @@ -90,6 +93,7 @@ export class TransactionExecutor { this._inFlightTimeoutIds = [] this.pipelineBegin = false + this.telemetryApi = TELEMETRY_APIS.MANAGED_TRANSACTION this._verifyAfterConstruction() } @@ -99,13 +103,23 @@ export class TransactionExecutor { transactionWork: TransactionWork, transactionWrapper?: (tx: Transaction) => Tx ): Promise { + const context: ExecutionContext = { + apiTransactionConfig: { + api: this.telemetryApi, + onTelemetrySuccess: () => { + context.apiTransactionConfig = undefined + } + } + } + return new Promise((resolve, reject) => { this._executeTransactionInsidePromise( transactionCreator, transactionWork, resolve, reject, - transactionWrapper + transactionWrapper, + context ).catch(reject) }).catch(error => { const retryStartTimeMs = Date.now() @@ -116,7 +130,8 @@ export class TransactionExecutor { error, retryStartTimeMs, retryDelayMs, - transactionWrapper + transactionWrapper, + context ) }) } @@ -133,7 +148,8 @@ export class TransactionExecutor { error: Error, retryStartTime: number, retryDelayMs: number, - transactionWrapper?: (tx: Transaction) => Tx + transactionWrapper?: (tx: Transaction) => Tx, + executionContext?: ExecutionContext ): Promise { const elapsedTimeMs = Date.now() - retryStartTime @@ -153,7 +169,8 @@ export class TransactionExecutor { transactionWork, resolve, reject, - transactionWrapper + transactionWrapper, + executionContext ).catch(reject) }, nextRetryTime) // add newly created timeoutId to the list of all in-flight timeouts @@ -166,7 +183,8 @@ export class TransactionExecutor { error, retryStartTime, nextRetryDelayMs, - transactionWrapper + transactionWrapper, + executionContext ) }) } @@ -176,11 +194,16 @@ export class TransactionExecutor { transactionWork: TransactionWork, resolve: Resolve, reject: Reject, - transactionWrapper?: (tx: Transaction) => Tx + transactionWrapper?: (tx: Transaction) => Tx, + executionContext?: ExecutionContext ): Promise { let tx: Transaction try { - const txPromise = transactionCreator() + const txPromise = transactionCreator( + executionContext?.apiTransactionConfig != null + ? { ...executionContext?.apiTransactionConfig } + : undefined + ) tx = this.pipelineBegin ? txPromise : await txPromise } catch (error) { // failed to create a transaction diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 6e27b1488..2fad4f2d7 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -21,10 +21,10 @@ import { FailedObserver, ResultStreamObserver } from './internal/observers' import { validateQueryAndParameters } from './internal/util' -import { FETCH_ALL, ACCESS_MODE_READ, ACCESS_MODE_WRITE } from './internal/constants' +import { FETCH_ALL, ACCESS_MODE_READ, ACCESS_MODE_WRITE, TELEMETRY_APIS } from './internal/constants' import { newError } from './error' import Result from './result' -import Transaction from './transaction' +import Transaction, { NonAutoCommitApiTelemetryConfig, NonAutoCommitTelemetryApis } from './transaction' import { ConnectionHolder } from './internal/connection-holder' import { TransactionExecutor } from './internal/transaction-executor' import { Bookmarks } from './internal/bookmarks' @@ -194,6 +194,9 @@ class Session { txConfig: autoCommitTxConfig, mode: this._mode, database: this._database, + apiTelemetryConfig: { + api: TELEMETRY_APIS.AUTO_COMMIT_TRANSACTION + }, impersonatedUser: this._impersonatedUser, afterComplete: (meta: any) => this._onCompleteCallback(meta, bookmarks), reactive: this._reactive, @@ -293,10 +296,10 @@ class Session { txConfig = new TxConfig(arg, this._log) } - return this._beginTransaction(this._mode, txConfig) + return this._beginTransaction(this._mode, txConfig, { api: TELEMETRY_APIS.UNMANAGED_TRANSACTION }) } - _beginTransaction (accessMode: SessionMode, txConfig: TxConfig): TransactionPromise { + _beginTransaction (accessMode: SessionMode, txConfig: TxConfig, apiTelemetryConfig?: NonAutoCommitApiTelemetryConfig): TransactionPromise { if (!this._open) { throw newError('Cannot begin a transaction on a closed session.') } @@ -322,7 +325,8 @@ class Session { fetchSize: this._fetchSize, lowRecordWatermark: this._lowRecordWatermark, highRecordWatermark: this._highRecordWatermark, - notificationFilter: this._notificationFilter + notificationFilter: this._notificationFilter, + apiTelemetryConfig }) tx._begin(() => this._bookmarks(), txConfig) return tx @@ -431,7 +435,7 @@ class Session { transactionWork: TransactionWork ): Promise { return this._transactionExecutor.execute( - () => this._beginTransaction(accessMode, transactionConfig), + (apiTelemetryConfig?: NonAutoCommitApiTelemetryConfig) => this._beginTransaction(accessMode, transactionConfig, apiTelemetryConfig), transactionWork ) } @@ -493,7 +497,7 @@ class Session { transactionWork: ManagedTransactionWork ): Promise { return this._transactionExecutor.execute( - () => this._beginTransaction(accessMode, transactionConfig), + (apiTelemetryConfig?: NonAutoCommitApiTelemetryConfig) => this._beginTransaction(accessMode, transactionConfig, apiTelemetryConfig), transactionWork, ManagedTransaction.fromTransaction ) @@ -594,13 +598,15 @@ class Session { } /** - * Configure the transaction executor to pipeline transaction begin. + * Configure the transaction executor * + * This used by {@link Driver#executeQuery} * @private * @returns {void} */ - private _setTxExecutorToPipelineBegin (pipelined: boolean): void { + private _configureTransactionExecutor (pipelined: boolean, telemetryApi: NonAutoCommitTelemetryApis): void { this._transactionExecutor.pipelineBegin = pipelined + this._transactionExecutor.telemetryApi = telemetryApi } /** diff --git a/packages/core/src/transaction-promise.ts b/packages/core/src/transaction-promise.ts index 3470df842..101ba5487 100644 --- a/packages/core/src/transaction-promise.ts +++ b/packages/core/src/transaction-promise.ts @@ -19,7 +19,7 @@ /* eslint-disable @typescript-eslint/promise-function-async */ -import Transaction from './transaction' +import Transaction, { NonAutoCommitApiTelemetryConfig } from './transaction' import { ConnectionHolder } from './internal/connection-holder' @@ -58,6 +58,7 @@ class TransactionPromise extends Transaction implements Promise { * @param {number} args.fetchSize - the record fetch size in each pulling batch. * @param {string} args.impersonatedUser - The name of the user which should be impersonated for the duration of the session. * @param {NotificationFilter} args.notificationFilter - The notification filter used for this transaction. + * @param {NonAutoCommitApiTelemetryConfig} args.apiTelemetryConfig - The api telemetry configuration. Empty/Null for disabling telemetry */ constructor ({ connectionHolder, @@ -69,7 +70,8 @@ class TransactionPromise extends Transaction implements Promise { impersonatedUser, highRecordWatermark, lowRecordWatermark, - notificationFilter + notificationFilter, + apiTelemetryConfig }: { connectionHolder: ConnectionHolder onClose: () => void @@ -81,6 +83,7 @@ class TransactionPromise extends Transaction implements Promise { highRecordWatermark: number lowRecordWatermark: number notificationFilter?: NotificationFilter + apiTelemetryConfig?: NonAutoCommitApiTelemetryConfig }) { super({ connectionHolder, @@ -92,7 +95,8 @@ class TransactionPromise extends Transaction implements Promise { impersonatedUser, highRecordWatermark, lowRecordWatermark, - notificationFilter + notificationFilter, + apiTelemetryConfig }) } diff --git a/packages/core/src/transaction.ts b/packages/core/src/transaction.ts index 1f51c142e..742cbd52c 100644 --- a/packages/core/src/transaction.ts +++ b/packages/core/src/transaction.ts @@ -19,7 +19,7 @@ /* eslint-disable @typescript-eslint/promise-function-async */ import { validateQueryAndParameters } from './internal/util' -import Connection from './connection' +import Connection, { ApiTelemetryConfig } from './connection' import { ConnectionHolder, ReadOnlyConnectionHolder, @@ -39,6 +39,10 @@ import Result from './result' import { Query } from './types' import { RecordShape } from './record' import NotificationFilter from './notification-filter' +import { TelemetryApis, TELEMETRY_APIS } from './internal/constants' + +type NonAutoCommitTelemetryApis = Exclude +type NonAutoCommitApiTelemetryConfig = ApiTelemetryConfig /** * Represents a transaction in the Neo4j database. @@ -63,6 +67,7 @@ class Transaction { private readonly _activePromise: Promise private _acceptActive: () => void private readonly _notificationFilter?: NotificationFilter + private readonly _apiTelemetryConfig?: NonAutoCommitApiTelemetryConfig /** * @constructor @@ -78,6 +83,7 @@ class Transaction { * @param {number} args.highRecordWatermark - The high watermark for the record buffer. * @param {number} args.lowRecordWatermark - The low watermark for the record buffer. * @param {NotificationFilter} args.notificationFilter - The notification filter used for this transaction. + * @param {NonAutoCommitApiTelemetryConfig} args.apiTelemetryConfig - The api telemetry configuration. Empty/Null for disabling telemetry */ constructor ({ connectionHolder, @@ -89,7 +95,8 @@ class Transaction { impersonatedUser, highRecordWatermark, lowRecordWatermark, - notificationFilter + notificationFilter, + apiTelemetryConfig }: { connectionHolder: ConnectionHolder onClose: () => void @@ -101,6 +108,7 @@ class Transaction { highRecordWatermark: number lowRecordWatermark: number notificationFilter?: NotificationFilter + apiTelemetryConfig?: NonAutoCommitApiTelemetryConfig }) { this._connectionHolder = connectionHolder this._reactive = reactive @@ -117,6 +125,7 @@ class Transaction { this._highRecordWatermark = highRecordWatermark this._bookmarks = Bookmarks.empty() this._notificationFilter = notificationFilter + this._apiTelemetryConfig = apiTelemetryConfig this._acceptActive = () => { } // satisfy DenoJS this._activePromise = new Promise((resolve, reject) => { this._acceptActive = resolve @@ -127,6 +136,7 @@ class Transaction { * @private * @param {Bookmarks | string | string []} bookmarks * @param {TxConfig} txConfig + * @param {Object} events List of observers to events * @returns {void} */ _begin (getBookmarks: () => Promise, txConfig: TxConfig, events?: { @@ -146,6 +156,7 @@ class Transaction { database: this._connectionHolder.database(), impersonatedUser: this._impersonatedUser, notificationFilter: this._notificationFilter, + apiTelemetryConfig: this._apiTelemetryConfig, beforeError: (error: Error) => { if (events != null) { events.onError(error) @@ -706,3 +717,7 @@ function newCompletedResult ( } export default Transaction +export type { + NonAutoCommitTelemetryApis, + NonAutoCommitApiTelemetryConfig +} diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 3714396c1..6989dbdd9 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -80,6 +80,7 @@ export class Config { logging?: LoggingConfig resolver?: (address: string) => string[] | Promise userAgent?: string + telemetryDisabled?: boolean /** * @constructor @@ -285,6 +286,34 @@ export class Config { * @type {string|undefined} */ this.userAgent = undefined + + /** + * Specify if telemetry collection is disabled. + * + * By default, the driver will send anonymous usage statistics to the server it connects to if the server requests those. + * By setting ``telemetryDisabled=true``, the driver will not send any telemetry data. + * + * The driver transmits the following information: + * + * Every time one of the following APIs is used to execute a query (for the first time), the server is informed of this + * (without any further information like arguments, client identifiers, etc.): + * + * * {@link Driver#executeQuery} + * * {@link Session#run} + * * {@link Session#beginTransaction} + * * {@link Session#executeRead} + * * {@link Session#executeWrite} + * * {@link Session#writeTransaction} + * * {@link Session#readTransaction} + * * The reactive counterparts of methods above. + * + * Metrics are only collected when enabled both in server and driver instances. + * + * **Default**: ```false``` + * + * @type {boolean} + */ + this.telemetryDisabled = false } } diff --git a/packages/core/test/internal/query-executor.test.ts b/packages/core/test/internal/query-executor.test.ts index 0aa4e2333..d636acfc5 100644 --- a/packages/core/test/internal/query-executor.test.ts +++ b/packages/core/test/internal/query-executor.test.ts @@ -22,6 +22,7 @@ import QueryExecutor from '../../src/internal/query-executor' import ManagedTransaction from '../../src/transaction-managed' import ResultStreamObserverMock from '../utils/result-stream-observer.mock' import { Query } from '../../src/types' +import { TELEMETRY_APIS } from '../../src/internal/constants' type ManagedTransactionWork = (tx: ManagedTransaction) => Promise | T @@ -86,18 +87,18 @@ describe('QueryExecutor', () => { expect(spyOnExecuteRead).toHaveBeenCalled() }) - it('should configure the session with pipeline begin', async () => { + it('should configure the session with pipeline begin and correct api metrics', async () => { const { queryExecutor, sessionsCreated } = createExecutor() await queryExecutor.execute(baseConfig, 'query') expect(sessionsCreated.length).toBe(1) - const [{ spyOnSetTxExecutorToPipelineBegin, spyOnExecuteRead }] = sessionsCreated + const [{ spyOnConfigureTransactionExecutor, spyOnExecuteRead }] = sessionsCreated - expect(spyOnSetTxExecutorToPipelineBegin).toHaveBeenCalledTimes(1) - expect(spyOnSetTxExecutorToPipelineBegin).toHaveBeenCalledWith(true) + expect(spyOnConfigureTransactionExecutor).toHaveBeenCalledTimes(1) + expect(spyOnConfigureTransactionExecutor).toHaveBeenCalledWith(true, TELEMETRY_APIS.EXECUTE_QUERY) expect(spyOnExecuteRead.mock.invocationCallOrder[0]).toBeGreaterThan( - spyOnSetTxExecutorToPipelineBegin.mock.invocationCallOrder[0] + spyOnConfigureTransactionExecutor.mock.invocationCallOrder[0] ) }) @@ -228,18 +229,18 @@ describe('QueryExecutor', () => { expect(spyOnExecuteWrite).toHaveBeenCalled() }) - it('should configure the session with pipeline begin', async () => { + it('should configure the session with pipeline begin and api telemetry', async () => { const { queryExecutor, sessionsCreated } = createExecutor() await queryExecutor.execute(baseConfig, 'query') expect(sessionsCreated.length).toBe(1) - const [{ spyOnSetTxExecutorToPipelineBegin, spyOnExecuteWrite }] = sessionsCreated + const [{ spyOnConfigureTransactionExecutor, spyOnExecuteWrite }] = sessionsCreated - expect(spyOnSetTxExecutorToPipelineBegin).toHaveBeenCalledTimes(1) - expect(spyOnSetTxExecutorToPipelineBegin).toHaveBeenCalledWith(true) + expect(spyOnConfigureTransactionExecutor).toHaveBeenCalledTimes(1) + expect(spyOnConfigureTransactionExecutor).toHaveBeenCalledWith(true, TELEMETRY_APIS.EXECUTE_QUERY) expect(spyOnExecuteWrite.mock.invocationCallOrder[0]).toBeGreaterThan( - spyOnSetTxExecutorToPipelineBegin.mock.invocationCallOrder[0] + spyOnConfigureTransactionExecutor.mock.invocationCallOrder[0] ) }) @@ -346,7 +347,7 @@ describe('QueryExecutor', () => { spyOnExecuteRead: jest.SpyInstance spyOnExecuteWrite: jest.SpyInstance spyOnClose: jest.SpyInstance> - spyOnSetTxExecutorToPipelineBegin: jest.SpyInstance + spyOnConfigureTransactionExecutor: jest.SpyInstance }> createSession: jest.Mock } { @@ -359,7 +360,7 @@ describe('QueryExecutor', () => { spyOnExecuteRead: jest.SpyInstance spyOnExecuteWrite: jest.SpyInstance spyOnClose: jest.SpyInstance> - spyOnSetTxExecutorToPipelineBegin: jest.SpyInstance + spyOnConfigureTransactionExecutor: jest.SpyInstance }> = [] const createSession = jest.fn((args) => { const session = new Session(args) @@ -369,7 +370,7 @@ describe('QueryExecutor', () => { spyOnExecuteWrite: jest.spyOn(session, 'executeWrite'), spyOnClose: jest.spyOn(session, 'close'), // @ts-expect-error - spyOnSetTxExecutorToPipelineBegin: jest.spyOn(session, '_setTxExecutorToPipelineBegin') + spyOnConfigureTransactionExecutor: jest.spyOn(session, '_configureTransactionExecutor') } sessionsCreated.push(sessionCreated) _mockSessionExecuteRead(sessionCreated.spyOnExecuteRead) diff --git a/packages/core/test/session.test.ts b/packages/core/test/session.test.ts index 0a9675f11..3c2c717b4 100644 --- a/packages/core/test/session.test.ts +++ b/packages/core/test/session.test.ts @@ -20,9 +20,10 @@ import { ConnectionProvider, Session, Connection, TransactionPromise, Transactio import { BeginTransactionConfig, CommitTransactionConfig } from '../src/connection' import { Releasable } from '../src/connection-provider' import { bookmarks } from '../src/internal' -import { ACCESS_MODE_READ, FETCH_ALL } from '../src/internal/constants' +import { ACCESS_MODE_READ, FETCH_ALL, TELEMETRY_APIS } from '../src/internal/constants' import { Logger } from '../src/internal/logger' import { TransactionExecutor } from '../src/internal/transaction-executor' +import { NonAutoCommitTelemetryApis } from '../src/transaction' import ManagedTransaction from '../src/transaction-managed' import { AuthToken, LoggerFunction } from '../src/types' import FakeConnection from './utils/connection.fake' @@ -87,6 +88,20 @@ describe('session', () => { expect(result._watermarks).toEqual({ high: Number.MAX_VALUE, low: Number.MAX_VALUE }) }) + it('run should set protocol option apiTelemetryConfig.api to TELEMETRY_APIS.AUTO_COMMIT_TRANSACTION', async () => { + const connection = newFakeConnection() + const session = newSessionWithConnection(connection, false, 1000) + + const result = session.run('RETURN 1') + await result + + expect(connection.seenProtocolOptions[0]).toMatchObject({ + apiTelemetryConfig: { + api: TELEMETRY_APIS.AUTO_COMMIT_TRANSACTION + } + }) + }) + it('run should send watermarks to Transaction when fetchsize if defined (begin)', async () => { const connection = newFakeConnection() const session = newSessionWithConnection(connection, false, 1000) @@ -551,6 +566,31 @@ describe('session', () => { ) ) }) + + it('should call begin query with apiTelemetryConfig with api equals to TELEMETRY_APIS.UNMANAGED_TRANSACTION', async () => { + const manager = bookmarkManager({ + initialBookmarks: [...neo4jBookmarks, ...systemBookmarks] + }) + + const connection = mockBeginWithSuccess(newFakeConnection()) + + const { session } = setupSession({ + connection, + bookmarkManager: manager, + beginTx: false, + database: 'neo4j' + }) + + await session.beginTransaction() + + expect(connection.seenBeginTransaction[0]).toEqual([ + expect.objectContaining({ + apiTelemetryConfig: { + api: TELEMETRY_APIS.UNMANAGED_TRANSACTION + } + }) + ]) + }) }) describe('.commit()', () => { @@ -689,6 +729,25 @@ describe('session', () => { ) }) + it('should call begin with apiTelemetryConfig with api equals to TELEMETRY_APIS.MANAGED_TRANSACTION', async () => { + const connection = mockBeginWithSuccess(newFakeConnection()) + const session = newSessionWithConnection(connection, false, FETCH_ALL) + // @ts-expect-error + jest.spyOn(Transaction.prototype, 'run').mockImplementation(async () => await Promise.resolve()) + const query = 'RETURN $a' + const params = { a: 1 } + + await execute(session)(async (tx: ManagedTransaction) => { + await tx.run(query, params) + }) + + expect(connection.seenBeginTransaction[0][0]).toMatchObject({ + apiTelemetryConfig: { + api: TELEMETRY_APIS.MANAGED_TRANSACTION + } + }) + }) + it('should log a warning for timeout configurations with sub milliseconds', async () => { return await fc.assert( fc.asyncProperty( @@ -1154,22 +1213,31 @@ describe('session', () => { }) }) - describe('Pipeline Begin on TxFunc', () => { + describe('_configureTransactionExecutor', () => { it('session should not change the default on session creation', () => { const session = newSessionWithConnection(new FakeConnection()) // @ts-expect-error expect(session._transactionExecutor.pipelineBegin).toEqual(new TransactionExecutor().pipelineBegin) + // @ts-expect-error + expect(session._transactionExecutor.telemetryApi).toEqual(new TransactionExecutor().telemetryApi) }) - it.each([true, false])('_setTxExecutorToPipelineBegin(%s) => configure executor', (pipelined) => { + it.each( + [...Object.values(TELEMETRY_APIS) + .filter(api => api !== TELEMETRY_APIS.AUTO_COMMIT_TRANSACTION) + .flatMap(api => [true, false].map(pipelined => [pipelined, api])) + ] + )('(%s, %s) => configure executor', (pipelined: boolean, telemetryApi: NonAutoCommitTelemetryApis) => { const session = newSessionWithConnection(new FakeConnection()) // @ts-expect-error - session._setTxExecutorToPipelineBegin(pipelined) + session._configureTransactionExecutor(pipelined, telemetryApi) // @ts-expect-error expect(session._transactionExecutor.pipelineBegin).toEqual(pipelined) + // @ts-expect-error + expect(session._transactionExecutor.telemetryApi).toEqual(telemetryApi) }) }) }) diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js index 547fd91fc..149723d2d 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js @@ -27,6 +27,7 @@ import { Chunker } from '../channel/index.js' import { structure, v1 } from '../packstream/index.js' import RequestMessage, { SIGNATURES } from './request-message.js' import { + CompletedObserver, LoginObserver, LogoffObserver, ResetObserver, @@ -454,6 +455,24 @@ export default class BoltProtocol { return observer } + /** + * Send a TELEMETRY through the underlying connection. + * + * @param {object} param0 Message params + * @param {number} param0.api The API called + * @param {object} param1 Configuration and callbacks + * @param {function()} param1.onCompleted Called when completed + * @param {function()} param1.onError Called when error + * @return {StreamObserver} the stream observer that monitors the corresponding server response. + */ + telemetry ({ api }, { onError, onCompleted } = {}) { + const observer = new CompletedObserver() + if (onCompleted) { + onCompleted() + } + return observer + } + _createPacker (chunker) { return new v1.Packer(chunker) } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x4.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x4.js new file mode 100644 index 000000000..b1b27cf0b --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x4.js @@ -0,0 +1,61 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import BoltProtocolV5x3 from './bolt-protocol-v5x3.js' + +import transformersFactories from './bolt-protocol-v5x4.transformer.js' +import RequestMessage from './request-message.js' +import { TelemetryObserver } from './stream-observers.js' +import Transformer from './transformer.js' + +import { internal } from '../../core/index.ts' + +const { + constants: { BOLT_PROTOCOL_V5_4 } +} = internal + +export default class BoltProtocol extends BoltProtocolV5x3 { + get version () { + return BOLT_PROTOCOL_V5_4 + } + + get transformer () { + if (this._transformer === undefined) { + this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config, this._log))) + } + return this._transformer + } + + /** + * Send a TELEMETRY through the underlying connection. + * + * @param {object} param0 Message params + * @param {number} param0.api The API called + * @param {object} param1 Configuration and callbacks callbacks + * @param {function()} param1.onCompleted Called when completed + * @param {function()} param1.onError Called when error + * @return {StreamObserver} the stream observer that monitors the corresponding server response. + */ + telemetry ({ api }, { onError, onCompleted } = {}) { + const observer = new TelemetryObserver({ onCompleted, onError }) + + this.write(RequestMessage.telemetry({ api }), observer, false) + + return observer + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x4.transformer.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x4.transformer.js new file mode 100644 index 000000000..96b723fe1 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x4.transformer.js @@ -0,0 +1,24 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import v5x3 from './bolt-protocol-v5x3.transformer.js' + +export default { + ...v5x3 +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js index 0b0918662..9d81145c2 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js @@ -30,6 +30,7 @@ import BoltProtocolV5x0 from './bolt-protocol-v5x0.js' import BoltProtocolV5x1 from './bolt-protocol-v5x1.js' import BoltProtocolV5x2 from './bolt-protocol-v5x2.js' import BoltProtocolV5x3 from './bolt-protocol-v5x3.js' +import BoltProtocolV5x4 from './bolt-protocol-v5x4.js' // eslint-disable-next-line no-unused-vars import { Chunker, Dechunker } from '../channel/index.js' import ResponseHandler from './response-handler.js' @@ -222,6 +223,14 @@ function createProtocol ( log, onProtocolError, serversideRouting) + case 5.4: + return new BoltProtocolV5x4(server, + chunker, + packingConfig, + createResponseHandler, + log, + onProtocolError, + serversideRouting) default: throw newError('Unknown Bolt protocol version: ' + version) } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/handshake.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/handshake.js index 52e92ecb3..169e1ab7f 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/handshake.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/handshake.js @@ -78,7 +78,7 @@ function parseNegotiatedResponse (buffer, log) { */ function newHandshakeBuffer () { return createHandshakeMessage([ - [version(5, 3), version(5, 0)], + [version(5, 4), version(5, 0)], [version(4, 4), version(4, 2)], version(4, 1), version(3, 0) diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/request-message.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/request-message.js index f86c08ab7..b4ce8e430 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/request-message.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/request-message.js @@ -38,6 +38,9 @@ const GOODBYE = 0x02 // 0000 0010 // GOODBYE const BEGIN = 0x11 // 0001 0001 // BEGIN const COMMIT = 0x12 // 0001 0010 // COMMIT const ROLLBACK = 0x13 // 0001 0011 // ROLLBACK + +const TELEMETRY = 0x54 // 0101 0100 // TELEMETRY + const ROUTE = 0x66 // 0110 0110 // ROUTE const LOGON = 0x6A // LOGON @@ -61,6 +64,7 @@ const SIGNATURES = Object.freeze({ BEGIN, COMMIT, ROLLBACK, + TELEMETRY, ROUTE, LOGON, LOGOFF, @@ -367,6 +371,15 @@ export default class RequestMessage { ) } + static telemetry ({ api }) { + const parsedApi = int(api) + return new RequestMessage( + TELEMETRY, + [parsedApi], + () => `TELEMETRY ${parsedApi.toString()}` + ) + } + /** * Generate the ROUTE message, this message is used to fetch the routing table from the server * diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/stream-observers.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/stream-observers.js index 849c8b674..19aa6a29c 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/stream-observers.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/stream-observers.js @@ -526,6 +526,38 @@ class ResetObserver extends StreamObserver { } } +class TelemetryObserver extends ResultStreamObserver { + /** + * + * @param {Object} param - + * @param {function(err: Error)} param.onError + * @param {function(metadata)} param.onCompleted + */ + constructor ({ onError, onCompleted } = {}) { + super() + this._onError = onError + this._onCompleted = onCompleted + } + + onNext (record) { + this.onError( + newError('Received RECORD when sending telemetry ' + json.stringify(record), PROTOCOL_ERROR) + ) + } + + onError (error) { + if (this._onError) { + this._onError(error) + } + } + + onCompleted (metadata) { + if (this._onCompleted) { + this._onCompleted(metadata) + } + } +} + class FailedObserver extends ResultStreamObserver { constructor ({ error, onError }) { super({ beforeError: onError }) @@ -708,5 +740,6 @@ export { FailedObserver, CompletedObserver, RouteObserver, - ProcedureRouteObserver + ProcedureRouteObserver, + TelemetryObserver } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js index 48ba86127..8e55080cc 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js @@ -87,7 +87,8 @@ export function createChannelConnection ( serversideRouting, chunker, config.notificationFilter, - createProtocol + createProtocol, + config.telemetryDisabled ) // forward all pending bytes to the dechunker @@ -121,7 +122,8 @@ export default class ChannelConnection extends Connection { serversideRouting = null, chunker, // to be removed, notificationFilter, - protocolSupplier + protocolSupplier, + telemetryDisabled ) { super(errorHandler) this._authToken = null @@ -137,6 +139,8 @@ export default class ChannelConnection extends Connection { this._log = createConnectionLogger(this, log) this._serversideRouting = serversideRouting this._notificationFilter = notificationFilter + this._telemetryDisabledDriverConfig = telemetryDisabled === true + this._telemetryDisabledConnection = true // connection from the database, returned in response for HELLO message and might not be available this._dbConnectionId = null @@ -157,13 +161,31 @@ export default class ChannelConnection extends Connection { } beginTransaction (config) { + this._sendTelemetryIfEnabled(config) return this._protocol.beginTransaction(config) } run (query, parameters, config) { + this._sendTelemetryIfEnabled(config) return this._protocol.run(query, parameters, config) } + _sendTelemetryIfEnabled (config) { + if (this._telemetryDisabledConnection || + this._telemetryDisabledDriverConfig || + config == null || + config.apiTelemetryConfig == null) { + return + } + + this._protocol.telemetry({ + api: config.apiTelemetryConfig.api + }, { + onCompleted: config.apiTelemetryConfig.onTelemetrySuccess, + onError: config.beforeError + }) + } + commitTransaction (config) { return this._protocol.commitTransaction(config) } @@ -290,6 +312,11 @@ export default class ChannelConnection extends Connection { ) } } + + const telemetryEnabledHint = metadata.hints['telemetry.enabled'] + if (telemetryEnabledHint === true) { + this._telemetryDisabledConnection = false + } } } resolve(self) diff --git a/packages/neo4j-driver-deno/lib/core/connection.ts b/packages/neo4j-driver-deno/lib/core/connection.ts index cf9cb9eca..1066cb870 100644 --- a/packages/neo4j-driver-deno/lib/core/connection.ts +++ b/packages/neo4j-driver-deno/lib/core/connection.ts @@ -15,21 +15,30 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - */ +*/ /* eslint-disable @typescript-eslint/promise-function-async */ import { Bookmarks } from './internal/bookmarks.ts' -import { AccessMode } from './internal/constants.ts' +import { AccessMode, TelemetryApis } from './internal/constants.ts' import { ResultStreamObserver } from './internal/observers.ts' import { TxConfig } from './internal/tx-config.ts' import NotificationFilter from './notification-filter.ts' +interface ApiTelemetryConfig { + api: Apis + onTelemetrySuccess?: () => void +} + +interface HasApiTelemetry { + apiTelemetryConfig?: ApiTelemetryConfig +} + interface HasBeforeErrorAndAfterComplete { beforeError?: (error: Error) => void afterComplete?: (metadata: unknown) => void } -interface BeginTransactionConfig extends HasBeforeErrorAndAfterComplete { +interface BeginTransactionConfig extends HasBeforeErrorAndAfterComplete, HasApiTelemetry { bookmarks: Bookmarks txConfig: TxConfig mode?: AccessMode @@ -104,5 +113,6 @@ export type { BeginTransactionConfig, CommitTransactionConfig, RollbackConnectionConfig, - RunQueryConfig + RunQueryConfig, + ApiTelemetryConfig } diff --git a/packages/neo4j-driver-deno/lib/core/internal/constants.ts b/packages/neo4j-driver-deno/lib/core/internal/constants.ts index f4cf14d9d..3c0f0c724 100644 --- a/packages/neo4j-driver-deno/lib/core/internal/constants.ts +++ b/packages/neo4j-driver-deno/lib/core/internal/constants.ts @@ -37,6 +37,16 @@ const BOLT_PROTOCOL_V5_0: number = 5.0 const BOLT_PROTOCOL_V5_1: number = 5.1 const BOLT_PROTOCOL_V5_2: number = 5.2 const BOLT_PROTOCOL_V5_3: number = 5.3 +const BOLT_PROTOCOL_V5_4: number = 5.4 + +const TELEMETRY_APIS = { + MANAGED_TRANSACTION: 0, + UNMANAGED_TRANSACTION: 1, + AUTO_COMMIT_TRANSACTION: 2, + EXECUTE_QUERY: 3 +} as const + +export type TelemetryApis = typeof TELEMETRY_APIS[keyof typeof TELEMETRY_APIS] export type AccessMode = typeof ACCESS_MODE_READ | typeof ACCESS_MODE_WRITE @@ -58,5 +68,7 @@ export { BOLT_PROTOCOL_V5_0, BOLT_PROTOCOL_V5_1, BOLT_PROTOCOL_V5_2, - BOLT_PROTOCOL_V5_3 + BOLT_PROTOCOL_V5_3, + BOLT_PROTOCOL_V5_4, + TELEMETRY_APIS } diff --git a/packages/neo4j-driver-deno/lib/core/internal/query-executor.ts b/packages/neo4j-driver-deno/lib/core/internal/query-executor.ts index bbf9b2ba1..8704c2d1e 100644 --- a/packages/neo4j-driver-deno/lib/core/internal/query-executor.ts +++ b/packages/neo4j-driver-deno/lib/core/internal/query-executor.ts @@ -22,6 +22,7 @@ import Session from '../session.ts' import Result from '../result.ts' import ManagedTransaction from '../transaction-managed.ts' import { Query } from '../types.ts' +import { TELEMETRY_APIS } from './constants.ts' type SessionFactory = (config: { database?: string, bookmarkManager?: BookmarkManager, impersonatedUser?: string }) => Session @@ -48,7 +49,7 @@ export default class QueryExecutor { }) // @ts-expect-error The method is private for external users - session._setTxExecutorToPipelineBegin(true) + session._configureTransactionExecutor(true, TELEMETRY_APIS.EXECUTE_QUERY) try { const executeInTransaction: TransactionFunction = config.routing === 'READ' diff --git a/packages/neo4j-driver-deno/lib/core/internal/transaction-executor.ts b/packages/neo4j-driver-deno/lib/core/internal/transaction-executor.ts index 27b055ba3..69e2d4e05 100644 --- a/packages/neo4j-driver-deno/lib/core/internal/transaction-executor.ts +++ b/packages/neo4j-driver-deno/lib/core/internal/transaction-executor.ts @@ -17,17 +17,17 @@ * limitations under the License. */ /* eslint-disable @typescript-eslint/promise-function-async */ - import { newError, isRetriableError } from '../error.ts' -import Transaction from '../transaction.ts' +import Transaction, { NonAutoCommitApiTelemetryConfig, NonAutoCommitTelemetryApis } from '../transaction.ts' import TransactionPromise from '../transaction-promise.ts' +import { TELEMETRY_APIS } from './constants.ts' const DEFAULT_MAX_RETRY_TIME_MS = 30 * 1000 // 30 seconds const DEFAULT_INITIAL_RETRY_DELAY_MS = 1000 // 1 seconds const DEFAULT_RETRY_DELAY_MULTIPLIER = 2.0 const DEFAULT_RETRY_DELAY_JITTER_FACTOR = 0.2 -type TransactionCreator = () => TransactionPromise +type TransactionCreator = (apiTransactionConfig?: NonAutoCommitApiTelemetryConfig) => TransactionPromise type TransactionWork = (tx: Tx) => T | Promise type Resolve = (value: T | PromiseLike) => void type Reject = (value: any) => void @@ -48,6 +48,8 @@ function clearTimeoutWrapper (timeoutId: Timeout): void { return clearTimeout(timeoutId) } +interface ExecutionContext { apiTransactionConfig?: NonAutoCommitApiTelemetryConfig } + export class TransactionExecutor { private readonly _maxRetryTimeMs: number private readonly _initialRetryDelayMs: number @@ -56,6 +58,7 @@ export class TransactionExecutor { private _inFlightTimeoutIds: Timeout[] private readonly _setTimeout: SetTimeout private readonly _clearTimeout: ClearTimeout + public telemetryApi: NonAutoCommitTelemetryApis public pipelineBegin: boolean constructor ( @@ -90,6 +93,7 @@ export class TransactionExecutor { this._inFlightTimeoutIds = [] this.pipelineBegin = false + this.telemetryApi = TELEMETRY_APIS.MANAGED_TRANSACTION this._verifyAfterConstruction() } @@ -99,13 +103,23 @@ export class TransactionExecutor { transactionWork: TransactionWork, transactionWrapper?: (tx: Transaction) => Tx ): Promise { + const context: ExecutionContext = { + apiTransactionConfig: { + api: this.telemetryApi, + onTelemetrySuccess: () => { + context.apiTransactionConfig = undefined + } + } + } + return new Promise((resolve, reject) => { this._executeTransactionInsidePromise( transactionCreator, transactionWork, resolve, reject, - transactionWrapper + transactionWrapper, + context ).catch(reject) }).catch(error => { const retryStartTimeMs = Date.now() @@ -116,7 +130,8 @@ export class TransactionExecutor { error, retryStartTimeMs, retryDelayMs, - transactionWrapper + transactionWrapper, + context ) }) } @@ -133,7 +148,8 @@ export class TransactionExecutor { error: Error, retryStartTime: number, retryDelayMs: number, - transactionWrapper?: (tx: Transaction) => Tx + transactionWrapper?: (tx: Transaction) => Tx, + executionContext?: ExecutionContext ): Promise { const elapsedTimeMs = Date.now() - retryStartTime @@ -153,7 +169,8 @@ export class TransactionExecutor { transactionWork, resolve, reject, - transactionWrapper + transactionWrapper, + executionContext ).catch(reject) }, nextRetryTime) // add newly created timeoutId to the list of all in-flight timeouts @@ -166,7 +183,8 @@ export class TransactionExecutor { error, retryStartTime, nextRetryDelayMs, - transactionWrapper + transactionWrapper, + executionContext ) }) } @@ -176,11 +194,16 @@ export class TransactionExecutor { transactionWork: TransactionWork, resolve: Resolve, reject: Reject, - transactionWrapper?: (tx: Transaction) => Tx + transactionWrapper?: (tx: Transaction) => Tx, + executionContext?: ExecutionContext ): Promise { let tx: Transaction try { - const txPromise = transactionCreator() + const txPromise = transactionCreator( + executionContext?.apiTransactionConfig != null + ? { ...executionContext?.apiTransactionConfig } + : undefined + ) tx = this.pipelineBegin ? txPromise : await txPromise } catch (error) { // failed to create a transaction diff --git a/packages/neo4j-driver-deno/lib/core/session.ts b/packages/neo4j-driver-deno/lib/core/session.ts index 08e1daf85..3e0626a1e 100644 --- a/packages/neo4j-driver-deno/lib/core/session.ts +++ b/packages/neo4j-driver-deno/lib/core/session.ts @@ -21,10 +21,10 @@ import { FailedObserver, ResultStreamObserver } from './internal/observers.ts' import { validateQueryAndParameters } from './internal/util.ts' -import { FETCH_ALL, ACCESS_MODE_READ, ACCESS_MODE_WRITE } from './internal/constants.ts' +import { FETCH_ALL, ACCESS_MODE_READ, ACCESS_MODE_WRITE, TELEMETRY_APIS } from './internal/constants.ts' import { newError } from './error.ts' import Result from './result.ts' -import Transaction from './transaction.ts' +import Transaction, { NonAutoCommitApiTelemetryConfig, NonAutoCommitTelemetryApis } from './transaction.ts' import { ConnectionHolder } from './internal/connection-holder.ts' import { TransactionExecutor } from './internal/transaction-executor.ts' import { Bookmarks } from './internal/bookmarks.ts' @@ -194,6 +194,9 @@ class Session { txConfig: autoCommitTxConfig, mode: this._mode, database: this._database, + apiTelemetryConfig: { + api: TELEMETRY_APIS.AUTO_COMMIT_TRANSACTION + }, impersonatedUser: this._impersonatedUser, afterComplete: (meta: any) => this._onCompleteCallback(meta, bookmarks), reactive: this._reactive, @@ -293,10 +296,10 @@ class Session { txConfig = new TxConfig(arg, this._log) } - return this._beginTransaction(this._mode, txConfig) + return this._beginTransaction(this._mode, txConfig, { api: TELEMETRY_APIS.UNMANAGED_TRANSACTION }) } - _beginTransaction (accessMode: SessionMode, txConfig: TxConfig): TransactionPromise { + _beginTransaction (accessMode: SessionMode, txConfig: TxConfig, apiTelemetryConfig?: NonAutoCommitApiTelemetryConfig): TransactionPromise { if (!this._open) { throw newError('Cannot begin a transaction on a closed session.') } @@ -322,7 +325,8 @@ class Session { fetchSize: this._fetchSize, lowRecordWatermark: this._lowRecordWatermark, highRecordWatermark: this._highRecordWatermark, - notificationFilter: this._notificationFilter + notificationFilter: this._notificationFilter, + apiTelemetryConfig }) tx._begin(() => this._bookmarks(), txConfig) return tx @@ -431,7 +435,7 @@ class Session { transactionWork: TransactionWork ): Promise { return this._transactionExecutor.execute( - () => this._beginTransaction(accessMode, transactionConfig), + (apiTelemetryConfig?: NonAutoCommitApiTelemetryConfig) => this._beginTransaction(accessMode, transactionConfig, apiTelemetryConfig), transactionWork ) } @@ -493,7 +497,7 @@ class Session { transactionWork: ManagedTransactionWork ): Promise { return this._transactionExecutor.execute( - () => this._beginTransaction(accessMode, transactionConfig), + (apiTelemetryConfig?: NonAutoCommitApiTelemetryConfig) => this._beginTransaction(accessMode, transactionConfig, apiTelemetryConfig), transactionWork, ManagedTransaction.fromTransaction ) @@ -594,13 +598,15 @@ class Session { } /** - * Configure the transaction executor to pipeline transaction begin. + * Configure the transaction executor * + * This used by {@link Driver#executeQuery} * @private * @returns {void} */ - private _setTxExecutorToPipelineBegin (pipelined: boolean): void { + private _configureTransactionExecutor (pipelined: boolean, telemetryApi: NonAutoCommitTelemetryApis): void { this._transactionExecutor.pipelineBegin = pipelined + this._transactionExecutor.telemetryApi = telemetryApi } /** diff --git a/packages/neo4j-driver-deno/lib/core/transaction-promise.ts b/packages/neo4j-driver-deno/lib/core/transaction-promise.ts index a7d547fc1..4c3e7b6b6 100644 --- a/packages/neo4j-driver-deno/lib/core/transaction-promise.ts +++ b/packages/neo4j-driver-deno/lib/core/transaction-promise.ts @@ -19,7 +19,7 @@ /* eslint-disable @typescript-eslint/promise-function-async */ -import Transaction from './transaction.ts' +import Transaction, { NonAutoCommitApiTelemetryConfig } from './transaction.ts' import { ConnectionHolder } from './internal/connection-holder.ts' @@ -58,6 +58,7 @@ class TransactionPromise extends Transaction implements Promise { * @param {number} args.fetchSize - the record fetch size in each pulling batch. * @param {string} args.impersonatedUser - The name of the user which should be impersonated for the duration of the session. * @param {NotificationFilter} args.notificationFilter - The notification filter used for this transaction. + * @param {NonAutoCommitApiTelemetryConfig} args.apiTelemetryConfig - The api telemetry configuration. Empty/Null for disabling telemetry */ constructor ({ connectionHolder, @@ -69,7 +70,8 @@ class TransactionPromise extends Transaction implements Promise { impersonatedUser, highRecordWatermark, lowRecordWatermark, - notificationFilter + notificationFilter, + apiTelemetryConfig }: { connectionHolder: ConnectionHolder onClose: () => void @@ -81,6 +83,7 @@ class TransactionPromise extends Transaction implements Promise { highRecordWatermark: number lowRecordWatermark: number notificationFilter?: NotificationFilter + apiTelemetryConfig?: NonAutoCommitApiTelemetryConfig }) { super({ connectionHolder, @@ -92,7 +95,8 @@ class TransactionPromise extends Transaction implements Promise { impersonatedUser, highRecordWatermark, lowRecordWatermark, - notificationFilter + notificationFilter, + apiTelemetryConfig }) } diff --git a/packages/neo4j-driver-deno/lib/core/transaction.ts b/packages/neo4j-driver-deno/lib/core/transaction.ts index 703577a97..577093d9d 100644 --- a/packages/neo4j-driver-deno/lib/core/transaction.ts +++ b/packages/neo4j-driver-deno/lib/core/transaction.ts @@ -19,7 +19,7 @@ /* eslint-disable @typescript-eslint/promise-function-async */ import { validateQueryAndParameters } from './internal/util.ts' -import Connection from './connection.ts' +import Connection, { ApiTelemetryConfig } from './connection.ts' import { ConnectionHolder, ReadOnlyConnectionHolder, @@ -39,6 +39,10 @@ import Result from './result.ts' import { Query } from './types.ts' import { RecordShape } from './record.ts' import NotificationFilter from './notification-filter.ts' +import { TelemetryApis, TELEMETRY_APIS } from './internal/constants.ts' + +type NonAutoCommitTelemetryApis = Exclude +type NonAutoCommitApiTelemetryConfig = ApiTelemetryConfig /** * Represents a transaction in the Neo4j database. @@ -63,6 +67,7 @@ class Transaction { private readonly _activePromise: Promise private _acceptActive: () => void private readonly _notificationFilter?: NotificationFilter + private readonly _apiTelemetryConfig?: NonAutoCommitApiTelemetryConfig /** * @constructor @@ -78,6 +83,7 @@ class Transaction { * @param {number} args.highRecordWatermark - The high watermark for the record buffer. * @param {number} args.lowRecordWatermark - The low watermark for the record buffer. * @param {NotificationFilter} args.notificationFilter - The notification filter used for this transaction. + * @param {NonAutoCommitApiTelemetryConfig} args.apiTelemetryConfig - The api telemetry configuration. Empty/Null for disabling telemetry */ constructor ({ connectionHolder, @@ -89,7 +95,8 @@ class Transaction { impersonatedUser, highRecordWatermark, lowRecordWatermark, - notificationFilter + notificationFilter, + apiTelemetryConfig }: { connectionHolder: ConnectionHolder onClose: () => void @@ -101,6 +108,7 @@ class Transaction { highRecordWatermark: number lowRecordWatermark: number notificationFilter?: NotificationFilter + apiTelemetryConfig?: NonAutoCommitApiTelemetryConfig }) { this._connectionHolder = connectionHolder this._reactive = reactive @@ -117,6 +125,7 @@ class Transaction { this._highRecordWatermark = highRecordWatermark this._bookmarks = Bookmarks.empty() this._notificationFilter = notificationFilter + this._apiTelemetryConfig = apiTelemetryConfig this._acceptActive = () => { } // satisfy DenoJS this._activePromise = new Promise((resolve, reject) => { this._acceptActive = resolve @@ -127,6 +136,7 @@ class Transaction { * @private * @param {Bookmarks | string | string []} bookmarks * @param {TxConfig} txConfig + * @param {Object} events List of observers to events * @returns {void} */ _begin (getBookmarks: () => Promise, txConfig: TxConfig, events?: { @@ -146,6 +156,7 @@ class Transaction { database: this._connectionHolder.database(), impersonatedUser: this._impersonatedUser, notificationFilter: this._notificationFilter, + apiTelemetryConfig: this._apiTelemetryConfig, beforeError: (error: Error) => { if (events != null) { events.onError(error) @@ -706,3 +717,7 @@ function newCompletedResult ( } export default Transaction +export type { + NonAutoCommitTelemetryApis, + NonAutoCommitApiTelemetryConfig +} diff --git a/packages/neo4j-driver-deno/lib/core/types.ts b/packages/neo4j-driver-deno/lib/core/types.ts index d595841b4..8c88aca5f 100644 --- a/packages/neo4j-driver-deno/lib/core/types.ts +++ b/packages/neo4j-driver-deno/lib/core/types.ts @@ -80,6 +80,7 @@ export class Config { logging?: LoggingConfig resolver?: (address: string) => string[] | Promise userAgent?: string + telemetryDisabled?: boolean /** * @constructor @@ -285,6 +286,34 @@ export class Config { * @type {string|undefined} */ this.userAgent = undefined + + /** + * Specify if telemetry collection is disabled. + * + * By default, the driver will send anonymous usage statistics to the server it connects to if the server requests those. + * By setting ``telemetryDisabled=true``, the driver will not send any telemetry data. + * + * The driver transmits the following information: + * + * Every time one of the following APIs is used to execute a query (for the first time), the server is informed of this + * (without any further information like arguments, client identifiers, etc.): + * + * * {@link Driver#executeQuery} + * * {@link Session#run} + * * {@link Session#beginTransaction} + * * {@link Session#executeRead} + * * {@link Session#executeWrite} + * * {@link Session#writeTransaction} + * * {@link Session#readTransaction} + * * The reactive counterparts of methods above. + * + * Metrics are only collected when enabled both in server and driver instances. + * + * **Default**: ```false``` + * + * @type {boolean} + */ + this.telemetryDisabled = false } } diff --git a/packages/neo4j-driver/src/session-rx.js b/packages/neo4j-driver/src/session-rx.js index 6a0cad1ae..1cdef102a 100644 --- a/packages/neo4j-driver/src/session-rx.js +++ b/packages/neo4j-driver/src/session-rx.js @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { defer, Observable, throwError } from 'rxjs' +import { defer, Observable, of, throwError } from 'rxjs' import { mergeMap, catchError, concatWith } from 'rxjs/operators' import RxResult from './result-rx' // eslint-disable-next-line no-unused-vars @@ -26,7 +26,7 @@ import RxManagedTransaction from './transaction-managed-rx' import RxRetryLogic from './internal/retry-logic-rx' const { - constants: { ACCESS_MODE_READ, ACCESS_MODE_WRITE }, + constants: { ACCESS_MODE_READ, ACCESS_MODE_WRITE, TELEMETRY_APIS }, txConfig: { TxConfig } } = internal @@ -80,7 +80,7 @@ export default class RxSession { * @returns {Observable} - A reactive stream that will generate at most **one** RxTransaction instance. */ beginTransaction (transactionConfig) { - return this._beginTransaction(this._session._mode, transactionConfig) + return this._beginTransaction(this._session._mode, transactionConfig, { api: TELEMETRY_APIS.UNMANAGED_TRANSACTION }) } /** @@ -198,7 +198,7 @@ export default class RxSession { /** * @private */ - _beginTransaction (accessMode, transactionConfig) { + _beginTransaction (accessMode, transactionConfig, apiTelemetryConfig) { let txConfig = TxConfig.empty() if (transactionConfig) { txConfig = new TxConfig(transactionConfig, this._log) @@ -206,7 +206,7 @@ export default class RxSession { return new Observable(observer => { try { - this._session._beginTransaction(accessMode, txConfig) + this._session._beginTransaction(accessMode, txConfig, apiTelemetryConfig) .then(tx => { observer.next( new RxTransaction(tx) @@ -231,8 +231,18 @@ export default class RxSession { txConfig = new TxConfig(transactionConfig) } + const context = { + apiTelemetryConfig: { + api: TELEMETRY_APIS.MANAGED_TRANSACTION, + onTelemetrySuccess: () => { + context.apiTelemetryConfig = undefined + } + } + } + return this._retryLogic.retry( - this._beginTransaction(accessMode, txConfig).pipe( + of(1).pipe( + mergeMap(() => this._beginTransaction(accessMode, txConfig, context.apiTelemetryConfig)), mergeMap(txc => defer(() => { try { diff --git a/packages/neo4j-driver/test/internal/transaction-executor.test.js b/packages/neo4j-driver/test/internal/transaction-executor.test.js index 60763e8ae..26cf65926 100644 --- a/packages/neo4j-driver/test/internal/transaction-executor.test.js +++ b/packages/neo4j-driver/test/internal/transaction-executor.test.js @@ -314,6 +314,107 @@ describe('#unit TransactionExecutor', () => { expect(context.workCalls).toEqual(2) }) + ;[ + [0, executor => executor], + [3, executor => { + executor.telemetryApi = 3 + return executor + }] + ].forEach(([telemetryApi, applyChangesToExecutor]) => { + describe(`telemetry when api is ${telemetryApi}`, () => { + let executor + + beforeEach(() => { + const executorAndFakeSetTimeout = createTransactionExecutorWithFakeTimeout() + executor = applyChangesToExecutor(executorAndFakeSetTimeout.executor) + }) + + it(`should create transaction with telemetryApi equals to ${telemetryApi}`, async () => { + const tx = new FakeTransaction() + const transactionCreator = spyOnFunction(({ onTelemetrySuccess }) => { + onTelemetrySuccess() + return tx + }) + + await executor.execute(transactionCreator, async (x) => 1) + + expect(transactionCreator.calls.length).toBe(1) + expect(transactionCreator.calls[0][0].api).toBe(telemetryApi) + }) + + it('should not send metric on the retry when metrics sent with success', async () => { + const transactions = [ + new FakeTransaction(undefined, undefined, TRANSIENT_ERROR_1), + new FakeTransaction() + ] + + const transactionCreator = spyOnFunction(({ onTelemetrySuccess } = {}) => { + if (onTelemetrySuccess) { + onTelemetrySuccess() + } + return transactions.shift() + }) + + await executor.execute(transactionCreator, async (x) => 1) + + expect(transactionCreator.calls.length).toBe(2) + expect(transactionCreator.calls[0][0].api).toBe(telemetryApi) + expect(transactionCreator.calls[1][0]).toBe(undefined) + }) + + it('should send metrics on the retry when metrics sent without success', async () => { + const transactions = [ + new FakeTransaction(undefined, undefined, TRANSIENT_ERROR_1), + new FakeTransaction() + ] + + const transactionCreator = spyOnFunction(({ onTelemetrySuccess }) => { + if (transactions.length === 1) { + onTelemetrySuccess() + } + return transactions.shift() + }) + + await executor.execute(transactionCreator, async (x) => 1) + + expect(transactionCreator.calls.length).toBe(2) + expect(transactionCreator.calls[0][0].api).toBe(telemetryApi) + expect(transactionCreator.calls[1][0].api).toBe(telemetryApi) + }) + + it('should isolate execution context', async () => { + const tx = new FakeTransaction() + const transactionCreator = spyOnFunction(({ onTelemetrySuccess }) => { + onTelemetrySuccess() + return tx + }) + + await executor.execute(transactionCreator, async (x) => 1) + await executor.execute(transactionCreator, async (x) => 1) + + expect(transactionCreator.calls.length).toBe(2) + expect(transactionCreator.calls[0][0].api).toBe(telemetryApi) + expect(transactionCreator.calls[1][0].api).toBe(telemetryApi) + }) + + afterEach(async () => { + await executor.close() + }) + + function spyOnFunction (fun) { + const context = { + calls: [] + } + function myFunction (...args) { + context.calls.push(args) + return fun(...args) + } + + return Object.defineProperty(myFunction, 'calls', { get: () => context.calls }) + } + }) + }) + async function testRetryWhenTransactionCreatorFails (errorCodes) { const { fakeSetTimeout, executor } = createTransactionExecutorWithFakeTimeout() executor.pipelineBegin = pipelineBegin diff --git a/packages/neo4j-driver/test/rx/session.test.js b/packages/neo4j-driver/test/rx/session.test.js index 2aee9aa26..52548dc21 100644 --- a/packages/neo4j-driver/test/rx/session.test.js +++ b/packages/neo4j-driver/test/rx/session.test.js @@ -17,7 +17,7 @@ * limitations under the License. */ -import { Notification, throwError } from 'rxjs' +import { Notification, of, throwError, firstValueFrom } from 'rxjs' import { map, materialize, toArray, concatWith } from 'rxjs/operators' import neo4j, { ResultSummary, int } from '../../src' import RxSession from '../../src/session-rx' @@ -31,7 +31,7 @@ import { } from 'neo4j-driver-core' const { SERVICE_UNAVAILABLE, SESSION_EXPIRED } = error -const { bookmarks, logger } = internal +const { bookmarks, logger, constants: { TELEMETRY_APIS } } = internal describe('#integration rx-session', () => { let driver @@ -547,6 +547,138 @@ describe('#unit rx-session', () => { }) }) + describe('.beginTransaction()', () => { + it(`should send telemetry configuration with API equals to ${TELEMETRY_APIS.UNMANAGED_TRANSACTION}`, () => { + const capture = [] + const _session = { + _beginTransaction: async (...args) => { + capture.push(args) + return {} + } + } + + const session = new RxSession({ + session: _session + }) + + const timeout = 0.2 + + session.beginTransaction({ timeout }) + .subscribe() + + expect(capture[0][2]).toEqual({ + api: TELEMETRY_APIS.UNMANAGED_TRANSACTION + }) + }) + }) + + ;[ + 'executeRead', + 'executeWrite', + 'readTransaction', + 'writeTransaction' + ].forEach(txFun => { + describe(`.${txFun}()`, () => { + it(`should send telemetry configuration with API equals to ${TELEMETRY_APIS.MANAGED_TRANSACTION}`, async () => { + const capture = [] + const _session = { + _beginTransaction: async (...args) => { + capture.push(args) + return { + commit: () => Promise.resolve(), + rollback: () => Promise.resolve(), + run: () => Promise.resolve(), + isOpen: () => true + } + } + } + + const session = new RxSession({ + session: _session + }) + + const fun = session[txFun].bind(session) + await firstValueFrom(fun(() => { return of(0) })) + + expect(capture.length).toEqual(1) + expect(capture[0][2]).toEqual(jasmine.objectContaining({ + api: TELEMETRY_APIS.MANAGED_TRANSACTION + })) + }) + + it('should send telemetry on retry original when telemetry doesn\'t succeeded', async () => { + const capture = [] + const errors = [newError('message', SERVICE_UNAVAILABLE)] + const _session = { + _beginTransaction: async (...args) => { + capture.push(args) + const error = errors.pop() + if (error) { + throw error + } + return { + commit: () => Promise.resolve(), + rollback: () => Promise.resolve(), + run: () => Promise.resolve(), + isOpen: () => true + } + } + } + + const session = new RxSession({ + session: _session + }) + + const fun = session[txFun].bind(session) + await firstValueFrom(fun(() => { return of(0) })) + + expect(capture.length).toEqual(2) + expect(capture[0][2]).toEqual(jasmine.objectContaining({ + api: TELEMETRY_APIS.MANAGED_TRANSACTION + })) + expect(capture[1][2]).toEqual(jasmine.objectContaining({ + api: TELEMETRY_APIS.MANAGED_TRANSACTION + })) + }) + + it('should not send telemetry on retry original when telemetry succeeded', async () => { + const capture = [] + const errors = [newError('message', SERVICE_UNAVAILABLE)] + const _session = { + _beginTransaction: async (...args) => { + capture.push(args) + if (args[2] && args[2].onTelemetrySuccess) { + args[2].onTelemetrySuccess() + } + const error = errors.pop() + if (error) { + throw error + } + return { + commit: () => Promise.resolve(), + rollback: () => Promise.resolve(), + run: () => Promise.resolve(), + isOpen: () => true + } + } + } + + const session = new RxSession({ + session: _session + }) + + const fun = session[txFun].bind(session) + await firstValueFrom(fun(() => { return of(0) })) + + expect(capture.length).toEqual(2) + expect(capture[0][2]).toEqual(jasmine.objectContaining({ + api: TELEMETRY_APIS.MANAGED_TRANSACTION + })) + expect(capture[1][2]).toEqual(undefined) + }) + }) + }) + function newSession (lastBookmarks = bookmarks.Bookmarks.empty()) { const connectionProvider = new ConnectionProvider() connectionProvider.acquireConnection = () => Promise.resolve(null) diff --git a/packages/testkit-backend/src/feature/common.js b/packages/testkit-backend/src/feature/common.js index 9dc77280b..67887a714 100644 --- a/packages/testkit-backend/src/feature/common.js +++ b/packages/testkit-backend/src/feature/common.js @@ -22,6 +22,7 @@ const features = [ 'Feature:Bolt:5.1', 'Feature:Bolt:5.2', 'Feature:Bolt:5.3', + 'Feature:Bolt:5.4', 'Feature:Bolt:Patch:UTC', 'Feature:API:ConnectionAcquisitionTimeout', 'Feature:API:Driver.ExecuteQuery', diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index 47b17b876..bdb98bc2b 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -30,10 +30,10 @@ export function NewDriver ({ neo4j }, context, data, wire) { const resolver = resolverRegistered ? address => - new Promise((resolve, reject) => { - const id = context.addResolverRequest(resolve, reject) - wire.writeResponse(responses.ResolverResolutionRequired({ id, address })) - }) + new Promise((resolve, reject) => { + const id = context.addResolverRequest(resolve, reject) + wire.writeResponse(responses.ResolverResolutionRequired({ id, address })) + }) : undefined const config = { @@ -78,6 +78,10 @@ export function NewDriver ({ neo4j }, context, data, wire) { disabledCategories: data.notificationsDisabledCategories } } + if ('telemetryDisabled' in data) { + config.telemetryDisabled = data.telemetryDisabled + } + let driver try { driver = neo4j.driver(uri, parsedAuthToken, config)