dd-trace 5.1.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,10 +17,7 @@ require,istanbul-lib-coverage,BSD-3-Clause,Copyright 2012-2015 Yahoo! Inc.
17
17
  require,jest-docblock,MIT,Copyright Meta Platforms, Inc. and affiliates.
18
18
  require,koalas,MIT,Copyright 2013-2017 Brian Woodward
19
19
  require,limiter,MIT,Copyright 2011 John Hurliman
20
- require,lodash.kebabcase,MIT,Copyright JS Foundation and other contributors
21
- require,lodash.pick,MIT,Copyright JS Foundation and other contributors
22
20
  require,lodash.sortby,MIT,Copyright JS Foundation and other contributors
23
- require,lodash.uniq,MIT,Copyright JS Foundation and other contributors
24
21
  require,lru-cache,ISC,Copyright (c) 2010-2022 Isaac Z. Schlueter and Contributors
25
22
  require,methods,MIT,Copyright 2013-2014 TJ Holowaychuk
26
23
  require,module-details-from-path,MIT,Copyright 2016 Thomas Watson Steen
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "5.1.0",
3
+ "version": "5.2.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -87,10 +87,7 @@
87
87
  "jest-docblock": "^29.7.0",
88
88
  "koalas": "^1.0.2",
89
89
  "limiter": "1.1.5",
90
- "lodash.kebabcase": "^4.1.1",
91
- "lodash.pick": "^4.4.0",
92
90
  "lodash.sortby": "^4.7.0",
93
- "lodash.uniq": "^4.5.0",
94
91
  "lru-cache": "^7.14.0",
95
92
  "methods": "^1.1.2",
96
93
  "module-details-from-path": "^1.0.3",
@@ -5,7 +5,7 @@ const {
5
5
  addHook,
6
6
  AsyncResource
7
7
  } = require('./helpers/instrument')
8
- const kebabCase = require('lodash.kebabcase')
8
+ const kebabCase = require('../../utils/src/kebabcase')
9
9
  const shimmer = require('../../datadog-shimmer')
10
10
 
11
11
  const startCh = channel('apm:amqplib:command:start')
@@ -73,6 +73,7 @@ module.exports = {
73
73
  'mongodb': () => require('../mongodb'),
74
74
  'mongodb-core': () => require('../mongodb-core'),
75
75
  'mongoose': () => require('../mongoose'),
76
+ 'mquery': () => require('../mquery'),
76
77
  'mysql': () => require('../mysql'),
77
78
  'mysql2': () => require('../mysql2'),
78
79
  'net': () => require('../net'),
@@ -0,0 +1,65 @@
1
+ 'use strict'
2
+
3
+ const dc = require('dc-polyfill')
4
+ const {
5
+ channel,
6
+ addHook
7
+ } = require('./helpers/instrument')
8
+ const shimmer = require('../../datadog-shimmer')
9
+
10
+ const prepareCh = channel('datadog:mquery:filter:prepare')
11
+ const tracingCh = dc.tracingChannel('datadog:mquery:filter')
12
+
13
+ const methods = [
14
+ 'find',
15
+ 'findOne',
16
+ 'findOneAndRemove',
17
+ 'findOneAndDelete',
18
+ 'count',
19
+ 'distinct',
20
+ 'where'
21
+ ]
22
+
23
+ const methodsOptionalArgs = ['findOneAndUpdate']
24
+
25
+ function getFilters (args, methodName) {
26
+ const [arg0, arg1] = args
27
+
28
+ const filters = arg0 && typeof arg0 === 'object' ? [arg0] : []
29
+
30
+ if (arg1 && typeof arg1 === 'object' && methodsOptionalArgs.includes(methodName)) {
31
+ filters.push(arg1)
32
+ }
33
+
34
+ return filters
35
+ }
36
+
37
+ addHook({
38
+ name: 'mquery',
39
+ versions: ['>=5.0.0']
40
+ }, Query => {
41
+ [...methods, ...methodsOptionalArgs].forEach(methodName => {
42
+ if (!(methodName in Query.prototype)) return
43
+
44
+ shimmer.wrap(Query.prototype, methodName, method => {
45
+ return function () {
46
+ if (prepareCh.hasSubscribers) {
47
+ const filters = getFilters(arguments, methodName)
48
+ if (filters?.length) {
49
+ prepareCh.publish({ filters })
50
+ }
51
+ }
52
+
53
+ return method.apply(this, arguments)
54
+ }
55
+ })
56
+ })
57
+
58
+ shimmer.wrap(Query.prototype, 'exec', originalExec => {
59
+ return function wrappedExec () {
60
+ return tracingCh.tracePromise(originalExec, {}, this, arguments)
61
+ }
62
+ })
63
+
64
+ return Query
65
+ })
@@ -1,6 +1,11 @@
1
1
  'use strict'
2
+ const {
3
+ CONTEXT_PROPAGATION_KEY
4
+ } = require('../../../dd-trace/src/datastreams/processor')
5
+ const { encodePathwayContext } = require('../../../dd-trace/src/datastreams/pathway')
2
6
  const log = require('../../../dd-trace/src/log')
3
7
  const BaseAwsSdkPlugin = require('../base')
8
+
4
9
  class Kinesis extends BaseAwsSdkPlugin {
5
10
  static get id () { return 'kinesis' }
6
11
  static get peerServicePrecursors () { return ['streamname'] }
@@ -37,8 +42,9 @@ class Kinesis extends BaseAwsSdkPlugin {
37
42
  if (!request.params) {
38
43
  return
39
44
  }
40
-
41
45
  const traceData = {}
46
+
47
+ // inject data with DD context
42
48
  this.tracer.inject(span, 'text_map', traceData)
43
49
  let injectPath
44
50
  if (request.params.Records && request.params.Records.length > 0) {
@@ -49,9 +55,30 @@ class Kinesis extends BaseAwsSdkPlugin {
49
55
  log.error('No valid payload passed, unable to pass trace context')
50
56
  return
51
57
  }
58
+
52
59
  const parsedData = this._tryParse(injectPath.Data)
53
60
  if (parsedData) {
54
61
  parsedData._datadog = traceData
62
+
63
+ // set DSM hash if enabled
64
+ if (this.config.dsmEnabled) {
65
+ // get payload size of request data
66
+ const payloadSize = Buffer.from(JSON.stringify(parsedData)).byteLength
67
+ let stream
68
+ // users can optionally use either stream name or stream arn
69
+ if (request.params && request.params.StreamArn) {
70
+ stream = request.params.StreamArn
71
+ } else if (request.params && request.params.StreamName) {
72
+ stream = request.params.StreamName
73
+ }
74
+ const dataStreamsContext = this.tracer
75
+ .setCheckpoint(['direction:out', `topic:${stream}`, 'type:kinesis'], span, payloadSize)
76
+ if (dataStreamsContext) {
77
+ const pathwayCtx = encodePathwayContext(dataStreamsContext)
78
+ parsedData._datadog[CONTEXT_PROPAGATION_KEY] = pathwayCtx.toJSON()
79
+ }
80
+ }
81
+
55
82
  const finalData = Buffer.from(JSON.stringify(parsedData))
56
83
  const byteSize = finalData.length
57
84
  // Kinesis max payload size is 1MB
@@ -1,4 +1,6 @@
1
1
  'use strict'
2
+ const { CONTEXT_PROPAGATION_KEY, getHeadersSize } = require('../../../dd-trace/src/datastreams/processor')
3
+ const { encodePathwayContext } = require('../../../dd-trace/src/datastreams/pathway')
2
4
  const log = require('../../../dd-trace/src/log')
3
5
  const BaseAwsSdkPlugin = require('../base')
4
6
 
@@ -11,6 +13,7 @@ class Sns extends BaseAwsSdkPlugin {
11
13
 
12
14
  if (!params.TopicArn && !(response.data && response.data.TopicArn)) return {}
13
15
  const TopicArn = params.TopicArn || response.data.TopicArn
16
+
14
17
  // Split the ARN into its parts
15
18
  // ex.'arn:aws:sns:us-east-1:123456789012:my-topic'
16
19
  const arnParts = TopicArn.split(':')
@@ -72,10 +75,25 @@ class Sns extends BaseAwsSdkPlugin {
72
75
  }
73
76
  const ddInfo = {}
74
77
  this.tracer.inject(span, 'text_map', ddInfo)
78
+ // add ddInfo before checking DSM so we can include DD attributes in payload size
75
79
  params.MessageAttributes._datadog = {
76
80
  DataType: 'Binary',
77
- BinaryValue: Buffer.from(JSON.stringify(ddInfo)) // BINARY types are automatically base64 encoded
81
+ BinaryValue: ddInfo
82
+ }
83
+ if (this.config.dsmEnabled) {
84
+ const payloadSize = getHeadersSize({
85
+ Message: params.Message,
86
+ MessageAttributes: params.MessageAttributes
87
+ })
88
+ const dataStreamsContext = this.tracer
89
+ .setCheckpoint(['direction:out', `topic:${params.TopicArn}`, 'type:sns'], span, payloadSize)
90
+ if (dataStreamsContext) {
91
+ const pathwayCtx = encodePathwayContext(dataStreamsContext)
92
+ ddInfo[CONTEXT_PROPAGATION_KEY] = pathwayCtx.toJSON()
93
+ }
78
94
  }
95
+ // BINARY types are automatically base64 encoded
96
+ params.MessageAttributes._datadog.BinaryValue = Buffer.from(JSON.stringify(ddInfo))
79
97
  }
80
98
  }
81
99
 
@@ -3,6 +3,8 @@
3
3
  const log = require('../../../dd-trace/src/log')
4
4
  const BaseAwsSdkPlugin = require('../base')
5
5
  const { storage } = require('../../../datadog-core')
6
+ const { CONTEXT_PROPAGATION_KEY, getHeadersSize } = require('../../../dd-trace/src/datastreams/processor')
7
+ const { encodePathwayContext } = require('../../../dd-trace/src/datastreams/pathway')
6
8
 
7
9
  class Sqs extends BaseAwsSdkPlugin {
8
10
  static get id () { return 'sqs' }
@@ -19,20 +21,27 @@ class Sqs extends BaseAwsSdkPlugin {
19
21
  const { request, response } = obj
20
22
  const store = storage.getStore()
21
23
  const plugin = this
22
- const maybeChildOf = this.responseExtract(request.params, request.operation, response)
23
- if (maybeChildOf) {
24
+ const contextExtraction = this.responseExtract(request.params, request.operation, response)
25
+ let span
26
+ let parsedMessageAttributes
27
+ if (contextExtraction && contextExtraction.datadogContext) {
24
28
  obj.needsFinish = true
25
29
  const options = {
26
- childOf: maybeChildOf,
30
+ childOf: contextExtraction.datadogContext,
27
31
  tags: Object.assign(
28
32
  {},
29
33
  this.requestTags.get(request) || {},
30
34
  { 'span.kind': 'server' }
31
35
  )
32
36
  }
33
- const span = plugin.tracer.startSpan('aws.response', options)
37
+ parsedMessageAttributes = contextExtraction.parsedAttributes
38
+ span = plugin.tracer.startSpan('aws.response', options)
34
39
  this.enter(span, store)
35
40
  }
41
+ // extract DSM context after as we might not have a parent-child but may have a DSM context
42
+ this.responseExtractDSMContext(
43
+ request.operation, request.params, response, span ?? null, parsedMessageAttributes ?? null
44
+ )
36
45
  })
37
46
 
38
47
  this.addSub('apm:aws:response:finish:sqs', err => {
@@ -133,19 +142,69 @@ class Sqs extends BaseAwsSdkPlugin {
133
142
 
134
143
  const datadogAttribute = message.MessageAttributes._datadog
135
144
 
145
+ const parsedAttributes = this.parseDatadogAttributes(datadogAttribute)
146
+ if (parsedAttributes) {
147
+ return {
148
+ datadogContext: this.tracer.extract('text_map', parsedAttributes),
149
+ parsedAttributes: parsedAttributes
150
+ }
151
+ }
152
+ }
153
+
154
+ parseDatadogAttributes (attributes) {
136
155
  try {
137
- if (datadogAttribute.StringValue) {
138
- const textMap = datadogAttribute.StringValue
139
- return this.tracer.extract('text_map', JSON.parse(textMap))
140
- } else if (datadogAttribute.Type === 'Binary') {
141
- const buffer = Buffer.from(datadogAttribute.Value, 'base64')
142
- return this.tracer.extract('text_map', JSON.parse(buffer))
156
+ if (attributes.StringValue) {
157
+ const textMap = attributes.StringValue
158
+ return JSON.parse(textMap)
159
+ } else if (attributes.Type === 'Binary') {
160
+ const buffer = Buffer.from(attributes.Value, 'base64')
161
+ return JSON.parse(buffer)
143
162
  }
144
163
  } catch (e) {
145
164
  log.error(e)
146
165
  }
147
166
  }
148
167
 
168
+ responseExtractDSMContext (operation, params, response, span, parsedAttributes) {
169
+ if (!this.config.dsmEnabled) return
170
+ if (operation !== 'receiveMessage') return
171
+ if (!response || !response.Messages || !response.Messages[0]) return
172
+
173
+ // we only want to set the payloadSize on the span if we have one message
174
+ span = response.Messages.length > 1 ? null : span
175
+
176
+ response.Messages.forEach(message => {
177
+ // we may have already parsed the message attributes when extracting trace context
178
+ if (!parsedAttributes) {
179
+ if (message.Body) {
180
+ try {
181
+ const body = JSON.parse(message.Body)
182
+
183
+ // SNS to SQS
184
+ if (body.Type === 'Notification') {
185
+ message = body
186
+ }
187
+ } catch (e) {
188
+ // SQS to SQS
189
+ }
190
+ }
191
+ if (message.MessageAttributes && message.MessageAttributes._datadog) {
192
+ parsedAttributes = this.parseDatadogAttributes(message.MessageAttributes._datadog)
193
+ }
194
+ }
195
+ if (parsedAttributes && parsedAttributes[CONTEXT_PROPAGATION_KEY]) {
196
+ const payloadSize = getHeadersSize({
197
+ Body: message.Body,
198
+ MessageAttributes: message.MessageAttributes
199
+ })
200
+ const queue = params.QueueUrl.split('/').pop()
201
+ this.tracer.decodeDataStreamsContext(Buffer.from(parsedAttributes[CONTEXT_PROPAGATION_KEY]))
202
+ this.tracer
203
+ .setCheckpoint(['direction:in', `topic:${queue}`, 'type:sqs'], span, payloadSize)
204
+ }
205
+ })
206
+ }
207
+
149
208
  requestInject (span, request) {
150
209
  const operation = request.operation
151
210
  if (operation === 'sendMessage') {
@@ -164,6 +223,20 @@ class Sqs extends BaseAwsSdkPlugin {
164
223
  DataType: 'String',
165
224
  StringValue: JSON.stringify(ddInfo)
166
225
  }
226
+ if (this.config.dsmEnabled) {
227
+ const payloadSize = getHeadersSize({
228
+ Body: request.params.MessageBody,
229
+ MessageAttributes: request.params.MessageAttributes
230
+ })
231
+ const queue = request.params.QueueUrl.split('/').pop()
232
+ const dataStreamsContext = this.tracer
233
+ .setCheckpoint(['direction:out', `topic:${queue}`, 'type:sqs'], span, payloadSize)
234
+ if (dataStreamsContext) {
235
+ const pathwayCtx = encodePathwayContext(dataStreamsContext)
236
+ ddInfo[CONTEXT_PROPAGATION_KEY] = pathwayCtx.toJSON()
237
+ }
238
+ }
239
+ request.params.MessageAttributes._datadog.StringValue = JSON.stringify(ddInfo)
167
240
  }
168
241
  }
169
242
  }
@@ -1,5 +1,6 @@
1
1
  'use strict'
2
2
 
3
+ const pick = require('../../utils/src/pick')
3
4
  const CompositePlugin = require('../../dd-trace/src/plugins/composite')
4
5
  const log = require('../../dd-trace/src/log')
5
6
  const GraphQLExecutePlugin = require('./execute')
@@ -63,10 +64,4 @@ function getHooks (config) {
63
64
  return { execute, parse, validate }
64
65
  }
65
66
 
66
- // non-lodash pick
67
-
68
- function pick (obj, selectors) {
69
- return Object.fromEntries(Object.entries(obj).filter(([key]) => selectors.includes(key)))
70
- }
71
-
72
67
  module.exports = GraphQLPlugin
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const pick = require('lodash.pick')
3
+ const pick = require('../../utils/src/pick')
4
4
  const log = require('../../dd-trace/src/log')
5
5
 
6
6
  module.exports = {
@@ -9,7 +9,7 @@ const { storage } = require('../../../../../datadog-core')
9
9
  const { getIastContext } = require('../iast-context')
10
10
  const { HTTP_REQUEST_PARAMETER, HTTP_REQUEST_BODY } = require('../taint-tracking/source-types')
11
11
 
12
- const EXCLUDED_PATHS_FROM_STACK = getNodeModulesPaths('mongodb', 'mongoose')
12
+ const EXCLUDED_PATHS_FROM_STACK = getNodeModulesPaths('mongodb', 'mongoose', 'mquery')
13
13
  const MONGODB_NOSQL_SECURE_MARK = getNextSecureMark()
14
14
 
15
15
  function iterateObjectStrings (target, fn, levelKeys = [], depth = 50, visited = new Set()) {
@@ -37,34 +37,39 @@ class NosqlInjectionMongodbAnalyzer extends InjectionAnalyzer {
37
37
  onConfigure () {
38
38
  this.configureSanitizers()
39
39
 
40
- this.addSub('datadog:mongodb:collection:filter:start', ({ filters }) => {
40
+ const onStart = ({ filters }) => {
41
41
  const store = storage.getStore()
42
42
  if (store && !store.nosqlAnalyzed && filters?.length) {
43
43
  filters.forEach(filter => {
44
44
  this.analyze({ filter }, store)
45
45
  })
46
46
  }
47
- })
48
47
 
49
- this.addSub('datadog:mongoose:model:filter:start', ({ filters }) => {
50
- const store = storage.getStore()
51
- if (!store) return
48
+ return store
49
+ }
52
50
 
53
- if (filters?.length) {
54
- filters.forEach(filter => {
55
- this.analyze({ filter }, store)
56
- })
51
+ const onStartAndEnterWithStore = (message) => {
52
+ const store = onStart(message || {})
53
+ if (store) {
54
+ storage.enterWith({ ...store, nosqlAnalyzed: true, nosqlParentStore: store })
57
55
  }
56
+ }
58
57
 
59
- storage.enterWith({ ...store, nosqlAnalyzed: true, mongooseParentStore: store })
60
- })
61
-
62
- this.addSub('datadog:mongoose:model:filter:finish', () => {
58
+ const onFinish = () => {
63
59
  const store = storage.getStore()
64
- if (store?.mongooseParentStore) {
65
- storage.enterWith(store.mongooseParentStore)
60
+ if (store?.nosqlParentStore) {
61
+ storage.enterWith(store.nosqlParentStore)
66
62
  }
67
- })
63
+ }
64
+
65
+ this.addSub('datadog:mongodb:collection:filter:start', onStart)
66
+
67
+ this.addSub('datadog:mongoose:model:filter:start', onStartAndEnterWithStore)
68
+ this.addSub('datadog:mongoose:model:filter:finish', onFinish)
69
+
70
+ this.addSub('datadog:mquery:filter:prepare', onStart)
71
+ this.addSub('tracing:datadog:mquery:filter:start', onStartAndEnterWithStore)
72
+ this.addSub('tracing:datadog:mquery:filter:asyncEnd', onFinish)
68
73
  }
69
74
 
70
75
  configureSanitizers () {
@@ -125,6 +125,21 @@ function getSizeOrZero (obj) {
125
125
  if (Buffer.isBuffer(obj)) {
126
126
  return obj.length
127
127
  }
128
+ if (Array.isArray(obj) && obj.length > 0) {
129
+ if (typeof obj[0] === 'number') return Buffer.from(obj).length
130
+ let payloadSize = 0
131
+ obj.forEach(item => {
132
+ payloadSize += getSizeOrZero(item)
133
+ })
134
+ return payloadSize
135
+ }
136
+ if (typeof obj === 'object') {
137
+ try {
138
+ return getHeadersSize(obj)
139
+ } catch {
140
+ // pass
141
+ }
142
+ }
128
143
  return 0
129
144
  }
130
145
 
@@ -157,7 +172,8 @@ class DataStreamsProcessor {
157
172
  env,
158
173
  tags,
159
174
  version,
160
- service
175
+ service,
176
+ flushInterval
161
177
  } = {}) {
162
178
  this.writer = new DataStreamsWriter({
163
179
  hostname,
@@ -173,11 +189,13 @@ class DataStreamsProcessor {
173
189
  this.service = service || 'unnamed-nodejs-service'
174
190
  this.version = version || ''
175
191
  this.sequence = 0
192
+ this.flushInterval = flushInterval
176
193
 
177
194
  if (this.enabled) {
178
- this.timer = setInterval(this.onInterval.bind(this), 10000)
195
+ this.timer = setInterval(this.onInterval.bind(this), flushInterval)
179
196
  this.timer.unref()
180
197
  }
198
+ process.once('beforeExit', () => this.onInterval())
181
199
  }
182
200
 
183
201
  onInterval () {
@@ -201,7 +219,8 @@ class DataStreamsProcessor {
201
219
  */
202
220
  bucketFromTimestamp (timestamp) {
203
221
  const bucketTime = Math.round(timestamp - (timestamp % this.bucketSizeNs))
204
- return this.buckets.forTime(bucketTime)
222
+ const bucket = this.buckets.forTime(bucketTime)
223
+ return bucket
205
224
  }
206
225
 
207
226
  recordCheckpoint (checkpoint, span = null) {
@@ -259,8 +278,10 @@ class DataStreamsProcessor {
259
278
  }
260
279
  if (direction === 'direction:out') {
261
280
  // Add the header for this now, as the callee doesn't have access to context when producing
262
- payloadSize += getSizeOrZero(encodePathwayContext(dataStreamsContext))
263
- payloadSize += CONTEXT_PROPAGATION_KEY.length
281
+ // - 1 to account for extra byte for {
282
+ const ddInfoContinued = {}
283
+ ddInfoContinued[CONTEXT_PROPAGATION_KEY] = encodePathwayContext(dataStreamsContext).toJSON()
284
+ payloadSize += getSizeOrZero(JSON.stringify(ddInfoContinued)) - 1
264
285
  }
265
286
  const checkpoint = {
266
287
  currentTimestamp: nowNs,
@@ -322,6 +343,10 @@ class DataStreamsProcessor {
322
343
  Stats: serializedBuckets
323
344
  }
324
345
  }
346
+
347
+ setUrl (url) {
348
+ this.writer.setUrl(url)
349
+ }
325
350
  }
326
351
 
327
352
  module.exports = {
@@ -59,6 +59,15 @@ class DataStreamsWriter {
59
59
  })
60
60
  })
61
61
  }
62
+
63
+ setUrl (url) {
64
+ try {
65
+ url = new URL(url)
66
+ this._url = url
67
+ } catch (e) {
68
+ log.warn(e.stack)
69
+ }
70
+ }
62
71
  }
63
72
 
64
73
  module.exports = {
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const pick = require('lodash.pick')
3
+ const pick = require('../../../../utils/src/pick')
4
4
  const id = require('../../id')
5
5
  const DatadogSpanContext = require('../span_context')
6
6
  const log = require('../../log')
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const uniq = require('lodash.uniq')
3
+ const uniq = require('../../../../utils/src/uniq')
4
4
  const analyticsSampler = require('../../analytics_sampler')
5
5
  const FORMAT_HTTP_HEADERS = 'http_headers'
6
6
  const log = require('../../log')
@@ -135,6 +135,7 @@ class DatadogTracer extends Tracer {
135
135
 
136
136
  setUrl (url) {
137
137
  this._exporter.setUrl(url)
138
+ this._dataStreamsProcessor.setUrl(url)
138
139
  }
139
140
 
140
141
  scope () {
@@ -0,0 +1,16 @@
1
+ 'use strict'
2
+
3
+ module.exports = str => {
4
+ if (typeof str !== 'string') {
5
+ throw new TypeError('Expected a string')
6
+ }
7
+
8
+ return str
9
+ .trim()
10
+ .replace(/([a-z])([A-Z])/g, '$1-$2')
11
+ .replace(/\s+/g, '-')
12
+ .replace(/^-+|-+$/g, '')
13
+ .replace(/_/g, '-')
14
+ .replace(/-{2,}/g, '-')
15
+ .toLowerCase()
16
+ }
@@ -0,0 +1,11 @@
1
+ 'use strict'
2
+
3
+ module.exports = (object, props) => {
4
+ const result = {}
5
+ props.forEach(prop => {
6
+ if (prop in object) {
7
+ result[prop] = object[prop]
8
+ }
9
+ })
10
+ return result
11
+ }
@@ -0,0 +1,5 @@
1
+ 'use strict'
2
+
3
+ module.exports = function (arr) {
4
+ return [...new Set(arr)]
5
+ }