{"record":{"id":"51f0f5f038f498b2","repo":"googleapis/mcp-toolbox","slug":"job-has-no-placement-info","errorCode":null,"errorMessage":"job has no placement info","messagePattern":"job has no placement info","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"internal/sources/dataproc/url.go","lineNumber":68,"sourceCode":"\t\t\tif !stateStartTime.IsZero() {\n\t\t\t\tendTime = stateStartTime\n\t\t\t\tstartTime = stateStartTime.Add(-1 * time.Hour)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn ClusterLogsURL(clusterPb.ProjectId, region, clusterPb.ClusterName, clusterPb.ClusterUuid, startTime, endTime)\n}\n\n// JobConsoleURLFromProto builds a URL to the Google Cloud Console linking to the job page.\nfunc JobConsoleURLFromProto(jobPb *dataprocpb.Job, region string) string {\n\treturn JobConsoleURL(jobPb.Reference.ProjectId, region, jobPb.Reference.JobId)\n}\n\n// JobLogsURLFromProto builds a URL to the Google Cloud Console showing Cloud Logging for the given job.\nfunc JobLogsURLFromProto(jobPb *dataprocpb.Job, region string) (string, error) {\n\tif jobPb.Placement == nil {\n\t\treturn \"\", fmt.Errorf(\"job has no placement info\")\n\t}\n\tclusterName := jobPb.Placement.ClusterName\n\tprojectID := jobPb.Reference.ProjectId\n\tjobID := jobPb.Reference.JobId\n\n\tvar startTime, endTime time.Time\n\t// Find min start time from history\n\tfor _, s := range jobPb.StatusHistory {\n\t\tt := s.StateStartTime.AsTime()\n\t\tif startTime.IsZero() || t.Before(startTime) {\n\t\t\tstartTime = t\n\t\t}\n\t}\n\t// Also check current status\n\tif jobPb.Status != nil {\n\t\tt := jobPb.Status.StateStartTime.AsTime()\n\t\tif startTime.IsZero() || t.Before(startTime) {\n\t\t\tstartTime = t","sourceCodeStart":50,"sourceCodeEnd":86,"githubUrl":"https://github.com/googleapis/mcp-toolbox/blob/8cc6e09de2ad7b8bffc77751799585a1401a48eb/internal/sources/dataproc/url.go#L50-L86","documentation":"JobLogsURLFromProto builds a Console Cloud Logging URL for a job and requires jobPb.Placement.ClusterName. If the Job proto's Placement field is nil, there is no cluster name to embed in the URL, so this sentinel error is returned. It propagates to ToJobs (ListJobs) and GetJob.","triggerScenarios":"Passing a *dataprocpb.Job with Placement == nil to JobLogsURLFromProto — directly in tests (TestJobLogsURLFromProto) or indirectly via ToJobs/GetJob when the API returns a placement-less job.","commonSituations":"Unit tests constructing minimal Job protos without placement; API responses for jobs in edge states lacking placement metadata.","solutions":["Guard the call: check jobPb.Placement != nil before calling JobLogsURLFromProto","In tests, populate dataprocpb.Job{Placement: &dataprocpb.JobPlacement{ClusterName: \"...\"}}","Fall back to a console URL variant that does not require the cluster name when placement is absent"],"exampleFix":"// before\nlogsUrl, err := JobLogsURLFromProto(jobPb, region)\n// after\nvar logsUrl string\nif jobPb.GetPlacement() != nil {\n\tlogsUrl, _ = JobLogsURLFromProto(jobPb, region)\n}","handlingStrategy":"type-guard","validationCode":"if jobPb.GetPlacement() == nil || jobPb.GetPlacement().GetClusterName() == \"\" {\n\treturn \"\", fmt.Errorf(\"job has no placement info\")\n}","typeGuard":"func jobHasPlacement(j *dataprocpb.Job) bool {\n\treturn j != nil && j.GetPlacement() != nil && j.GetPlacement().GetClusterName() != \"\"\n}","tryCatchPattern":"var logsUrl string\nif jobHasPlacement(jobPb) {\n\tlogsUrl, _ = JobLogsURLFromProto(jobPb, region)\n}","preventionTips":["Always use generated getters (GetPlacement) which are nil-safe","In tests, populate Placement with a ClusterName","Prefer degrading to no URL over propagating an error"],"tags":["gcp","dataproc","url-generation","nil-check","missing-metadata"],"backgroundTag":"missing-metadata-field","analyzedSha":"8cc6e09de2ad7b8bffc77751799585a1401a48eb","analyzedAt":"2026-09-05T01:10:36.887Z","contentChangedAt":"2026-09-05T01:10:36.887Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}