diff --git a/power-apps/udf-trace/README.md b/power-apps/udf-trace/README.md new file mode 100644 index 0000000..3fa2120 --- /dev/null +++ b/power-apps/udf-trace/README.md @@ -0,0 +1,65 @@ +# Send telemetry logs with User Defined Functions to Azure Application Insights + +This snippet demonstrates how to implement telemetry logging in Power Apps using User Defined Functions (UDFs) and Azure Application Insights. It showcases the definition and application of UDFs to efficiently manage and send telemetry data. + +![preview](./assets/udf-trace.png) + +## Problem statement + +Telemetry collection is crucial for developing and enhancing apps. In Power Apps, developers use the `Trace()` function to send telemetry logs to Azure Application Insights. + +However, using this function independently often results in inconsistencies in trace parameters, message formats, and severity levels. These variations can lead to errors and lack a standardized approach, affecting the reliability and usefulness of the data collected. Additionally, without properly preparing the data for logging, it could also cause redundant nests, making the logs harder to parse and analyze effectively. + +## Solution + +By implementing a standardized `User Defined Function (UDF)` for telemetry, it's possible to ensure consistent data collection, minimize errors, and improve the analysis and utility of application insights. + +## Authors + +Author|Socials +--------|--------- +Katerina Chernevskaya | [GitHub](https://github.com/Katerina-Chernevskaya/) - [LinkedIn](https://www.linkedin.com/in/katerinachernevskaya/) + +## Prerequisites + +1. Existing Azure Application Insights resource. Learn more how to create Azure Application Insights resource [here](https://learn.microsoft.com/en-us/azure/azure-monitor/app/create-workspace-resource?tabs=portal). + +2. Connect your Power Apps Canvas app to Application Insights. You can follow the [official guide](https://learn.microsoft.com/en-us/power-apps/maker/canvas-apps/application-insights#connect-your-app-to-application-insights) or use the steps below: + - Go to your Application Insights resource in Azure. On the **Overview** page, locate and copy the **Connection String**: + ![Connection String - Copy](./assets/key.png) + - In Power Apps Studio, select the **App** level, then navigate to the **Connection string** property on the right-hand pane and paste the copied value: + ![Connection String - Paste](./assets/key-paste.png) + +3. Make sure that experimental features are enabled: + - User-defined functions + - User-defined types + ![experimental-features](./assets/experimental-features.png) + +4. Power Apps Canvas app with the following controls (name the controls the same as in the list below). You can use the [sample screen snippet](./source/screen.pa.yaml) to quickly add a preconfigured screen with all necessary controls. + - DatePicker + - Slider + - Gallery + - RadioGroup + - Dropdown + - Toggle + - Rating + - Combobox + - Checkbox + - Button + +## Minimal path to awesome + +1. Open your Power App in edit mode +2. Copy the contents of the **[udf.fx](./source/udf.fx)** +3. Add the copied code into `Formula` property on the `App` level +4. *(Optional: if you didn't create the screen with the [sample screen snippet](./source/screen.pa.yaml))* Copy the contents of the **[onselect.fx](./source/onselect.fx)** and insert the copied code into the `OnSelect` property of the Button +5. Make sure the screen with controls is easily accessible - either move it to a higher position in the screen list or add a navigation control to reach it. Save the app and publish it +6. Play the app, test the button to send telemetry to Azure Application Insights +7. Navigate to the Azure Application Insights resource. Open the `Logs` under `Monitoring`. Select `trace` table. You will see the collected telemetry under the `customDimensions` +![Azure Application Insights](./assets/aap.png) + +## Disclaimer + +**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.** + + \ No newline at end of file diff --git a/power-apps/udf-trace/assets/aap.png b/power-apps/udf-trace/assets/aap.png new file mode 100644 index 0000000..657dbab Binary files /dev/null and b/power-apps/udf-trace/assets/aap.png differ diff --git a/power-apps/udf-trace/assets/experimental-features.png b/power-apps/udf-trace/assets/experimental-features.png new file mode 100644 index 0000000..a4d857b Binary files /dev/null and b/power-apps/udf-trace/assets/experimental-features.png differ diff --git a/power-apps/udf-trace/assets/key-paste.png b/power-apps/udf-trace/assets/key-paste.png new file mode 100644 index 0000000..5c3dd04 Binary files /dev/null and b/power-apps/udf-trace/assets/key-paste.png differ diff --git a/power-apps/udf-trace/assets/key.png b/power-apps/udf-trace/assets/key.png new file mode 100644 index 0000000..413df90 Binary files /dev/null and b/power-apps/udf-trace/assets/key.png differ diff --git a/power-apps/udf-trace/assets/sample.json b/power-apps/udf-trace/assets/sample.json new file mode 100644 index 0000000..b5b4942 --- /dev/null +++ b/power-apps/udf-trace/assets/sample.json @@ -0,0 +1,50 @@ +[ + { + "$schema": "https://developer.microsoft.com/en-us/json-schemas/pnp/samples/v1.0/metadata-schema.json", + "name": "pnp-powerplatform-snippets-udf-trace", + "version": "1.0.0.0", + "source": "pnp", + "creationDateTime": "2025-04-02T00:00:00.000Z", + "updateDateTime": "2025-04-02T00:00:00.000Z", + "title": "Send telemetry logs with User Defined Functions to Azure Application Insights", + "shortDescription": "This snippet demonstrates how to implement telemetry logging in Power Apps using User Defined Functions (UDFs) and Azure Application Insights. It showcases the definition and application of UDFs to efficiently manage and send telemetry data.", + "longDescription": [ + "This sample demonstrates a structured approach to telemetry logging in Power Apps using the Trace() function with Azure Application Insights. It addresses common issues that arise when Trace() is used inconsistently - such as mismatched parameters, varying message formats, and unstandardized severity levels. These inconsistencies can lead to unreliable telemetry data and complicate analysis. The sample also highlights the importance of preparing data properly before logging to avoid redundant nesting, which can make logs more difficult to parse and interpret." + ], + "url": "https://github.com/pnp/powerplatform-snippets/tree/main/power-apps/udf-trace/", + "products": [ + "Power Platform", + "Power Apps", + "powerplatform-snippets", + "power-apps-snippets", + "formula-snippets" + ], + "tags": [ ], + "categories": [ ], + "metadata": [ + { + "key": "Product", + "value": "Power Apps" + }, + { + "key": "Type", + "value": "Formula" + } + ], + "thumbnails": [ + { + "type": "image", + "order": 100, + "url": "https://raw.githubusercontent.com/Katerina-Chernevskaya/powerplatform-snippets/dc573cf7bc535f6072dcaa5abf2d91ebb270f53d/power-apps/udf-trace/assets/udf-trace.png", + "alt": "Preview PNG" + } + ], + "authors": [ + { + "gitHubAccount": "Katerina-Chernevskaya", + "name": "Katerina Chernevskaya", + "pictureUrl": "https://github.com/Katerina-Chernevskaya.png" + } + ] + } +] diff --git a/power-apps/udf-trace/assets/udf-trace.png b/power-apps/udf-trace/assets/udf-trace.png new file mode 100644 index 0000000..c58b24e Binary files /dev/null and b/power-apps/udf-trace/assets/udf-trace.png differ diff --git a/power-apps/udf-trace/source/onselect.fx b/power-apps/udf-trace/source/onselect.fx new file mode 100644 index 0000000..8fe9aab --- /dev/null +++ b/power-apps/udf-trace/source/onselect.fx @@ -0,0 +1,17 @@ +udfTrace( + DatePicker.SelectedDate, + Slider.Value, + ShowColumns( + Filter( + Gallery.AllItems, + status = "Yes" // Replace status with your gallery property name + ), + title // Replace title with your gallery property name + ), + RadioGroup.SelectedItems, + Dropdown.SelectedItems, + Toggle.Checked, + Rating.Value, + Combobox.SelectedItems, + Checkbox.Checked +) \ No newline at end of file diff --git a/power-apps/udf-trace/source/screen.pa.yaml b/power-apps/udf-trace/source/screen.pa.yaml new file mode 100644 index 0000000..fea7d29 --- /dev/null +++ b/power-apps/udf-trace/source/screen.pa.yaml @@ -0,0 +1,352 @@ +Screens: + Screen1: + Properties: + Fill: = + OnVisible: |- + =ClearCollect( + colParameters, + { + id: 1, + title: "Ergonomic chair", + svg: " ", + status: "Yes" + }, + { + id: 2, + title: "Locker", + svg: " ", + status: "No" + }, + { + id: 3, + title: "Monitor", + svg: "", + status: "Yes" + }, + { + id: 4, + title: "Next to the air conditioner", + svg: " ", + status: "Yes" + }, + { + id: 5, + title: "Next to the heater", + svg: " ", + status: "No" + }, + { + id: 6, + title: "Next to the window", + svg: "", + status: "No" + }, + { + id: 7, + title: "Socket", + svg: "", + status: "No" + }, + { + id: 8, + title: "Soundproofing", + svg: "", + status: "No" + }, + { + id: 9, + title: "Suitable for people with disabilities", + svg: "", + status: "No" + }, + { + id: 10, + title: "USB Socket", + svg: "", + status: "No" + } + ) + Children: + - Gallery: + Control: Gallery@2.15.0 + Variant: Vertical + Properties: + Height: =429 + Items: =colParameters + ShowNavigation: =true + TemplateSize: =137 + Transition: =Transition.Push + Width: =320 + WrapCount: =2 + X: =35 + Y: =249 + Children: + - Select parameters - Mockup - Parameter button background: + Control: HtmlViewer@2.1.0 + Properties: + DisabledBorderColor: =RGBA(56, 56, 56, 1) + Font: =Font.'Open Sans' + Height: =137 + HtmlText: "=\"
\"\n\n" + OnSelect: = + PaddingBottom: =0 + PaddingLeft: =0 + PaddingRight: =0 + PaddingTop: =0 + Width: =128 + X: =5 + - Select parameters - Mockup - Parameter description: + Control: Label@2.5.1 + Properties: + Align: =Align.Center + Color: |- + =If( + ThisItem.status = "Yes", + Color.White, + ColorValue("#0072ce") + ) + Font: =Font.Lato + Height: =50 + OnSelect: =Select(Parent) + PaddingBottom: =0 + PaddingTop: =0 + Size: =10 + Text: =ThisItem.title + Width: ='Select parameters - Mockup - Button'.Width + X: ='Select parameters - Mockup - Button'.X + Y: ='Select parameters - Mockup - Parameter image'.Y + 'Select parameters - Mockup - Parameter image'.Height + 5 + - Select parameters - Mockup - Parameter image: + Control: Image@2.2.3 + Properties: + Height: =34 + Image: "=\"data:image/svg+xml;utf8, \"&EncodeUrl(\"\r\n \r\n \r\n\r\n \" & \r\n ThisItem.svg\r\n & \"\r\n\r\n \r\n\r\n\")\r\n" + ImagePosition: =ImagePosition.Center + OnSelect: =Select(Parent) + RadiusBottomLeft: =45 + RadiusBottomRight: =45 + RadiusTopLeft: =45 + RadiusTopRight: =45 + Width: ='Select parameters - Mockup - Button'.Width + X: ='Select parameters - Mockup - Button'.X + Y: =25 + - Select parameters - Mockup - Button: + Control: Classic/Button@2.2.0 + Properties: + BorderColor: =RGBA(0, 0, 0, 0) + BorderThickness: =0 + Color: =Color.Transparent + DisabledBorderColor: =Color.Transparent + DisabledColor: =Color.Transparent + DisabledFill: =Color.Transparent + Fill: =Color.Transparent + FocusedBorderColor: =Color.Transparent + FocusedBorderThickness: =0 + Height: =110 + HoverBorderColor: =Color.Transparent + HoverColor: =Color.Transparent + HoverFill: =Color.Transparent + OnSelect: "=Patch(\r\n colParameters,\r\n LookUp(\r\n colParameters, title = ThisItem.title\r\n ),\r\n {\r\n status: \r\n If(\r\n ThisItem.status = \"Yes\",\r\n \"No\",\r\n \"Yes\"\r\n )\r\n }\r\n); " + PressedBorderColor: =Color.Transparent + PressedColor: =Color.Transparent + PressedFill: =Color.Transparent + Text: ="" + Width: =105 + X: =16 + Y: =9 + - DatePicker: + Control: DatePicker@0.0.42 + Properties: + Format: ='DatePickerCanvas.Format'.Short + Placeholder: ="When sould you like to book a workspace?" + X: =35 + Y: =90 + - Checkbox: + Control: CheckBox@0.0.27 + Properties: + Label: ="I have read and agree to the workspace usage policy" + Width: =400 + X: =395 + Y: =558 + - RadioGroup: + Control: Radio@0.0.24 + Properties: + Items: |- + =[ + "Standard workspace", + "Private booth", + "Quiet zone" + ] + X: =403 + Y: =122 + - Toggle: + Control: Toggle@1.1.5 + Properties: + Checked: =true + Label: |- + =If( + Self.Checked, + nfToggleOn, + nfToggleOff + ) + Width: =400 + X: =403 + Y: =326 + - Slider: + Control: Slider@1.0.32 + Properties: + BasePaletteColor: =PowerAppsTheme.Colors.Primary + Height: =31 + Max: =8 + Min: =1 + Value: =1 + Width: =156 + X: =199 + Y: =154 + - Rating: + Control: Rating@2.1.0 + Properties: + RatingFill: =App.Theme.Colors.Primary + X: =403 + Y: =413 + - RadioGroup - header: + Control: Text@0.0.50 + Properties: + Font: =Font.'Lato Black' + Text: ="Select workspace type" + VerticalAlign: =VerticalAlign.Middle + Width: =223 + X: =403 + Y: =90 + - Dropdown - header: + Control: Text@0.0.50 + Properties: + Font: =Font.'Lato Black' + Text: ="Select department or zone" + VerticalAlign: =VerticalAlign.Middle + Width: =208 + X: =403 + Y: =242 + - Dropdown: + Control: DropDown@0.0.44 + Properties: + Items: |- + =[ + "IT Department", + "Finance Department", + "Shared Space", + "Lounge Zone", + "Open Space" + ] + Width: =208 + X: =595 + Y: =242 + - Slider - header: + Control: Text@0.0.50 + Properties: + Font: =Font.'Lato Black' + Text: |- + ="Booking duration: " & Slider.Value & " h" + VerticalAlign: =VerticalAlign.Middle + Width: =164 + X: =35 + Y: =154 + - Toggle - header: + Control: Text@0.0.50 + Properties: + Font: =Font.'Lato Black' + Text: ="Parking space reservation" + VerticalAlign: =VerticalAlign.Middle + Width: =275 + X: =403 + Y: =294 + - Rating - header: + Control: Text@0.0.50 + Properties: + Font: =Font.'Lato Black' + Text: ="Workspace comfort rating (post-use feedback)" + VerticalAlign: =VerticalAlign.Middle + Width: =320 + X: =403 + Y: =381 + - Gallery - header: + Control: Text@0.0.50 + Properties: + Font: =Font.'Lato Black' + Text: ="Select Parameters" + VerticalAlign: =VerticalAlign.Middle + Width: =320 + X: =35 + Y: =217 + - app version: + Control: Text@0.0.50 + Properties: + Align: ='TextCanvas.Align'.End + FontColor: =App.Theme.Colors.PrimaryForeground + Size: =10 + Text: ="ver.1.28" + VerticalAlign: =VerticalAlign.Bottom + X: =1270 + Y: =736 + - Button: + Control: Button@0.0.44 + Properties: + BorderRadius: =8 + Font: =Font.Lato + FontSize: =20 + FontWeight: =FontWeight.Normal + Height: =62 + OnSelect: |- + =udfTrace( + DatePicker.SelectedDate, + Slider.Value, + ShowColumns( + Filter( + Gallery.AllItems, + status = "Yes" + ), + title + ), + RadioGroup.SelectedItems, + Dropdown.SelectedItems, + Toggle.Checked, + Rating.Value, + Combobox.SelectedItems, + Checkbox.Checked + ) + Text: ="Send telemetry" + Width: =216 + X: =403 + Y: =616 + - Combobox: + Control: ComboBox@0.0.49 + Properties: + Items: |- + =[ + "Snacks", + "Coffee", + "Beverage", + "Lunch" + ] + SelectMultiple: =true + X: =403 + Y: =505 + - Combobox - header: + Control: Text@0.0.50 + Properties: + Font: =Font.'Lato Black' + Text: ="Select meal preorder" + VerticalAlign: =VerticalAlign.Middle + Width: =320 + X: =403 + Y: =473 + - App header: + Control: Text@0.0.50 + Properties: + Font: =Font.'Lato Black' + Height: =52 + Size: =30 + Text: ="Workplace booking preferences" + VerticalAlign: =VerticalAlign.Middle + Width: =810 + X: =35 + Y: =12 \ No newline at end of file diff --git a/power-apps/udf-trace/source/udf.fx b/power-apps/udf-trace/source/udf.fx new file mode 100644 index 0000000..1545458 --- /dev/null +++ b/power-apps/udf-trace/source/udf.fx @@ -0,0 +1,80 @@ +nfToggleOn = "Reserve a parking space along with the workspace"; +nfToggleOff = "Parking space not required"; + +tpGalleryInputTable := Type([{title: Text}]); // Replace title with your gallery property name + +tpGeneralInputTable := Type([{Value: Text}]); + +udfTrace( + datePicker: Date, + slider: Number, + galleryItems: tpGalleryInputTable, + radio: tpGeneralInputTable, + dropdown: tpGeneralInputTable, + toggle: Boolean, + rating: Number, + combobox: tpGeneralInputTable, + checkbox: Text +):Boolean = +{ + With( + { + // Transform Gallery Items into JSON + wthGalleryItemsJsonOutput: + Concat( + ForAll( + Sequence(CountRows(galleryItems)), + { + tempRowIndex: Value - 1, + tempTitle: Last(FirstN(galleryItems, Value)).title // Replace title with your gallery property name + } + ), + """" & Text(tempRowIndex) & """:""" & Substitute(tempTitle, """", "\""") & """", + "," + ), + + // Transform Gallery Items into String + wthGalleryItemsString: + Concat(galleryItems,title,", "), // Replace title with your gallery property name + + // Transform Combobox Items into JSON + wthComboboxItemsJsonOutput: + Concat( + ForAll( + Sequence(CountRows(combobox)), + { + tempRowIndex: Value - 1, + tempTitle: Last(FirstN(combobox, Value)).Value + } + ), + """" & Text(tempRowIndex) & """:""" & Substitute(tempTitle, """", "\""") & """", + "," + ), + + // Transform Combobox Items into String + wthComboboxItemsString: + Concat(combobox,Value,", ") + }, + Trace( + "udf_trace_demo", + TraceSeverity.Information, + { + datePicker: Text(DateValue(datePicker), DateTimeFormat.ShortDate), + slider: slider, + gallery_items_record: "{" & wthGalleryItemsJsonOutput & "}", + gallery_items_string: wthGalleryItemsString, + radio: Text(First(radio).Value), + dropdown: Text(First(dropdown).Value), + toggle: If( + toggle, + nfToggleOn, + nfToggleOff + ), + rating: rating, + combobox_record: "{" & wthComboboxItemsJsonOutput & "}", + combobox_string: wthComboboxItemsString, + checkbox: checkbox + } + ) + ) +}; \ No newline at end of file