Skip to content

Commit a412e6d

Browse files
committed
feat: component tracking and improved error message when node is move… (#22428)
* feat: component tracking and improved error message when node is moved to another ui (#22282)
1 parent 45711e3 commit a412e6d

File tree

11 files changed

+555
-22
lines changed

11 files changed

+555
-22
lines changed

flow-server/src/main/java/com/vaadin/flow/component/Component.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import java.util.stream.Stream;
1616
import java.util.stream.Stream.Builder;
1717

18+
import com.vaadin.flow.component.internal.ComponentTracker;
1819
import com.vaadin.flow.component.polymertemplate.Id;
1920
import com.vaadin.flow.component.polymertemplate.PolymerTemplate;
2021
import com.vaadin.flow.dom.Element;
@@ -87,6 +88,7 @@ public MapToExistingElement(Element element,
8788
* instead of creating a new element.
8889
*/
8990
protected Component() {
91+
ComponentTracker.trackCreate(this);
9092
Optional<String> tagNameAnnotation = AnnotationReader
9193
.getAnnotationFor(getClass(), Tag.class).map(Tag::value);
9294
if (!tagNameAnnotation.isPresent()) {
@@ -125,6 +127,7 @@ protected Component() {
125127
* the root element for the component
126128
*/
127129
protected Component(Element element) {
130+
ComponentTracker.trackCreate(this);
128131
if (elementToMapTo.get() != null) {
129132
mapToElement(element == null ? null : element.getTag());
130133
templateMapped = this.element != null
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
/**
2+
* Copyright (C) 2025 Vaadin Ltd
3+
*
4+
* This program is available under Vaadin Commercial License and Service Terms.
5+
*
6+
* See {@literal <https://vaadin.com/commercial-license-and-service-terms>} for the full
7+
* license.
8+
*/
9+
package com.vaadin.flow.component.internal;
10+
11+
import java.io.Serializable;
12+
import java.util.Arrays;
13+
import java.util.Collections;
14+
import java.util.List;
15+
import java.util.Map;
16+
import java.util.Objects;
17+
import java.util.Optional;
18+
import java.util.WeakHashMap;
19+
import java.util.stream.Collectors;
20+
import java.util.stream.Stream;
21+
22+
import com.vaadin.flow.component.Component;
23+
import com.vaadin.flow.function.DeploymentConfiguration;
24+
import com.vaadin.flow.router.internal.AbstractNavigationStateRenderer;
25+
import com.vaadin.flow.server.InitParameters;
26+
import com.vaadin.flow.server.VaadinService;
27+
28+
/**
29+
* Tracks the location in source code where components were instantiated.
30+
**/
31+
public class ComponentTracker {
32+
33+
private static final Map<Component, Location> createLocation = Collections
34+
.synchronizedMap(new WeakHashMap<>());
35+
private static final Map<Component, Location> attachLocation = Collections
36+
.synchronizedMap(new WeakHashMap<>());
37+
38+
private static Boolean disabled = null;
39+
private static final String[] prefixesToSkip = new String[] {
40+
"com.vaadin.flow.component.", "com.vaadin.flow.di.",
41+
"com.vaadin.flow.dom.", "com.vaadin.flow.internal.",
42+
"com.vaadin.flow.spring.", "java.", "jdk.",
43+
"org.springframework.beans." };
44+
45+
/**
46+
* Represents a location in the source code.
47+
*/
48+
public static class Location implements Serializable {
49+
private final String className;
50+
private final String filename;
51+
private final String methodName;
52+
private final int lineNumber;
53+
54+
public Location(String className, String filename, String methodName,
55+
int lineNumber) {
56+
this.className = className;
57+
this.filename = filename;
58+
this.methodName = methodName;
59+
this.lineNumber = lineNumber;
60+
}
61+
62+
public String className() {
63+
return className;
64+
}
65+
66+
public String filename() {
67+
return filename;
68+
}
69+
70+
public String methodName() {
71+
return methodName;
72+
}
73+
74+
public int lineNumber() {
75+
return lineNumber;
76+
}
77+
78+
@Override
79+
public boolean equals(Object o) {
80+
if (this == o)
81+
return true;
82+
if (o == null || getClass() != o.getClass())
83+
return false;
84+
85+
Location location = (Location) o;
86+
87+
if (lineNumber != location.lineNumber)
88+
return false;
89+
if (!Objects.equals(className, location.className))
90+
return false;
91+
if (!Objects.equals(filename, location.filename))
92+
return false;
93+
return Objects.equals(methodName, location.methodName);
94+
}
95+
96+
@Override
97+
public int hashCode() {
98+
int result = className != null ? className.hashCode() : 0;
99+
result = 31 * result + (filename != null ? filename.hashCode() : 0);
100+
result = 31 * result
101+
+ (methodName != null ? methodName.hashCode() : 0);
102+
result = 31 * result + lineNumber;
103+
return result;
104+
}
105+
106+
@Override
107+
public String toString() {
108+
return "Component '" + className + "' at '" + filename + "' ("
109+
+ methodName + " LINE " + lineNumber + ")";
110+
}
111+
}
112+
113+
/**
114+
* Finds the location where the given component instance was created.
115+
*
116+
* @param component
117+
* the component to find
118+
* @return the location where the component was created
119+
*/
120+
public static Location findCreate(Component component) {
121+
return createLocation.get(component);
122+
}
123+
124+
/**
125+
* Tracks the location where the component was created. This should be
126+
* called from the Component constructor so that the creation location can
127+
* be found from the current stacktrace.
128+
*
129+
* @param component
130+
* the component to track
131+
*/
132+
public static void trackCreate(Component component) {
133+
if (isDisabled()) {
134+
return;
135+
}
136+
StackTraceElement[] stack = Thread.currentThread().getStackTrace();
137+
Location[] relevantLocations = findRelevantLocations(stack);
138+
Location location = findRelevantLocation(component.getClass(),
139+
relevantLocations, null);
140+
if (location == null || isNavigatorCreate(location)) {
141+
location = findRelevantLocation(null, relevantLocations, null);
142+
}
143+
createLocation.put(component, location);
144+
}
145+
146+
/**
147+
* Finds the location where the given component instance was attached to a
148+
* parent.
149+
*
150+
* @param component
151+
* the component to find
152+
* @return the location where the component was attached
153+
*/
154+
public static Location findAttach(Component component) {
155+
return attachLocation.get(component);
156+
}
157+
158+
/**
159+
* Tracks the location where the component was attached. This should be
160+
* called from the Component attach logic so that the creation location can
161+
* be found from the current stacktrace.
162+
*
163+
* @param component
164+
* the component to track
165+
*/
166+
public static void trackAttach(Component component) {
167+
if (isDisabled()) {
168+
return;
169+
}
170+
StackTraceElement[] stack = Thread.currentThread().getStackTrace();
171+
172+
// In most cases the interesting attach call is found in the same class
173+
// where the component was created and not in a generic layout class
174+
Location[] relevantLocations = findRelevantLocations(stack);
175+
Location location = findRelevantLocation(component.getClass(),
176+
relevantLocations, findCreate(component));
177+
if (location == null || isNavigatorCreate(location)) {
178+
// For routes, we can just show the init location as we have nothing
179+
// better
180+
location = createLocation.get(component);
181+
}
182+
attachLocation.put(component, location);
183+
}
184+
185+
private static Location[] findRelevantLocations(StackTraceElement[] stack) {
186+
return Stream.of(stack).filter(e -> {
187+
for (String prefixToSkip : prefixesToSkip) {
188+
if (e.getClassName().startsWith(prefixToSkip)) {
189+
return false;
190+
}
191+
}
192+
return true;
193+
}).map(ComponentTracker::toLocation).toArray(Location[]::new);
194+
}
195+
196+
private static Location findRelevantLocation(
197+
Class<? extends Component> excludeClass, Location[] locations,
198+
Location preferredClass) {
199+
List<Location> candidates = Arrays.stream(locations)
200+
.filter(location -> excludeClass == null
201+
|| !location.className().equals(excludeClass.getName()))
202+
.filter(location -> {
203+
for (String prefixToSkip : prefixesToSkip) {
204+
if (location.className().startsWith(prefixToSkip)) {
205+
return false;
206+
}
207+
}
208+
return true;
209+
}).collect(Collectors.toList());
210+
if (preferredClass != null) {
211+
Optional<Location> preferredCandidate = candidates.stream()
212+
.filter(location -> location.className()
213+
.equals(preferredClass.className()))
214+
.findFirst();
215+
if (preferredCandidate.isPresent()) {
216+
return preferredCandidate.get();
217+
}
218+
}
219+
return candidates.isEmpty() ? null : candidates.get(0);
220+
}
221+
222+
private static boolean isNavigatorCreate(Location location) {
223+
return location.className()
224+
.equals(AbstractNavigationStateRenderer.class.getName());
225+
}
226+
227+
/**
228+
* Checks if the component tracking is disabled.
229+
* <p>
230+
* Tracking is disabled when application is running in production mode or if
231+
* the configuration property
232+
* {@literal vaadin.devmode.componentTracker.enabled} is set to
233+
* {@literal false}.
234+
* <p>
235+
* When unsure, reports that production mode is true so tracking does not
236+
* take place in production.
237+
*
238+
* @return true if in production mode or the mode is unclear, false if in
239+
* development mode
240+
**/
241+
private static boolean isDisabled() {
242+
if (disabled != null) {
243+
return disabled;
244+
}
245+
246+
VaadinService service = VaadinService.getCurrent();
247+
if (service == null) {
248+
// Rather fall back to not tracking if we are unsure, so we do not
249+
// use memory in production
250+
return true;
251+
}
252+
253+
DeploymentConfiguration configuration = service
254+
.getDeploymentConfiguration();
255+
if (configuration == null) {
256+
return true;
257+
}
258+
259+
disabled = configuration.isProductionMode()
260+
|| !configuration.getBooleanProperty(
261+
InitParameters.APPLICATION_PARAMETER_DEVMODE_ENABLE_COMPONENT_TRACKER,
262+
true);
263+
return disabled;
264+
}
265+
266+
private static Location toLocation(StackTraceElement stackTraceElement) {
267+
if (stackTraceElement == null) {
268+
return null;
269+
}
270+
271+
String className = stackTraceElement.getClassName();
272+
String fileName = stackTraceElement.getFileName();
273+
String methodName = stackTraceElement.getMethodName();
274+
int lineNumber = stackTraceElement.getLineNumber();
275+
return new Location(className, fileName, methodName, lineNumber);
276+
}
277+
278+
}

flow-server/src/main/java/com/vaadin/flow/dom/Element.java

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
package com.vaadin.flow.dom;
1010

1111
import java.io.Serializable;
12-
import java.util.ArrayList;
1312
import java.util.HashMap;
1413
import java.util.Locale;
1514
import java.util.Map;
@@ -36,14 +35,11 @@
3635
import com.vaadin.flow.internal.JavaScriptSemantics;
3736
import com.vaadin.flow.internal.JsonCodec;
3837
import com.vaadin.flow.internal.StateNode;
39-
import com.vaadin.flow.internal.nodefeature.ElementData;
40-
import com.vaadin.flow.internal.nodefeature.TextNodeMap;
4138
import com.vaadin.flow.internal.nodefeature.VirtualChildrenList;
4239
import com.vaadin.flow.server.AbstractStreamResource;
4340
import com.vaadin.flow.server.Command;
4441
import com.vaadin.flow.server.StreamResource;
4542
import com.vaadin.flow.shared.Registration;
46-
4743
import elemental.json.Json;
4844
import elemental.json.JsonValue;
4945

@@ -135,14 +131,9 @@ public Element(String tag, boolean autocreate) {
135131
public static Element get(StateNode node) {
136132
assert node != null;
137133

138-
if (node.hasFeature(TextNodeMap.class)) {
139-
return get(node, BasicTextElementStateProvider.get());
140-
} else if (node.hasFeature(ElementData.class)) {
141-
return get(node, BasicElementStateProvider.get());
142-
} else {
143-
throw new IllegalArgumentException(
144-
"Node is not valid as an element");
145-
}
134+
return ElementUtil.from(node)
135+
.orElseThrow(() -> new IllegalArgumentException(
136+
"Node is not valid as an element"));
146137
}
147138

148139
/**

flow-server/src/main/java/com/vaadin/flow/dom/ElementUtil.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@
2020

2121
import com.vaadin.flow.component.Component;
2222
import com.vaadin.flow.component.Composite;
23+
import com.vaadin.flow.dom.impl.BasicElementStateProvider;
24+
import com.vaadin.flow.dom.impl.BasicTextElementStateProvider;
25+
import com.vaadin.flow.internal.StateNode;
26+
import com.vaadin.flow.internal.nodefeature.ElementData;
27+
import com.vaadin.flow.internal.nodefeature.TextNodeMap;
2328

2429
/**
2530
* Provides utility methods for {@link Element}.
@@ -293,4 +298,26 @@ public static boolean isScript(Element element) {
293298
&& "script".equalsIgnoreCase(element.getTag());
294299
}
295300

301+
/**
302+
* Gets the element mapped to the given state node.
303+
*
304+
* @param node
305+
* the state node, not <code>null</code>
306+
* @return the element for the node, or an empty Optional if the state node
307+
* is not mapped to any particular element.
308+
*/
309+
public static Optional<Element> from(StateNode node) {
310+
assert node != null;
311+
312+
if (node.hasFeature(TextNodeMap.class)) {
313+
return Optional
314+
.of(Element.get(node, BasicTextElementStateProvider.get()));
315+
} else if (node.hasFeature(ElementData.class)) {
316+
return Optional
317+
.of(Element.get(node, BasicElementStateProvider.get()));
318+
} else {
319+
return Optional.empty();
320+
}
321+
}
322+
296323
}

0 commit comments

Comments
 (0)