[IGN-7976]Path to Component Event

I am trying to make a hold your hand script for everything script errors:

I managed to get figure out how to get module/function_name/line number and was able to create a traceback path which is nice. For example:

def func(t, *args, **kwargs):
	print t
	0/0
	return

def func1(t, *args, **kwargs):
	func(t)

def func2(t, *args, **kwargs):
	func1(t)

def func3(t, *args, **kwargs):
	func2(t)

try:
	func3("test", 1, 2, 3, trial = "testing kwargs")
except:
	print trace.debug()

Yields the following output:

ZeroDivisionError @ <admin@script_console>:<module>:17 -> <admin@script_console>:func3:14 -> <admin@script_console>:func2:11 -> <admin@script_console>:func1:8 -> <admin@script_console>:func:4 ->  integer division or modulo by zero

We have used this on site track down errors in complicated business logic.

However, now I am trying to take it to the next level. One pet peeve of mine was value change scripts do not identify which tag or component they are coming from. They simply say something like function:valueChanged.

My questions from this are:

  • Which function?
  • Which component?
  • Which event?

This works with script modules, but not with components. Ideally I would want to have something like <view_path>/<component_name>/<event_name>/

I was able to solve this for tag Events, but I can’t figure it out for component events.

Here is my current code:

class debug:
	"""Creates debug object with metadata on the specific error."""
	
	error_path = None
	error_name = None
	error_message = None
	error_string = None
	
	def __init__(self):
		self.get_error_details()
		gwlog(self.error_string)
	
	def __str__(self):
		return self.error_string
		
	@classmethod
	def get_error_path(self, traceback = None, path = ""):
		if traceback is None:
			return path
		else:
			frame = traceback.tb_frame
			frame_locals = frame.f_locals
			file_name = frame.f_code.co_filename
			function_name = frame.f_code.co_name
			line_number = traceback.tb_lineno
			
			gwlog(frame_locals)
			
			if file_name == "<tagevent:valueChanged>":
				tag_path = frame_locals.get("tagPath")
				file_name = "<tagevent:valueChanged>" if tag_path is None else tag_path
				
			elif file_name == "<input>":
				file_name = "<%s@script_console>" % system.vision.getUsername()
			
			elif file_name == "<function:runAction>":
				component = frame_locals.get("self")
				if component is not None:
					component_meta = getattr(component, "meta")
					component_name = getattr(component_meta, "name")
					file_name = str(component_name)
					
					## NEED HELP IDENTIFYING THIS COMPONENT. IDK WHERE function.runAction is
					## Looking to get <view_path>/<component_name>/<event_name>/<I already have the rest from here>
			
			path +=  "%s:%s:%s -> " % (file_name, function_name, line_number)
			
			final_path = self.get_error_path(traceback.tb_next, path)
			if final_path is None:
				return
			else:
				return final_path
	
	def get_error_details(self):
		
		TYPE, MSG_OBJ, TRACEBACK = sys.exc_info()
		
		self.error_name = TYPE.__name__ if TYPE else "Unknown Error"
		self.error_message = MSG_OBJ.message if MSG_OBJ.message else MSG_OBJ
		self.error_path = self.get_error_path(TRACEBACK)
		
		self.error_string = "%s @ %s %s" % (self.error_name, self.error_path, self.error_message)

Does anyone know how to get reference to a view path or a component path from the calling component event itself? Or perhaps there is another way to absolutely define the script location of a perspective component?

Constraints: Passing a path into my class constructor is not acceptable as it is impossible to reliably force a user to “remember” to add this in.

Also, I am aware that business logic should not be written in components themselves, we avoid this as much as possible. However, things like a simple change script may not be classified as “business logic”. However, on screens with many components, especially on SCADAs with tonnes of embedded views and components with the same names, it is useful to know which one made the mistake. The goal is this: If I have 1000 buttons on the screen, I shouldn’t have to open each one to find out which one caused a traceback on function.onValueChanged.

I think you are looking for component.view.id to get the view name and you can remove

component_meta = getattr(component, "meta")
component_name = getattr(component_meta, "name")

and can just component.meta.name because you already have the reference to the component.

This will be the great script for base debug script for every project for sure. Thanks for sharing up the code bud.

2 Likes

Agreed but that does not mean they shouldn’t be in somewhere in your script library. I often have a gui scripting library under the appropriate package for things like change scripts that run on the front end. Worth it for a number of reasons not related to your post, but what is related s in your stack trace you should see what function was throwing the issue, so you could potentially fix the issue without even finding or caring about what specific component threw it.

Making the valueChange script like

def valueChanged(self, previousValue, currentValue, origin, missedEvents):
	test.valueChangedFunction(self, previousValue, currentValue, origin, missedEvents)

where my script is

def valueChangedFunction(self, previousValue, currentValue, origin, missedEvents):
	x = 1 / 0

Now in my logs I see

Error running property change script on TextField.props.text: Traceback (most recent call last): File "<function:valueChanged>", line 2, in valueChanged File "<module:test>", line 2, in valueChangedFunction ZeroDivisionError: integer division or modulo by zero

I don’t know what component threw it but I see exactly where I would fix it - test.valueChangedFunction and I can fix it without doing any run down on the GUI.

I understand this can have issues if one of your many 1000 components has a slight difference to the others and you need to inspect the gui anyways but I do think doing things in this style will help you avoid that in a many scenarios – in the event it is strictly a jython/java errror and not caused by a GUI input being a type mismatch or something of that nature.

I don’t have an answer to your initial query but I do think doing things in the manner I described above removes a lot of the pain points that made you go down this path in the first place.

This is basically the same request as this other post. There's an existing ticket (that I was unfortunately able to get done in 8.3.0) to add this kind of meta information, which is very easy for us to do, and very awkward for you to obtain) to scripts invoked as extension functions, via the __name__ or __file__ builtin keys.

1 Like

@Subin_Adhikari This is exactly what I needed!

Turns out the trick is to use component.view.id to get the path to the view. I didn’t realize this because my view was at the root of views :stuck_out_tongue: I guess I was expecting Views/<my_view>. So naturally I added it to my class.

Here is the finished product:

class debug:
	"""Creates debug object with metadata on the specific error."""
	
	error_path = None
	error_name = None
	error_message = None
	error_string = None
	
	def __init__(self):
		self.get_error_details()
		gwlog(self.error_string)
	
	def __str__(self):
		return self.error_string
		
	@classmethod
	def get_error_path(self, traceback = None, path = ""):
		if traceback is None:
			return path
		else:
			frame = traceback.tb_frame
			frame_locals = frame.f_locals
			file_name = frame.f_code.co_filename
			function_name = frame.f_code.co_name
			line_number = traceback.tb_lineno
			
			gwlog(frame_locals)
			
			if file_name == "<tagevent:valueChanged>":
				tag_path = frame_locals.get("tagPath")
				file_name = "<tagevent:valueChanged>" if tag_path is None else tag_path
				
			elif file_name == "<input>":
				file_name = "<%s@script_console>" % system.vision.getUsername()
			
			elif file_name == "<function:runAction>" or "<custom-method" in file_name:
				component = frame_locals.get("self")
				if component is not None:
					if component.meta is not None:
						component_name = "/" + component.meta.name
					else:
						component_name = "<view>"
					view_name = component.view.id
					file_name = "Views/%s/%s" % (view_name, component_name)
			
			path +=  "%s:%s:%s -> " % (file_name, function_name, line_number)
			
			final_path = self.get_error_path(traceback.tb_next, path)
			if final_path is None:
				return
			else:
				return final_path

Now I can catch errors with a path to the error even on perspective components/views, tag event scripts and component methods. Next stop will be message handlers.

@bkarabinchak.psi I hear what you are saying. That being said, I am more building this to be a universal function that I can go into a service call with on a project we didn’t make and debug anything I want. It’s hard to force the world to change, so this is my attempt to work with it. Also, this allows be to set up logging to the db very easily in a universal way because everything is controlled in one place. A lot easier to remember an just call this class in a try: except: trace.debug() then to worry about calling a specialized script in X location.

@paul-griffith While we are on the topic of adding features, may I suggest adding the component name to the traceback? A few pain points I have tried to fix over the years are:

  • Tag Changed Event Scripts: Technically passed, but default traceback doesn’t show it without inspect. Requires my debug class.
  • Component Scripts: runAction() is shown, but event name like onStartup script or onActionPerformed should be available in the traceback msg or passed in event.name or something.
  • Message Handlers: Receiver component name with view path should be available in traceback.

These have been the greatest pain points throughout the years. Getting more identity info in the traceback certainly will help while debugging.

4 Likes

Consider also exploring what you have access to through the MDC thread locals, which we use to decorate our logs:

1 Like