Skip to content

Commit d8aec30

Browse files
piehLekoArts
andauthored
feat(gatsby): detect node mutations (enabled by flag or env var) (#34006)
* feat(gatsby): detect node mutations (enabled by flag or env var) * tmp docs * rename env var to drop experimental * fix stack trace label not dropping 'Error:' * tmp of debugging docs * add headings to doc and complete example migration for onCreateNode case * fix env var names, add flag description * update v3->v4 migration guide to mention new doc * update docs * Update migrating-from-v3-to-v4.md * Update flags.ts Co-authored-by: Lennart <[email protected]>
1 parent 3fe5316 commit d8aec30

File tree

9 files changed

+340
-16
lines changed

9 files changed

+340
-16
lines changed
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
---
2+
title: Debugging Missing or Stale Data Fields on Nodes
3+
---
4+
5+
## Overview of `LMDB` datastore behavior changes
6+
7+
In Gatsby 3 we introduced a new data store called `LMDB` (enabled with `LMDB_STORE` and/or `PARALLEL_QUERY_RUNNING` flags). In Gatsby 4 it became the default data store. It allows Gatsby to execute data layer related processing outside of the main build process and enables Gatsby to run queries in multiple processes as well as support additional rendering strategies ([DSG](/docs/reference/rendering-options/deferred-static-generation/) and [SSR](/docs/reference/rendering-options/server-side-rendering/)).
8+
9+
In a lot of cases that change is completely invisible to users, but there are cases where things behave differently.
10+
11+
Direct node mutations in various API lifecycles are not persisted anymore. In previous Gatsby versions it did work because source of truth for the data layer was directly in Node.js memory, so mutating node was in fact mutating source of truth. Now Gatsby edits data it receives from the database, so unless it explicitly upserts data to this database after edits, those edits will not be persisted (even for same the same build).
12+
13+
Common errors when doing swap to `LMDB` will be that some fields don't exist anymore or are `null`/`undefined` when trying to execute GraphQL queries.
14+
15+
## Diagnostic mode
16+
17+
Gatsby (starting with version 4.4) can detect those node mutations. Unfortunately it adds measurable overhead so it isn't enabled by default. You can opt into it when you see data related issues (particularly when you didn't have this issue before using `LMDB`). To enable diagnostic mode:
18+
19+
- Use truthy environment variable `GATSBY_DETECT_NODE_MUTATIONS`:
20+
```
21+
GATSBY_DETECT_NODE_MUTATIONS=1 gatsby build
22+
```
23+
- Or use `DETECT_NODE_MUTATIONS` config flag:
24+
```javascript:title=gatsby-config.js
25+
module.exports = {
26+
flags: {
27+
DETECT_NODE_MUTATIONS: true,
28+
},
29+
}
30+
```
31+
32+
Example diagnostic message you might see:
33+
34+
```
35+
warn Node mutation detected
36+
37+
File <rootDir>/plugins/transform/gatsby-node.js:4:20
38+
2 | if (node.internal.type === `Test`) {
39+
3 | // console.log(`[Mutating node in onCreateNode]`)
40+
> 4 | node.nested.a2 = `edited`
41+
| ^
42+
5 | }
43+
6 | }
44+
7 |
45+
46+
Stack trace:
47+
at Object.exports.onCreateNode (<rootDir>/plugins/transform/gatsby-node.js:4:20)
48+
at runAPI (<rootDir>/node_modules/gatsby/src/utils/api-runner-node.js:462:22)
49+
at Promise.catch.decorateEvent.pluginName
50+
(<rootDir>/node_modules/gatsby/src/utils/api-runner-node.js:613:13)
51+
at Promise._execute
52+
... Rest of Stacktrace
53+
```
54+
55+
It will point you to the code that is trying to mutate nodes. Note that it might also point to installed plugins in which case you should notify the plugin maintainer about it.
56+
57+
**Please Note:** Be sure to stop using this mode once you find and handle all problematic code paths as it will decrease performance.
58+
59+
## Migration
60+
61+
### Mutating a node in `onCreateNode`
62+
63+
Instead of mutating nodes directly, `createNodeField` action should be used instead. This way Gatsby will update the source of truth (to actually update the node in the datastore). `createNodeField` will create that additional field under `fields.fieldName`. If you want to preserve schema shape, so that additional field is on the root of a node, you can use schema customization.
64+
65+
```diff
66+
const { createRemoteFileNode } = require(`gatsby-source-filesystem`)
67+
68+
exports.onCreateNode = async ({
69+
node, // the node that was just created
70+
- actions: { createNode },
71+
+ actions: { createNode, createNodeField },
72+
createNodeId,
73+
getCache,
74+
}) => {
75+
if (node.internal.type === `SomeNodeType`) {
76+
const fileNode = await createRemoteFileNode({
77+
// the url of the remote image to generate a node for
78+
url: node.imgUrl,
79+
parentNodeId: node.id,
80+
createNode,
81+
createNodeId,
82+
getCache,
83+
})
84+
85+
if (fileNode) {
86+
- node.localFile___NODE = fileNode.id
87+
+ createNodeField({ node, name: 'localFile', value: fileNode.id })
88+
}
89+
}
90+
}
91+
+
92+
+exports.createSchemaCustomization = ({ actions }) => {
93+
+ const { createTypes } = actions
94+
+
95+
+ createTypes(`
96+
+ type SomeNodeType implements Node {
97+
+ localFile: File @link(from: "fields.localFile")
98+
+ }
99+
+ `)
100+
+}
101+
```

docs/docs/reference/release-notes/migrating-from-v3-to-v4.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -583,8 +583,7 @@ This was never an intended feature of Gatsby and is considered an anti-pattern (
583583
Starting with v4 Gatsby introduces a persisted storage for nodes and thus this pattern will no longer work
584584
because nodes are persisted after `createNode` call and all direct mutations after that will be lost.
585585

586-
Unfortunately it is hard to detect it automatically (without sacrificing performance), so we recommend you to
587-
check your code to ensure you don't mutate nodes directly.
586+
Gatsby provides diagnostic mode to detect those direct mutations, unfortunately it has noticeable performance overhead so we don't enable it by default. See [Debugging missing data](/docs/how-to/local-development/debugging-missing-data/) for more details on it.
588587

589588
Gatsby provides several actions available in `sourceNodes` and `onCreateNode` APIs to use instead:
590589

packages/gatsby/src/redux/actions/public.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const normalizePath = require(`../../utils/normalize-path`).default
2828
import { createJobV2FromInternalJob } from "./internal"
2929
import { maybeSendJobToMainProcess } from "../../utils/jobs/worker-messaging"
3030
import { reportOnce } from "../../utils/report-once"
31+
import { wrapNode } from "../../utils/detect-node-mutations"
3132

3233
const isNotTestEnv = process.env.NODE_ENV !== `test`
3334
const isTestEnv = process.env.NODE_ENV === `test`
@@ -868,7 +869,7 @@ actions.createNode =
868869

869870
const { payload: node, traceId, parentSpan } = createNodeAction
870871
return apiRunnerNode(`onCreateNode`, {
871-
node,
872+
node: wrapNode(node),
872873
traceId,
873874
parentSpan,
874875
traceTags: { nodeId: node.id, nodeType: node.internal.type },

packages/gatsby/src/schema/node-model.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
} from "../datastore"
2323
import { GatsbyIterable, isIterable } from "../datastore/common/iterable"
2424
import { reportOnce } from "../utils/report-once"
25+
import { wrapNode, wrapNodes } from "../utils/detect-node-mutations"
2526

2627
type TypeOrTypeName = string | GraphQLOutputType
2728

@@ -149,7 +150,7 @@ class LocalNodeModel {
149150
this.trackInlineObjectsInRootNode(node)
150151
}
151152

152-
return this.trackPageDependencies(result, pageDependencies)
153+
return wrapNode(this.trackPageDependencies(result, pageDependencies))
153154
}
154155

155156
/**
@@ -180,7 +181,7 @@ class LocalNodeModel {
180181
result.forEach(node => this.trackInlineObjectsInRootNode(node))
181182
}
182183

183-
return this.trackPageDependencies(result, pageDependencies)
184+
return wrapNodes(this.trackPageDependencies(result, pageDependencies))
184185
}
185186

186187
/**
@@ -221,7 +222,7 @@ class LocalNodeModel {
221222
typeof type === `string` ? type : type.name
222223
}
223224

224-
return this.trackPageDependencies(result, pageDependencies)
225+
return wrapNodes(this.trackPageDependencies(result, pageDependencies))
225226
}
226227

227228
/**
@@ -346,7 +347,10 @@ class LocalNodeModel {
346347
pageDependencies.connectionType = gqlType.name
347348
}
348349
this.trackPageDependencies(result.entries, pageDependencies)
349-
return result
350+
return {
351+
entries: wrapNodes(result.entries),
352+
totalCount: result.totalCount,
353+
}
350354
}
351355

352356
/**
@@ -383,7 +387,7 @@ class LocalNodeModel {
383387
// the query whenever any node of this type changes.
384388
pageDependencies.connectionType = gqlType.name
385389
}
386-
return this.trackPageDependencies(first, pageDependencies)
390+
return wrapNode(this.trackPageDependencies(first, pageDependencies))
387391
}
388392

389393
prepareNodes(type, queryFields, fieldsToResolve) {

packages/gatsby/src/services/initialize.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { IBuildContext } from "./types"
2121
import { detectLmdbStore } from "../datastore"
2222
import { loadConfigAndPlugins } from "../bootstrap/load-config-and-plugins"
2323
import type { InternalJob } from "../utils/jobs/types"
24+
import { enableNodeMutationsDetection } from "../utils/detect-node-mutations"
2425

2526
interface IPluginResolution {
2627
resolve: string
@@ -185,6 +186,10 @@ export async function initialize({
185186
}
186187
const lmdbStoreIsUsed = detectLmdbStore()
187188

189+
if (process.env.GATSBY_DETECT_NODE_MUTATIONS) {
190+
enableNodeMutationsDetection()
191+
}
192+
188193
if (config && config.polyfill) {
189194
reporter.warn(
190195
`Support for custom Promise polyfills has been removed in Gatsby v2. We only support Babel 7's new automatic polyfilling behavior.`

packages/gatsby/src/utils/api-runner-node.js

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const { requireGatsbyPlugin } = require(`./require-gatsby-plugin`)
3030
const { getNonGatsbyCodeFrameFormatted } = require(`./stack-trace-utils`)
3131
const { trackBuildError, decorateEvent } = require(`gatsby-telemetry`)
3232
import errorParser from "./api-runner-error-parser"
33+
import { wrapNode, wrapNodes } from "./detect-node-mutations"
3334

3435
if (!process.env.BLUEBIRD_DEBUG && !process.env.BLUEBIRD_LONG_STACK_TRACES) {
3536
// Unless specified - disable longStackTraces
@@ -40,6 +41,21 @@ if (!process.env.BLUEBIRD_DEBUG && !process.env.BLUEBIRD_LONG_STACK_TRACES) {
4041
Promise.config({ longStackTraces: false })
4142
}
4243

44+
const nodeMutationsWrappers = {
45+
getNode(id) {
46+
return wrapNode(getNode(id))
47+
},
48+
getNodes() {
49+
return wrapNodes(getNodes())
50+
},
51+
getNodesByType(type) {
52+
return wrapNodes(getNodesByType(type))
53+
},
54+
getNodeAndSavePathDependency(id) {
55+
return wrapNode(getNodeAndSavePathDependency(id))
56+
},
57+
}
58+
4359
// Bind action creators per plugin so we can auto-add
4460
// metadata to actions they create.
4561
const boundPluginActionCreators = {}
@@ -372,6 +388,14 @@ const runAPI = async (plugin, api, args, activity) => {
372388
runningActivities.forEach(activity => activity.end())
373389
}
374390

391+
const shouldDetectNodeMutations = [
392+
`sourceNodes`,
393+
`onCreateNode`,
394+
`createResolvers`,
395+
`createSchemaCustomization`,
396+
`setFieldsOnGraphQLNodeType`,
397+
].includes(api)
398+
375399
const apiCallArgs = [
376400
{
377401
...args,
@@ -383,11 +407,19 @@ const runAPI = async (plugin, api, args, activity) => {
383407
store,
384408
emitter,
385409
getCache,
386-
getNodes,
387-
getNode,
388-
getNodesByType,
410+
getNodes: shouldDetectNodeMutations
411+
? nodeMutationsWrappers.getNodes
412+
: getNodes,
413+
getNode: shouldDetectNodeMutations
414+
? nodeMutationsWrappers.getNode
415+
: getNode,
416+
getNodesByType: shouldDetectNodeMutations
417+
? nodeMutationsWrappers.getNodesByType
418+
: getNodesByType,
389419
reporter: extendedLocalReporter,
390-
getNodeAndSavePathDependency,
420+
getNodeAndSavePathDependency: shouldDetectNodeMutations
421+
? nodeMutationsWrappers.getNodeAndSavePathDependency
422+
: getNodeAndSavePathDependency,
391423
cache,
392424
createNodeId: namespacedCreateNodeId,
393425
createContentDigest,

0 commit comments

Comments
 (0)